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.
GET /agent/x402/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
- Call
GET /agent/x402/subscription with no payment → 402 Payment Required with a payment-required header containing the amount, network, and pay-to address.
- 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:
| Field | Type | Description |
|---|
user_id | string | Agent identifier — use as agent_id for top-up |
api_keys | string[] | API keys linked to the wallet |
plan | string | Active plan name (startup, growth, enterprise, free) |
last_payment | string | null | Timestamp of the most recent payment |
payment_frequency | string | Billing cadence (monthly or yearly) |
left_days | number | Days remaining in the current billing period |
credits_left | number | Credits remaining for API usage |
plan_active | boolean | true 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 x402 Subscribe 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
| Situation | What happens | What to do |
|---|
| 404 response | No agent exists for this wallet | Subscribe first via x402 Subscribe to a plan |
Missing payment-required header | Script exits with error | Retry — usually a transient network issue |
| No Solana/EVM option in 402 | Script exits with error | Confirm API_URL is set to https://api.mobula.io |
| Wrong network | Payment rejected on-chain | Ensure 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:| Variable | Required | Default | Description |
|---|
SOLANA_PRIVATE_KEY | Yes | — | Base58-encoded private key of the paying wallet |
API_URL | No | http://localhost:4058 | API base URL |
What the script does:| Step | Action |
|---|
| 1 | Calls GET /agent/x402/subscription with no payment, receives 402 |
| 2 | Decodes payment details from payment-required header |
| 3 | Signs $0.001 USDC payment |
| 4 | Retries with x-payment header, receives 200 with plan stats |
| 5 | Prints 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}/agent/x402/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);
});
Prerequisites:
- EVM wallet on Base mainnet holding USDC for the $0.001 fee
Run:TEST_PRIVATE_KEY=0x<your_private_key> \
API_URL=https://api.mobula.io \
bun run scripts/src/x402/agent-plan-stats-evm.ts
Environment variables:| Variable | Required | Default | Description |
|---|
TEST_PRIVATE_KEY | Yes | — | Hex-encoded private key of the paying wallet (0x...) |
API_URL | No | http://localhost:4058 | API base URL |
What the script does:| Step | Action |
|---|
| 1 | Calls GET /agent/x402/subscription with no payment, receives 402 |
| 2 | Decodes payment details from payment-required header |
| 3 | Signs $0.001 USDC payment on Base |
| 4 | Retries with x-payment header, receives 200 with plan stats |
| 5 | Prints all subscription fields: user_id, plan, left_days, credits_left, api_keys |
Full script: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',
};
const SUBSCRIPTION_ENDPOINT = `${config.apiUrl}/agent/x402/subscription`;
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: Probe for 402 ─────────────────────────────────────────────────
console.log('Step 1 — Probing subscription endpoint...');
console.log(' Wallet:', account.address);
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 evmOption = decoded.accepts?.find(
(a: { network: string }) => a.network?.startsWith('eip155:') || a.network?.toLowerCase().includes('base'),
);
if (!evmOption) {
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 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-evm.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 Subscription and top-up.