**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}/x402/agent/subscription`;
function getSubscribeEndpoint(): string {
const params = new URLSearchParams({
plan: config.plan,
payment_frequency: config.paymentFrequency,
});
return `${config.apiUrl}/x402/agent/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);
});