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/top-up adds credits to your agent’s limit. Always call GET /x402/agent/subscription first — if plan_active !== true, subscribe or renew before topping up. Minimum top-up is $20 USDC.

Pricing

Payment is made in USDC on Base or Solana. The full amount_usd goes to Mobula (not a facilitator fee).
ParameterMinimumNotes
amount_usd$20Any amount at or above $20, denominated in USD

Query parameters

ParameterRequiredDescription
agent_idYesYour user_id returned from subscribe or from GET /x402/agent/subscription
amount_usdYesAmount in USD to add as credits (minimum $20)

Request flow

  1. Check status first — Call GET /x402/agent/subscription (costs $0.001 USDC). If plan_active !== true, subscribe or renew before continuing.
  2. Initiate top-up — Call GET /x402/agent/top-up?agent_id=<user_id>&amount_usd=<amount> with no payment → 402 Payment Required with the required USDC amount.
  3. Pay and confirm — Sign the payment and retry with an x-payment header → 200 OK with credits_added and new_credits_limit.

Reference scripts

Run from the monorepo root. Scripts call GET /x402/agent/subscription automatically before topping up.
Example Scripts. Checks plan stats first ($0.001), then pays amount_usd to top up. Uses @solana/kit, @x402/core, @x402/svm. Inline base58ToBytes below; or use the same helper as in the Subscribe guide.
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',
  subscriptionPath: '/x402/agent/subscription',
  topUpPath: '/x402/agent/top-up',
};
const getSubscriptionUrl = () => `${config.apiUrl}${config.subscriptionPath}`;
const getTopUpUrl = (agentId: string, amountUsd: number) =>
  `${config.apiUrl}${config.topUpPath}?agent_id=${encodeURIComponent(agentId)}&amount_usd=${amountUsd}`;

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 PRIVATE_KEY_B58 = process.env.SOLANA_PRIVATE_KEY;
  const AGENT_ID = process.env.AGENT_ID;
  const AMOUNT_USD = process.env.AMOUNT_USD != null ? Number(process.env.AMOUNT_USD) : 20;

  if (!PRIVATE_KEY_B58) {
    console.error('ERROR: Set SOLANA_PRIVATE_KEY (base58)');
    process.exit(1);
  }
  if (!AGENT_ID || AGENT_ID.trim() === '') {
    console.error('ERROR: Set AGENT_ID=<user_id> (from subscribe or from plan stats)');
    process.exit(1);
  }
  if (Number.isNaN(AMOUNT_USD) || AMOUNT_USD < 20) {
    console.error('ERROR: Set AMOUNT_USD=50 (min 20)');
    process.exit(1);
  }

  const signer = await createKeyPairSignerFromBytes(base58ToBytes(PRIVATE_KEY_B58));
  const client = new x402Client();
  registerExactSvmScheme(client, { signer });
  const httpClient = new x402HTTPClient(client);

  // Step 1: plan stats ($0.001); exit if no active plan
  const subscriptionUrl = getSubscriptionUrl();
  console.log('[1] Plan stats (GET /x402/agent/subscription) — pay $0.001');
  const statsProbeRes = await fetch(subscriptionUrl);
  if (statsProbeRes.status !== 402) {
    console.error('    Unexpected status:', statsProbeRes.status, await statsProbeRes.text());
    process.exit(1);
  }
  const statsPaymentRequiredHeader = statsProbeRes.headers.get('payment-required');
  if (!statsPaymentRequiredHeader) {
    console.error('    Missing payment-required header');
    process.exit(1);
  }
  const statsDecoded = JSON.parse(Buffer.from(statsPaymentRequiredHeader, 'base64').toString('utf-8'));
  const statsSvmOption = statsDecoded.accepts?.find((a: { network: string }) => a.network?.startsWith('solana:'));
  if (!statsSvmOption) {
    console.error('    No Solana option in 402');
    process.exit(1);
  }
  const statsPaymentRequired = httpClient.getPaymentRequiredResponse(
    (name) => statsProbeRes.headers.get(name),
    statsDecoded,
  );
  let statsPaymentHeaders: Record<string, string>;
  try {
    const payload = await httpClient.createPaymentPayload(statsPaymentRequired);
    statsPaymentHeaders = httpClient.encodePaymentSignatureHeader(payload);
  } catch (err) {
    console.error('    Plan stats payment failed:', err);
    process.exit(1);
  }
  const statsPaidRes = await fetch(subscriptionUrl, { headers: statsPaymentHeaders });
  const statsBody = await statsPaidRes.text();
  if (statsPaidRes.status === 404) {
    console.error('    No agent for this wallet. Subscribe first.');
    process.exit(1);
  }
  if (statsPaidRes.status === 200) {
    const stats = JSON.parse(statsBody) as { plan_active?: boolean; left_days?: number };
    const active = stats.plan_active === true || (typeof stats.left_days === 'number' && stats.left_days > 0);
    if (!active) {
      console.error('    No active plan. Renew or subscribe first.');
      process.exit(1);
    }
  } else {
    console.error('    Plan stats failed:', statsPaidRes.status, statsBody);
    process.exit(1);
  }
  console.log('    Plan active. Proceeding to top-up.\n');

  // Step 2: GET top-up → 402, then pay amount_usd and resubmit
  const topUpUrl = getTopUpUrl(AGENT_ID, AMOUNT_USD);
  console.log(`[2] GET ${topUpUrl} (no payment)`);
  const probeRes = await fetch(topUpUrl);
  if (probeRes.status !== 402) {
    const text = await probeRes.text();
    console.error('    Unexpected:', probeRes.status, text);
    process.exit(1);
  }
  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 option in 402');
    process.exit(1);
  }
  console.log(`[3] Amount: $${Number(svmOption.amount) / 1_000_000} USDC`);
  console.log('[4] Signing 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('    Payment creation failed:', err);
    process.exit(1);
  }
  console.log(`[5] GET ${topUpUrl} (with payment)`);
  const paidRes = await fetch(topUpUrl, { headers: paymentHeaders });
  const body = await paidRes.text();
  if (paidRes.status !== 200) {
    console.error('Request failed:', paidRes.status, body);
    process.exit(1);
  }
  const data = JSON.parse(body) as { credits_added: number; new_credits_limit: number };
  console.log('Top-up applied.');
  console.log('   credits_added   :', data.credits_added);
  console.log('   new_credits_limit:', data.new_credits_limit);
  process.exit(0);
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});
Prerequisites: Solana wallet with USDC (mainnet: amount_usd + $0.001 for the status check) and SOL for fees. Optional: API_URL (default https://api.mobula.io).
Environment variables:
VariableRequiredDescription
TEST_PRIVATE_KEYYesHex-encoded private key of the paying wallet (0x...)
AGENT_IDYesuser_id returned from subscribe
AMOUNT_USDYesAmount in USD to add as credits (minimum $20)
API_URLNoAPI base URL (default: https://api.mobula.io)
Full walkthrough: x402 Agent Subscription and Top-Up.