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 fullamount_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 /x402/agent/subscription |
amount_usd | Yes | Amount in USD to add as credits (minimum $20) |
Request flow
- Check status first — Call
GET /x402/agent/subscription(costs $0.001 USDC). Ifplan_active !== true, subscribe or renew before continuing. - 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. - Pay and confirm — Sign the payment and retry with an
x-paymentheader → 200 OK withcredits_addedandnew_credits_limit.
Reference scripts
Run from the monorepo root. Scripts callGET /x402/agent/subscription automatically before topping up.
- Solana
- EVM (Base)
Example Scripts. Checks plan stats first ($0.001), then pays Prerequisites: Solana wallet with USDC (mainnet:
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.Copy
Ask AI
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);
});
amount_usd + $0.001 for the status check) and SOL for fees. Optional: API_URL (default https://api.mobula.io).Prerequisites: EVM wallet on Base mainnet with USDC (
amount_usd + $0.001 for the status check).Full script:Copy
Ask AI
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: '/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}`;
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 /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 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);
});
| 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) |