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/subscription returns current plan stats for the paying wallet. Call this before subscribing or topping up — it prevents duplicate subscriptions and confirms whether a wallet already has an active plan.

Pricing

$0.001 USDC — a facilitator fee for x402 payment-protocol settlement. This does not go to Mobula.

Request flow

  1. Call GET /x402/agent/subscription with no payment → 402 Payment Required with a payment-required header containing the amount, network, and pay-to address.
  2. Sign the required USDC amount and retry with an x-payment header → 200 OK with the response body below.

Response

200 OK — Plan and usage details for the wallet:
FieldTypeDescription
user_idstringAgent identifier — use as agent_id for top-up
api_keysstring[]API keys linked to the wallet
planstringActive plan name (startup, growth, enterprise, free)
last_paymentstring | nullTimestamp of the most recent payment
payment_frequencystringBilling cadence (monthly or yearly)
left_daysnumberDays remaining in the current billing period
credits_leftnumberCredits remaining for API usage
plan_activebooleantrue if the plan is currently active
{
  "user_id": "agt_xxxxxxxxxxxxxxxxxxxxxxxx",
  "api_keys": ["mbl_xxxxxxxxxxxxxxxxxxxxxxxx"],
  "plan": "growth",
  "last_payment": "2025-01-01T00:00:00.000Z",
  "payment_frequency": "yearly",
  "left_days": 312,
  "credits_left": 4800,
  "plan_active": true
}
404 Not Found — No agent exists for this wallet. Subscribe first via Subscribe an Agent to a Plan.

Check before you act

Always call this endpoint first and use the response to decide your next step:
  • Before subscribing: if plan_active === true and the plan is not Free, do not subscribe again — the wallet is already active.
  • Before topping up: if plan_active === false, subscribe or renew first. Top-up requires an active plan.

Common errors

SituationWhat happensWhat to do
404 responseNo agent exists for this walletSubscribe first via Subscribe an Agent to a Plan
Missing payment-required headerScript exits with errorRetry — usually a transient network issue
No Solana/EVM option in 402Script exits with errorConfirm API_URL is set to https://api.mobula.io
Wrong networkPayment rejected on-chainEnsure wallet is on Base mainnet or Solana mainnet, not devnet

Reference scripts

Both scripts pay $0.001 USDC, fetch plan status, and print all subscription fields. Use the same wallet that was used to subscribe.
Prerequisites:
  • Solana wallet holding USDC (mainnet) for the $0.001 fee and SOL for transaction fees
  • Devnet USDC faucet: faucet.circle.com — select Solana devnet
Run:
SOLANA_PRIVATE_KEY="<base58_private_key>" \
API_URL=https://api.mobula.io \
bun run scripts/src/x402/agent-plan-stats-solana.ts
Environment variables:
VariableRequiredDefaultDescription
SOLANA_PRIVATE_KEYYesBase58-encoded private key of the paying wallet
API_URLNohttp://localhost:4058API base URL
What the script does:
StepAction
1Calls GET /x402/agent/subscription with no payment, receives 402
2Decodes payment details from payment-required header
3Signs $0.001 USDC payment
4Retries with x-payment header, receives 200 with plan stats
5Prints all subscription fields: user_id, plan, left_days, credits_left, api_keys
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',
};

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

/**
 * Decode a base58 string (e.g. Solana private key) into raw bytes.
 * Uses the standard Bitcoin/Solana base58 alphabet (no 0, O, I, l).
 */
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);
}

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.');
    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: Probe for 402 ─────────────────────────────────────────────────
  console.log('Step 1 — Probing subscription endpoint...');
  console.log('  Wallet:', walletAddress);
  console.log(`  GET ${SUBSCRIPTION_ENDPOINT}`);

  const probeRes = await fetch(SUBSCRIPTION_ENDPOINT);
  console.log('  Status:', probeRes.status);

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

  const paymentRequiredHeader = probeRes.headers.get('payment-required');
  if (!paymentRequiredHeader) {
    console.error('  Missing payment-required header.');
    process.exit(1);
  }

  const decoded = JSON.parse(Buffer.from(paymentRequiredHeader, 'base64').toString('utf-8'));
  const svmOption = decoded.accepts?.find(
    (a: { network: string }) => a.network?.startsWith('solana:'),
  );
  if (!svmOption) {
    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 2: Sign payment ──────────────────────────────────────────────────
  console.log('\nStep 2 — Signing $0.001 USDC payment...');

  const paymentRequired = httpClient.getPaymentRequiredResponse(
    (name) => probeRes.headers.get(name),
    decoded,
  );

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

  // ── Step 3: Submit and receive plan stats ─────────────────────────────────
  console.log('\nStep 3 — Fetching plan stats...');

  const paidRes = await fetch(SUBSCRIPTION_ENDPOINT, { headers: paymentHeaders });
  const body = await paidRes.text();
  console.log('  Status:', paidRes.status);

  if (paidRes.status === 404) {
    console.error('\n  No agent found for this wallet.');
    console.error('  Subscribe first: bun run scripts/src/x402/agent-subscribe-solana.ts');
    process.exit(1);
  }

  if (paidRes.status !== 200) {
    console.error('\n  Request failed.');
    console.error('  Response:', body || '(empty)');
    process.exit(1);
  }

  // ── Step 4: Print results ─────────────────────────────────────────────────
  const data = JSON.parse(body) as {
    user_id: string;
    api_keys: string[];
    plan: string;
    last_payment: string | null;
    payment_frequency: string;
    left_days: number;
    credits_left: number;
    plan_active: boolean;
  };

  console.log('\n✓ Subscription status retrieved.');
  console.log('');
  console.log('  user_id          :', data.user_id);
  console.log('  plan             :', data.plan);
  console.log('  plan_active      :', data.plan_active);
  console.log('  payment_frequency:', data.payment_frequency);
  console.log('  last_payment     :', data.last_payment ?? '—');
  console.log('  left_days        :', data.left_days);
  console.log('  credits_left     :', data.credits_left);
  console.log('  api_keys         :', data.api_keys.length, 'key(s)');
  data.api_keys.forEach((k, i) => {
    console.log(`    [${i + 1}]`, k);
  });

  process.exit(0);
}

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

Full walkthrough: x402 Agent Subscription and Top-Up.