Mobula's MPP API is pay-per-call : every request is settled on-chain from your own wallet using USDC.e on the Tempo chain (chainId 4217). No signup, no API key — just a funded wallet.
This skill gives you (the agent) everything needed to:
# 1. Clone the skill (skip if already installed in your skills dir)
git clone https://github.com/Flotapponnier/mpp-skill.git
cd mpp-skill
bun install
# 2. Create a hot wallet (AES-256-GCM encrypted at ~/.mpp-skill/wallet.json,
# encryption key at ~/.mpp-skill/.secret, both chmod 600)
bun run start wallet-create
# → prints address + bridge link
# 3. Fund the wallet with USDC.e on Tempo
# Open: https://relay.link/bridge/tempo?toAddress=<your-address>
# Bridge a few dollars of USDC from any chain (Base, Ethereum, …).
# Tempo's gas token IS USDC, so $1 of USDC.e is enough to make ~2,500 calls.
# 4. Verify the balance
bun run start balance
Once funded, every subsequent call is automatic — no manual signing.
| Command | What it does |
|---|---|
| --- | --- |
bun run start wallet-create | Generate a new Tempo wallet (won't overwrite an existing one) |
bun run start balance | Show wallet address, USDC.e balance, bridge link |
bun run start price | Token price — accepts name, symbol, or contract address |
bun run start wallet | Wallet portfolio positions |
bun run start lighthouse | Trending tokens |
bun run start call | Generic call to any /api/2/* endpoint |
Examples:
bun run start price bitcoin
bun run start price 0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2
bun run start wallet 0xd04b77bb40944110ec9c9e3165f67dadf9d52f21
bun run start lighthouse
bun run start call /api/2/wallet/activity wallet=0xabc...
Import directly from your TypeScript code — useful when each end-user has their own wallet (e.g. a Telegram bot where each Telegram ID gets a per-user encrypted wallet).
import { createUserWallet, getUserWalletAddress } from "mpp-skill/src/wallet";
import { userMobulaCall, getUserTempoBalance } from "mpp-skill/src/mpp/user-mpp";
// On first interaction with a Telegram user
const userId = 1162998296; // Telegram numeric ID
let address = await getUserWalletAddress(userId);
if (!address) {
const w = await createUserWallet(userId);
address = w.address;
// Tell the user to fund it:
// https://relay.link/bridge/tempo?toAddress=${address}
}
// Check funds
const bal = await getUserTempoBalance(userId);
console.log(`User has $${bal?.usd ?? 0} USDC.e on Tempo`);
// Make an API call (paid from the user's own wallet)
const price = await userMobulaCall(userId, "/api/2/token/price", { asset: "bitcoin" });
Per-user wallets are stored at .claude/claudeclaw/wallets/{userId}.json, AES-256-GCM encrypted with a key derived from HMAC-SHA256(WALLET_SECRET, userId). The secret is auto-generated once at .claude/claudeclaw/wallet.secret (chmod 600). Different users cannot decrypt each other's keys.
If you already have a private key and want full control:
import { tempoFetch } from "mpp-skill/src/mpp/tempo-client";
import type { Hex } from "viem";
const data = await tempoFetch(
"/api/2/token/price",
{ asset: "ethereum" },
process.env.TEMPO_PRIVATE_KEY as Hex,
);
GET https://mpp.mobula.io/api/2/token/price?asset=bitcoin with no auth.WWW-Authenticate: Payment id="…", realm="mpp.mobula.io", method="tempo", request="" . The decoded request specifies amount (in USDC.e atoms), currency: USDC.e, recipient, methodDetails.chainId: 4217, and optionally methodDetails.memo if the server pre-computes it.transferWithMemo(recipient, amount, attributionMemo) on USDC.e (0x20c000000000000000000000b9537d11c60e8b50) on Tempo. attributionMemo is NOT the raw challenge id — it's a structured 32-byte memo (see "Memo layout" below) that the server's mppx lib uses to recognize and bind the payment to the challenge.Authorization: Payment , where credential references the tx hash.The skill does steps 2–4 automatically — agents only see the data response.
Mobula uses the official mppx lib server-side. The bytes32 memo passed to transferWithMemo must follow the MPP attribution format, otherwise the server rejects with memo is not bound to this challenge:
| Bytes | Size | Field | Source |
|---|---|---|---|
| ------- | ------ | ------- | -------- |
| 0..4 | 4 | MPP tag | keccak256("mpp")[0..4] (constant 0xef1ed712) |
| 4..5 | 1 | version | 0x01 |
| 5..15 | 10 | server fingerprint | keccak256(realm)[0..10] (realm from WWW-Authenticate) |
| 15..25 | 10 | client fingerprint | zeros for anonymous, else keccak256(clientId)[0..10] |
| 25..32 | 7 | nonce | random 7 bytes, or keccak256(challengeId)[0..7] for a deterministic challenge-bound nonce |
Common mistakes that produce a "memo is not bound" error: passing the raw challengeId (UTF-8 padded to 32), the base64url-decoded challengeId, keccak256(challengeId), or any value that doesn't start with the 4-byte MPP tag.
If methodDetails.memo is present in the challenge JSON, use that hex value directly (server pre-computed it) and skip the layout build.
4217, RPC https://rpc.tempo.xyz)0x20c000000000000000000000b9537d11c60e8b50)| Error | Cause | Fix |
|---|---|---|
| --- | --- | --- |
No wallet found | First-time setup not done | bun run start wallet-create |
Insufficient Tempo balance: you have $0.0000… | Wallet not funded yet | Bridge USDC.e via the printed link |
Could not parse challenge from: … | Server didn't return a Tempo WWW-Authenticate header | Likely an upstream issue — retry or report |
Tempo tx failed: … | RPC error or revert on-chain | Check balance and https://explorer.tempo.xyz for the address |
Mobula's subscription endpoints (/agent/x402/subscribe, etc.) are not currently configured in production. Until they are, this pay-per-call flow is the only way to call MPP API endpoints from an agent. The CLI returns a clear error if you try the legacy subscribe / status / topup / key-create commands.
共 1 个版本