Skip to main content
x402 agent endpoints are in beta. For the full flow and reference scripts, see x402 Agent Subscription and Top-Up.
GET /x402/agent/subscribe creates or renews your agent and returns an api_key and user_id (also used as agent_id for top-ups). Always call GET /x402/agent/subscription first — if plan_active === true and the plan is not Free, do not subscribe again.

Pricing

Payment is made in USDC on Base or Solana. Prices depend on the plan and billing frequency you choose.
PlanMonthlyYearlyBest for
Startup$50$400Small agents, low-volume usage
Growth$400$4,200Production agents, moderate volume
Enterprise$750 (base) + variable$7,200 (base) + variableHigh-volume or custom requirements
Enterprise pricing includes a base fee plus a variable component based on usage. Contact the Mobula team on Telegram for a full quote.

Query parameters

ParameterRequiredDefaultDescription
planYesOne of startup, growth, enterprise
payment_frequencyYesOne of monthly, yearly

Request flow

  1. Check status first — Call GET /x402/agent/subscription (costs $0.001 USDC). If plan_active === true and the plan is not Free, stop — the wallet is already subscribed.
  2. Initiate subscribe — Call GET /x402/agent/subscribe?plan=...&payment_frequency=... with no payment → 402 Payment Required with the required USDC amount, network, and pay-to address.
  3. Sign and pay — Sign the USDC transfer and retry the same request with an x-payment header.
  4. Confirm — Server verifies on-chain → 200 OK with api_key and user_id.
Response (200 OK):
{
  "api_key": "mbl_xxxxxxxxxxxxxxxxxxxxxxxx",
  "user_id": "agt_xxxxxxxxxxxxxxxxxxxxxxxx"
}
ValueHow to use it
api_keyPass as the Authorization header on all Mobula API requests
user_idUse as agent_id when calling the top-up endpoint
Store both securely. These credentials are tied to your wallet.

Common errors

SituationWhat happensWhat to do
Plan already activeScript exits before subscribingNo action needed — use your existing api_key
Wrong network in 402Payment rejected on-chainEnsure your wallet is on Base mainnet or Solana mainnet, not devnet
Subscribed twice by mistakeSecond payment rejectedCall GET /x402/agent/subscription to confirm the first subscription is active
Missing payment-required headerScript exits with errorRetry — this is usually a transient network issue
402 but no Solana/EVM optionScript exits with errorConfirm API_URL is set to https://api.mobula.io

Reference scripts

Both scripts call GET /x402/agent/subscription automatically and skip subscribing if a paid plan is already active.
Prerequisites:
  • Solana wallet holding USDC (plan price + $0.001 for the status check) and SOL for transaction fees
Environment variables:
VariableRequiredDefaultDescription
SOLANA_PRIVATE_KEYYesBase58-encoded private key of the paying wallet
PLANNogrowthOne of startup, growth, enterprise
PAYMENT_FREQUENCYNoyearlyOne of monthly, yearly
API_URLNohttp://localhost:4058API base URL
What the script does:
StepAction
1Calls GET /x402/agent/subscription, pays $0.001 USDC to fetch plan status
2If a paid plan is already active, exits without subscribing
3Calls GET /x402/agent/subscribe?plan=...&payment_frequency=..., receives 402 with payment details
4Signs the USDC payment for the selected plan
5Resubmits with x-payment header
6On success, prints api_key and user_id
Full script:
import { createKeyPairSignerFromBytes } from '@solana/kit';
import { x402Client, x402HTTPClient } from '@x402/core/client';
import { registerExactSvmScheme } from '@x402/svm/exact/client';

const config = {
  apiUrl: process.env.API_URL ?? 'https://api.mobula.io',
  plan: process.env.PLAN ?? 'growth',
  paymentFrequency: process.env.PAYMENT_FREQUENCY ?? 'yearly',
};

const SUBSCRIPTION_ENDPOINT = `${config.apiUrl}/x402/agent/subscription`;

function base58ToBytes(b58: string): Uint8Array {
  const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
  const map = new Uint8Array(256).fill(255);
  for (let i = 0; i < ALPHABET.length; i++) map[ALPHABET.charCodeAt(i)] = i;
  const bytes: number[] = [0];
  for (const char of b58) {
    const value = map[char.charCodeAt(0)];
    if (value === undefined || value === 255) throw new Error(`Invalid base58: ${char}`);
    let carry: number = value;
    for (let j = bytes.length - 1; j >= 0; j--) {
      carry += (bytes[j] as number) * 58;
      bytes[j] = carry & 0xff;
      carry >>= 8;
    }
    while (carry > 0) {
      bytes.unshift(carry & 0xff);
      carry >>= 8;
    }
  }
  for (const char of b58) {
    if (char !== '1') break;
    bytes.unshift(0);
  }
  return new Uint8Array(bytes);
}

function getSubscribeEndpoint(): string {
  const params = new URLSearchParams({
    plan: config.plan,
    payment_frequency: config.paymentFrequency,
  });
  return `${config.apiUrl}/x402/agent/subscribe?${params.toString()}`;
}

async function main() {
  const privateKeyB58 = process.env.SOLANA_PRIVATE_KEY;
  if (!privateKeyB58) {
    console.error('ERROR: SOLANA_PRIVATE_KEY is not set.');
    console.error('  Provide a base58-encoded private key for a wallet holding USDC and SOL.');
    console.error('  Devnet USDC faucet: https://faucet.circle.com');
    process.exit(1);
  }

  const signer = await createKeyPairSignerFromBytes(base58ToBytes(privateKeyB58));
  const walletAddress = typeof signer.address === 'string' ? signer.address : String(signer.address);

  const client = new x402Client();
  registerExactSvmScheme(client, { signer });
  const httpClient = new x402HTTPClient(client);

  // ── Step 1: Check plan status ─────────────────────────────────────────────
  console.log('Step 1 — Checking plan status...');
  console.log('  Wallet:', walletAddress);

  const statusProbeRes = await fetch(SUBSCRIPTION_ENDPOINT);
  if (statusProbeRes.status !== 402) {
    console.error('  Unexpected response (expected 402):', statusProbeRes.status, await statusProbeRes.text());
    process.exit(1);
  }

  const statusPaymentHeader = statusProbeRes.headers.get('payment-required');
  if (!statusPaymentHeader) {
    console.error('  Missing payment-required header on status check.');
    process.exit(1);
  }

  const statusDecoded = JSON.parse(Buffer.from(statusPaymentHeader, 'base64').toString('utf-8'));
  const statusPaymentRequired = httpClient.getPaymentRequiredResponse(
    (name) => statusProbeRes.headers.get(name),
    statusDecoded,
  );

  let statusPaymentHeaders: Record<string, string>;
  try {
    const payload = await httpClient.createPaymentPayload(statusPaymentRequired);
    statusPaymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
  } catch (err) {
    console.error('  Failed to sign status-check payment:', err);
    process.exit(1);
  }

  const statusRes = await fetch(SUBSCRIPTION_ENDPOINT, { headers: statusPaymentHeaders });
  const statusBody = await statusRes.text();

  if (statusRes.status === 200) {
    const stats = JSON.parse(statusBody) as {
      plan_active?: boolean;
      left_days?: number;
      plan?: string;
    };
    const planName = (stats.plan ?? '').toString().toLowerCase().trim();
    const isActive =
      stats.plan_active === true ||
      (typeof stats.left_days === 'number' && stats.left_days > 0);

    if (isActive && planName !== 'free') {
      console.log('  Paid plan is already active — skipping subscribe.');
      console.log('  Current stats:', statusBody);
      process.exit(0);
    }
    if (isActive && planName === 'free') {
      console.log('  Wallet is on the Free plan. Proceeding to subscribe to a paid plan.');
    }
  }

  if (statusRes.status !== 200) {
    console.log('  No active plan found (404 or expired). Proceeding to subscribe.');
  }

  // ── Step 2: Initiate subscribe ────────────────────────────────────────────
  const subscribePath = getSubscribeEndpoint();
  console.log(`\nStep 2 — Initiating subscribe (${subscribePath})...`);

  const subscribeProbeRes = await fetch(subscribePath);
  if (subscribeProbeRes.status !== 402) {
    console.error('  Unexpected response (expected 402):', await subscribeProbeRes.text());
    process.exit(1);
  }
  console.log('  402 received — payment details follow.');

  const subscribePaymentHeader = subscribeProbeRes.headers.get('payment-required');
  if (!subscribePaymentHeader) {
    console.error('  Missing payment-required header on subscribe probe.');
    process.exit(1);
  }

  const subscribeDecoded = JSON.parse(
    Buffer.from(subscribePaymentHeader, 'base64').toString('utf-8'),
  );
  const subscribeSolanaOption = subscribeDecoded.accepts?.find(
    (a: { network: string }) => a.network?.startsWith('solana:'),
  );
  if (!subscribeSolanaOption) {
    console.error('  No Solana payment option found in 402 response.');
    console.error('  Check that API_URL is set to https://api.mobula.io');
    process.exit(1);
  }

  // ── Step 3: Review payment details ───────────────────────────────────────
  const SOLANA_DEVNET_NETWORK_ID = 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1';
  const networkLabel = subscribeSolanaOption.network === SOLANA_DEVNET_NETWORK_ID ? 'devnet' : 'mainnet';
  const priceUsdc = Number(subscribeSolanaOption.amount) / 1_000_000;

  console.log(`\nStep 3 — Payment details (Solana ${networkLabel}):`);
  console.log('  network:', subscribeSolanaOption.network);
  console.log('  amount :', subscribeSolanaOption.amount, `($${priceUsdc.toFixed(2)} USDC)`);
  console.log('  payTo  :', subscribeSolanaOption.payTo);

  // ── Step 4: Sign payment ──────────────────────────────────────────────────
  console.log('\nStep 4 — Signing payment...');

  const subscribePaymentRequired = httpClient.getPaymentRequiredResponse(
    (name) => subscribeProbeRes.headers.get(name),
    subscribeDecoded,
  );

  let subscribePaymentHeaders: Record<string, string>;
  try {
    const payload = await httpClient.createPaymentPayload(subscribePaymentRequired);
    subscribePaymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
  } catch (err) {
    console.error('  Failed to sign subscribe payment:', err);
    process.exit(1);
  }
  console.log('  Payment signed.');

  // ── Step 5: Submit and confirm ────────────────────────────────────────────
  console.log('\nStep 5 — Submitting payment...');

  const subscribeRes = await fetch(subscribePath, { headers: subscribePaymentHeaders });
  const subscribeBody = await subscribeRes.text();
  console.log('  Status:', subscribeRes.status);

  if (subscribeRes.status !== 200) {
    console.error(`\n❌ Subscribe failed (HTTP ${subscribeRes.status})`);
    if (subscribeBody) {
      try {
        const json = JSON.parse(subscribeBody) as Record<string, unknown>;
        const messages = [json.error, json.message, json.errorReason, json.errorMessage].filter(Boolean);
        if (messages.length > 0) {
          console.error('  Error:', messages.join(' — '));
        } else {
          console.error('  Response:', subscribeBody);
        }
      } catch {
        if (subscribeBody.trim()) console.error('  Response:', subscribeBody);
      }
    } else {
      console.error(
        '  No error details returned. Possible causes:\n' +
        '    - Invalid or already-used payment\n' +
        '    - Wrong network (devnet vs mainnet)\n' +
        '    - Payment not yet confirmed on-chain',
      );
    }
    process.exit(1);
  }

  // ── Step 6: Success ───────────────────────────────────────────────────────
  const result = JSON.parse(subscribeBody) as { api_key: string; user_id: string };

  console.log('\n✓ Subscribed successfully.');
  console.log('  api_key:', result.api_key);
  console.log('  user_id:', result.user_id);
  console.log('\n  Store both values:');
  console.log('    api_key → Authorization header for all Mobula API requests');
  console.log('    user_id → agent_id when calling the top-up endpoint');

  process.exit(0);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

Full walkthrough: x402 Agent Subscription and Top-Up.