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/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 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
- 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.
- 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.
- Sign and pay — Sign the USDC transfer and retry the same request with an
x-payment header.
- Confirm — Server verifies on-chain → 200 OK with
api_key and user_id.
Response (200 OK):
{
"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.
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: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);
});
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 |
Full walkthrough: x402 Subscription and top-up.