> ## 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 Subscribe to a plan

> GET /agent/x402/subscribe — Create or renew an agent subscription. Returns api_key and user_id. Check plan status first; payment is made in USDC.

<Note>
  **Agent endpoints are in beta.** The same operation exists on **MPP**: `GET /agent/mpp/subscribe` with identical query params. Overview: [Agentic payments](/guides/agentic-payments). Scripts (x402): [x402 Subscription and top-up](/guides/x402-subscription-and-topup).
</Note>

`GET /agent/x402/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 /agent/x402/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.

| Plan           | Monthly                 | Yearly                    | Best for                           |
| -------------- | ----------------------- | ------------------------- | ---------------------------------- |
| **Startup**    | \$50                    | \$400                     | Small agents, low-volume usage     |
| **Growth**     | \$400                   | \$4,200                   | Production agents, moderate volume |
| **Enterprise** | \$750 (base) + variable | \$7,200 (base) + variable | High-volume or custom requirements |

Enterprise pricing includes a base fee plus a variable component based on usage. Contact the Mobula team on [Telegram](https://t.me/mobuladevelopers) for a full quote.

***

## Query parameters

| Parameter           | Required | Default | Description                              |
| ------------------- | -------- | ------- | ---------------------------------------- |
| `plan`              | Yes      | —       | One of `startup`, `growth`, `enterprise` |
| `payment_frequency` | Yes      | —       | One of `monthly`, `yearly`               |

***

## Request flow

1. **Check status first** — Call `GET /agent/x402/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 /agent/x402/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):**

```json theme={null}
{
  "api_key": "mbl_xxxxxxxxxxxxxxxxxxxxxxxx",
  "user_id": "agt_xxxxxxxxxxxxxxxxxxxxxxxx"
}
```

| Value     | How to use it                                                 |
| --------- | ------------------------------------------------------------- |
| `api_key` | Pass as the `Authorization` header on all Mobula API requests |
| `user_id` | Use as `agent_id` when calling the top-up endpoint            |

Store both securely. These credentials are tied to your wallet.

***

## Common errors

| Situation                         | What happens                    | What to do                                                                      |
| --------------------------------- | ------------------------------- | ------------------------------------------------------------------------------- |
| Plan already active               | Script exits before subscribing | No action needed — use your existing `api_key`                                  |
| Wrong network in 402              | Payment rejected on-chain       | Ensure your wallet is on Base mainnet or Solana mainnet, not devnet             |
| Subscribed twice by mistake       | Second payment rejected         | Call `GET /agent/x402/subscription` to confirm the first subscription is active |
| Missing `payment-required` header | Script exits with error         | Retry — this is usually a transient network issue                               |
| 402 but no Solana/EVM option      | Script exits with error         | Confirm `API_URL` is set to `https://api.mobula.io`                             |

***

## Reference scripts

Both scripts call `GET /agent/x402/subscription` automatically and skip subscribing if a paid plan is already active.

<Tabs>
  <Tab title="Solana">
    **Prerequisites:**

    * Solana wallet holding USDC (plan price + \$0.001 for the status check) and SOL for transaction fees

    **Environment variables:**

    | Variable             | Required | Default                 | Description                                     |
    | -------------------- | -------- | ----------------------- | ----------------------------------------------- |
    | `SOLANA_PRIVATE_KEY` | Yes      | —                       | Base58-encoded private key of the paying wallet |
    | `PLAN`               | No       | `growth`                | One of `startup`, `growth`, `enterprise`        |
    | `PAYMENT_FREQUENCY`  | No       | `yearly`                | One of `monthly`, `yearly`                      |
    | `API_URL`            | No       | `http://localhost:4058` | API base URL                                    |

    **What the script does:**

    | Step | Action                                                                                              |
    | ---- | --------------------------------------------------------------------------------------------------- |
    | 1    | Calls `GET /agent/x402/subscription`, pays \$0.001 USDC to fetch plan status                        |
    | 2    | If a paid plan is already active, exits without subscribing                                         |
    | 3    | Calls `GET /agent/x402/subscribe?plan=...&payment_frequency=...`, receives 402 with payment details |
    | 4    | Signs the USDC payment for the selected plan                                                        |
    | 5    | Resubmits with `x-payment` header                                                                   |
    | 6    | On success, prints `api_key` and `user_id`                                                          |

    **Full script:**

    ```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',
      plan: process.env.PLAN ?? 'growth',
      paymentFrequency: process.env.PAYMENT_FREQUENCY ?? 'yearly',
    };

    const SUBSCRIPTION_ENDPOINT = `${config.apiUrl}/agent/x402/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}/agent/x402/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);
    });
    ```
  </Tab>

  <Tab title="EVM (Base)">
    **Prerequisites:**

    * EVM wallet on Base mainnet holding USDC (plan price + \$0.001 for the status check)

    ````

    **Full script:**

    ```typescript
    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',
      plan: process.env.PLAN ?? 'startup',
      paymentFrequency: process.env.PAYMENT_FREQUENCY ?? 'monthly',
    };

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

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

    async function main() {
      const privateKey = process.env.TEST_PRIVATE_KEY as `0x${string}` | undefined;
      if (!privateKey) {
        console.error('ERROR: TEST_PRIVATE_KEY is required (hex string, e.g. 0x...)');
        process.exit(1);
      }

      const account = privateKeyToAccount(privateKey);
      const client = new x402Client();
      registerExactEvmScheme(client, { signer: account });
      const httpClient = new x402HTTPClient(client);

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

      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 subscribeEvmOption = subscribeDecoded.accepts?.find(
        (a: { network: string }) => a.network?.toLowerCase().includes('base') || a.network?.startsWith('evm:'),
      );
      if (!subscribeEvmOption) {
        console.error('  No EVM (Base) 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 priceUsdc = Number(subscribeEvmOption.amount) / 1_000_000;
      console.log('\nStep 3 — Payment details (Base):');
      console.log('  network:', subscribeEvmOption.network);
      console.log('  amount :', subscribeEvmOption.amount, `($${priceUsdc.toFixed(2)} USDC)`);
      console.log('  payTo  :', subscribeEvmOption.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...');

      let subscribeRes: Response;
      let subscribeBody: string;
      const maxAttempts = 3;

      for (let attempt = 1; attempt <= maxAttempts; attempt++) {
        console.log('  Attempt', attempt + '/' + maxAttempts + '...');
        subscribeRes = await fetch(subscribePath, { headers: subscribePaymentHeaders });
        subscribeBody = await subscribeRes.text();
        console.log('  Status:', subscribeRes.status);

        if (subscribeRes.status === 200) break;

        console.log('  Failed:', subscribeBody || '(no body)');
        if (subscribeRes.status !== 503 || attempt === maxAttempts) {
          console.error('\n Subscribe failed (HTTP ' + subscribeRes.status + ')');
          if (subscribeBody?.trim()) {
            try {
              const json = JSON.parse(subscribeBody) as Record<string, unknown>;
              const messages = [json.error, json.message, json.errorReason].filter(Boolean);
              if (messages.length > 0) console.error('  Error:', messages.join(' — '));
              else console.error('  Response:', subscribeBody);
            } catch {
              console.error('  Response:', subscribeBody);
            }
          }
          process.exit(1);
        }
        await new Promise((r) => setTimeout(r, 1500));
      }

      // ── 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);
    });
    ````

    **Environment variables:**

    | Variable            | Required | Default                 | Description                                            |
    | ------------------- | -------- | ----------------------- | ------------------------------------------------------ |
    | `TEST_PRIVATE_KEY`  | Yes      | —                       | Hex-encoded private key of the paying wallet (`0x...`) |
    | `PLAN`              | No       | `startup`               | One of `startup`, `growth`, `enterprise`               |
    | `PAYMENT_FREQUENCY` | No       | `monthly`               | One of `monthly`, `yearly`                             |
    | `API_URL`           | No       | `http://localhost:4058` | API base URL                                           |

    **What the script does:**

    | Step | Action                                                                                              |
    | ---- | --------------------------------------------------------------------------------------------------- |
    | 1    | Calls `GET /agent/x402/subscription`, pays \$0.001 USDC to fetch plan status                        |
    | 2    | If a paid plan is already active, exits without subscribing                                         |
    | 3    | Calls `GET /agent/x402/subscribe?plan=...&payment_frequency=...`, receives 402 with payment details |
    | 4    | Signs the USDC payment for the selected plan on Base                                                |
    | 5    | Resubmits with `x-payment` header                                                                   |
    | 6    | On success, prints `api_key` and `user_id`                                                          |
  </Tab>
</Tabs>

***

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