> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
> Use this file to discover all available pages before exploring further.

# x402 Top up credits

> GET /agent/x402/top-up — Add credits to your agent. Requires an active plan; minimum $20 USDC. Check plan status first.

<Note>
  **Agent endpoints are in beta.** MPP equivalent: `GET /agent/mpp/top-up` (same query params). Overview: [Agentic payments](/guides/agentic-payments). x402 scripts: [x402 Subscription and top-up](/guides/x402-subscription-and-topup).
</Note>

`GET /agent/x402/top-up` adds credits to your agent's limit. Always call `GET /agent/x402/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).

| Parameter    | Minimum | Notes                                           |
| ------------ | ------- | ----------------------------------------------- |
| `amount_usd` | \$20    | Any amount at or above \$20, denominated in USD |

***

## Query parameters

| Parameter    | Required | Description                                                                   |
| ------------ | -------- | ----------------------------------------------------------------------------- |
| `agent_id`   | Yes      | Your `user_id` returned from subscribe or from `GET /agent/x402/subscription` |
| `amount_usd` | Yes      | Amount in USD to add as credits (minimum \$20)                                |

***

## Request flow

1. **Check status first** — Call `GET /agent/x402/subscription` (costs \$0.001 USDC). If `plan_active !== true`, subscribe or renew before continuing.
2. **Initiate top-up** — Call `GET /agent/x402/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 /agent/x402/subscription` automatically before topping up.

<Tabs>
  <Tab title="Solana">
    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 [x402 Subscribe to a plan](/guides/x402-agent-subscribe) guide.

    ```typescript theme={null}
    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: '/agent/x402/subscription',
      topUpPath: '/agent/x402/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 /agent/x402/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`).
  </Tab>

  <Tab title="EVM (Base)">
    **Prerequisites:** EVM wallet on Base mainnet with USDC (`amount_usd` + \$0.001 for the status check).

    **Full script:**

    ```typescript theme={null}
    import { x402Client, x402HTTPClient } from '@x402/core/client';
    import { registerExactEvmScheme } from '@x402/evm/exact/client';
    import { privateKeyToAccount } from 'viem/accounts';

    const config = {
      apiUrl: process.env.API_URL ?? 'https://api.mobula.io',
      subscriptionPath: '/agent/x402/subscription',
      topUpPath: '/agent/x402/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}`;

    async function main() {
      const privateKey = process.env.TEST_PRIVATE_KEY as `0x${string}` | undefined;
      const AGENT_ID = process.env.AGENT_ID;
      const AMOUNT_USD = process.env.AMOUNT_USD != null ? Number(process.env.AMOUNT_USD) : 20;

      if (!privateKey) {
        console.error('ERROR: Set TEST_PRIVATE_KEY=0x<your_private_key>');
        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 account = privateKeyToAccount(privateKey);
      const client = new x402Client();
      registerExactEvmScheme(client, { signer: account });
      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 /agent/x402/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 statsEvmOption = statsDecoded.accepts?.find(
        (a: { network: string }) => a.network?.startsWith('eip155:') || a.network?.toLowerCase().includes('base'),
      );
      if (!statsEvmOption) {
        console.error('    No EVM 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);
        if (probeRes.status === 403) {
          console.error('    (403 = e.g. amount_usd below minimum)');
        }
        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 evmOption = decoded.accepts?.find(
        (a: { network: string }) => a.network?.startsWith('eip155:') || a.network?.toLowerCase().includes('base'),
      );
      if (evmOption) {
        console.log('[3] Amount: $' + Number(evmOption.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('\n✓ 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);
    });
    ```
  </Tab>
</Tabs>

**Environment variables:**

| Variable           | Required | Description                                            |
| ------------------ | -------- | ------------------------------------------------------ |
| `TEST_PRIVATE_KEY` | Yes      | Hex-encoded private key of the paying wallet (`0x...`) |
| `AGENT_ID`         | Yes      | `user_id` returned from subscribe                      |
| `AMOUNT_USD`       | Yes      | Amount in USD to add as credits (minimum \$20)         |
| `API_URL`          | No       | API base URL (default: `https://api.mobula.io`)        |

Full walkthrough: [x402 Subscription and top-up](/guides/x402-subscription-and-topup).
