Beta. Same behavior as x402 top-up; path is
/agent/mpp/top-up. Overview: Agentic payments.Endpoint
Copy
Ask AI
GET https://api.mobula.io/agent/mpp/top-up?agent_id=<user_id>&amount_usd=<number>
agent_id—user_idfrom subscribe.amount_usd— Must be within API min/max (e.g. min $20 in production configs).
/agent/mpp/subscription first if unsure.
Payment
402 → payamount_usd via MPP → retry → 200 with credits_added, new_credits_limit (same shape as x402).
Reference script
Prerequisites:MPPX_PRIVATE_KEY, AGENT_ID (user_id from subscribe), AMOUNT_USD (minimum 20), optional API_URL, mppx, bun, viem. Run from the monorepo root.
Source: agent-topup.ts
Copy
Ask AI
/**
* MPP agent top-up — pay amount_usd on-chain (min $20) to add credits.
*
* Usage:
* MPPX_PRIVATE_KEY=0x<key> AGENT_ID=<user_id> AMOUNT_USD=50 bun run scripts/src/mpp/agent-topup.ts
*
* Optional: API_URL (default: http://localhost:4058)
* Required: AGENT_ID (user_id from subscribe), AMOUNT_USD (min 20).
*/
import { createPublicClient, formatUnits, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
const API_URL = process.env.API_URL ?? 'https://api.mobula.io';
const AGENT_ID = process.env.AGENT_ID;
if (!AGENT_ID || AGENT_ID.trim() === '') {
console.error('ERROR: Set AGENT_ID=<user_id> (from subscribe response)');
process.exit(1);
}
const AMOUNT_USD = process.env.AMOUNT_USD != null ? Number(process.env.AMOUNT_USD) : 50;
const PATHUSD = '0x20c0000000000000000000000000000000000000' as const;
const PATHUSD_DECIMALS = 6;
const PRIVATE_KEY = process.env.MPPX_PRIVATE_KEY as `0x${string}` | undefined;
if (!PRIVATE_KEY) {
console.error('ERROR: Set MPPX_PRIVATE_KEY=0x<your_private_key>');
process.exit(1);
}
if (Number.isNaN(AMOUNT_USD) || AMOUNT_USD < 20) {
console.error('ERROR: Set AMOUNT_USD=50 (min 20)');
process.exit(1);
}
const payer = privateKeyToAccount(PRIVATE_KEY as `0x${string}`);
const client = createPublicClient({
chain: {
id: 42431,
name: 'Tempo Testnet',
nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: ['https://rpc.moderato.tempo.xyz'] } },
},
transport: http(),
});
const balanceOfAbi = [
{
type: 'function',
name: 'balanceOf',
inputs: [{ name: 'account', type: 'address' }],
outputs: [{ name: '', type: 'uint256' }],
stateMutability: 'view',
},
] as const;
async function getPathUsdBalance(address: `0x${string}`): Promise<bigint> {
return client.readContract({
address: PATHUSD,
abi: balanceOfAbi,
functionName: 'balanceOf',
args: [address],
});
}
function fmtBalance(raw: bigint): string {
return `${formatUnits(raw, PATHUSD_DECIMALS)} PathUSD`;
}
async function getRecipientFromChallenge(url: string): Promise<`0x${string}`> {
const res = await fetch(url);
const wwwAuth = res.headers.get('www-authenticate') || '';
const requestMatch = wwwAuth.match(/request="([^"]*)"/);
if (requestMatch) {
const decoded = JSON.parse(Buffer.from(requestMatch[1], 'base64').toString());
return decoded.recipient;
}
throw new Error('Could not parse recipient from challenge');
}
function mppxRequest(endpoint: string): string {
const { execSync } = require('child_process');
const result = execSync(`MPPX_PRIVATE_KEY=${PRIVATE_KEY} bunx mppx "${endpoint}" -v`, {
encoding: 'utf-8',
cwd: process.cwd(),
timeout: 60_000,
});
return result;
}
const topUpUrl = `${API_URL}/agent/mpp/top-up?agent_id=${encodeURIComponent(AGENT_ID)}&amount_usd=${AMOUNT_USD}`;
const recipient = await getRecipientFromChallenge(topUpUrl);
console.log('\n--- Addresses ---');
console.log(` Payer: ${payer.address}`);
console.log(` Recipient: ${recipient}`);
console.log('\n--- Step 1: Balances BEFORE payment ---\n');
const payerBefore = await getPathUsdBalance(payer.address);
const recipientBefore = await getPathUsdBalance(recipient);
console.log(` Payer: ${fmtBalance(payerBefore)}`);
console.log(` Recipient: ${fmtBalance(recipientBefore)}`);
console.log(`\n--- Step 2: Top-up — $${AMOUNT_USD} ---\n`);
try {
const result = mppxRequest(topUpUrl);
console.log(result);
} catch (err: unknown) {
const execErr = err as { stdout?: string; stderr?: string };
console.error('mppx CLI failed:');
if (execErr.stdout) console.log(execErr.stdout);
if (execErr.stderr) console.error(execErr.stderr);
process.exit(1);
}
console.log('--- Step 3: Balances AFTER payment ---\n');
const payerAfter = await getPathUsdBalance(payer.address);
const recipientAfter = await getPathUsdBalance(recipient);
console.log(` Payer: ${fmtBalance(payerAfter)}`);
console.log(` Recipient: ${fmtBalance(recipientAfter)}`);
console.log('\n--- Balance changes ---\n');
console.log(
` Payer: ${fmtBalance(payerBefore)} → ${fmtBalance(payerAfter)} (${fmtBalance(payerAfter - payerBefore)})`,
);
console.log(
` Recipient: ${fmtBalance(recipientBefore)} → ${fmtBalance(recipientAfter)} (+${fmtBalance(recipientAfter - recipientBefore)})`,
);
process.exit(0);
Copy
Ask AI
MPPX_PRIVATE_KEY=0x<key> AGENT_ID=agt_xxx AMOUNT_USD=50 bun run agent-topup.ts