Pay for tools
Your agent pays for paid HTTP endpoints and MCP tools in USDC on Base
You're building an agent that calls paid endpoints — paid search APIs, paid MCP tools, anything that responds with HTTP 402. This page covers what the buyer side looks like, what your agent needs, and how to ship the reference buyer in five minutes.
How it works
Hand Loomal an x402 URL. Loomal runs the handshake on your project's Kernel smart account on Base — checks your mandate caps, signs the EIP-3009 USDC transfer, retries with X-Payment, returns { ok, content, txHash }.
Your agent never holds a signing key; the seller never sees your Loomal API key. The mandate is the spend policy, the chain is the source of truth.
Before you start
- A Loomal project with Pay → Spend turned on. This is typically a separate project from any seller projects you operate.
- A few cents of USDC on Base in that project's wallet. Top up from the Pay tab.
- The seller's URL — must exactly match what they registered with Loomal.
The flow
End-to-end, an SDK-based buyer is three steps: install, set a mandate once, then pay. The SDK does the rest.
1. Install
npm install @loomal/sdkpip install loomal-sdk2. Set a mandate (one-time)
A mandate is the wallet's spend policy — maxPerCallUsdc and dailyCapUsdc. Loomal enforces it server-side on every pay() call, so a runaway loop can't drain the wallet. Create one once (installing the on-chain session key takes 10–30s), then reuse it forever. Skip this step if you already set a mandate in the Pay tab.
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
const { mandates } = await loomal.payments.mandates.list();
let mandate = mandates.find((m) => !m.revokedAt && !m.installError);
if (!mandate) {
mandate = await loomal.payments.mandates.create({
maxPerCallUsdc: "0.10",
dailyCapUsdc: "1.00",
});
if (mandate.installError) throw new Error(mandate.installError);
}import os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
mandates = loomal.payments.mandates.list().get("mandates", [])
mandate = next(
(m for m in mandates if not m.get("revokedAt") and not m.get("installError")),
None,
)
if mandate is None:
mandate = loomal.payments.mandates.create(
max_per_call_usdc="0.10",
daily_cap_usdc="1.00",
)
if mandate.get("installError"):
raise RuntimeError(mandate["installError"])Full mandate reference: payments.mandates.
3. Pay
const paid = await loomal.payments.pay({
url: "https://seller.example.com/search",
});
if (!paid.ok) throw new Error(`${paid.code} — ${paid.message}`);
console.log(paid.content); // what the seller returned
console.log(paid.txHash); // on-chain settlement proofpaid = loomal.payments.pay(url="https://seller.example.com/search")
if not paid["ok"]:
raise RuntimeError(f"{paid['code']} — {paid['message']}")
print(paid["content"]) # what the seller returned
print(paid["txHash"]) # on-chain settlement proofThat's the whole buyer. payments.pay() returns a typed discriminated union — branch on ok. The 17 possible code values for failures are listed in the payments.pay reference.
Or: clone the reference agent
If you'd rather start from a working repo: loomal-ai/loomal-pay-examples has the Node (payment-agent) and Python (payment-agent-python) versions ready to run with .env + npm start / python main.py.
From the CLI
For shell scripts, CI jobs, or one-off testing, the @loomal/cli package exposes the same three steps as commands. Install once with npm install -g @loomal/cli, then:
export LOOMAL_API_KEY=loid-...
# 1. Set a mandate (one-time, ~10–30s while the session key lands on Base).
loomal mandate create --max-per-call 0.10 --daily-cap 1.00
# 2. Pay an x402 URL.
loomal pay https://seller.example.com/search
# 3. Bank-statement-style spend/income feed.
loomal activity --limit 10Output is human-readable tables; add --json to any command for machine-parseable JSON. Mandate management is a full CRUD: loomal mandate list, loomal mandate get <id>, loomal mandate revoke <id>.
loomal pay --dry-run <url> validates your mandate + balance against the seller's price without spending — useful for checking caps before automating.
From MCP (Claude Desktop, Cursor, ChatGPT)
If your agent talks to Loomal via MCP, the same operations are available as tools — no SDK install needed. Wire up Loomal as an MCP server (see MCP setup) and these become callable:
| Tool | What it does | Scope |
|---|---|---|
payments_pay | Pay an x402 URL within the active mandate | payments:spend |
payments_mandates_create | Create a spend mandate | payments:spend |
payments_mandates_list | List mandates for this identity | payments:spend |
payments_mandates_get | Fetch one mandate with live counters | payments:spend |
payments_mandates_revoke | Revoke a mandate | payments:spend |
payments_activity | Bank-statement-style spend/income feed | (none) |
Like the SDK, payments_pay returns a discriminated { ok, ... } body — agents should branch on ok and read code on failure rather than treating non-success as an exception.
Self-hosting MCP? The same tool surface ships in @loomal/mcp, a stdio MCP server you can run locally (npx -y @loomal/mcp).
Tangent — paid MCP tools as a seller: If your agent is consuming an MCP tool that itself charges per call (a paid search MCP, etc.), the MCP client speaks x402 directly with the seller via _meta — no Loomal payments_* tool needed for that case. Loomal sits on the seller side there, via @loomal/sdk/paywall/mcp.
Where the proof lives
You don't need to store anything yourself. Every successful payments.pay() leaves three independent records:
- In the response —
paid.txHash,paid.cost,paid.recipient,paid.resource, pluspaid.mandate.remainingTodayUsdcRawso you can see what's left in the daily cap. - On Base —
txHashis the on-chain USDC transfer. Anyone with the hash can verify amount, payer, and recipient on a block explorer or via RPC. The chain is the canonical record. - In Loomal —
loomal.payments.activity()(orGET /v0/payments/activity) returns your project's full spend history (and income, if you're also a seller) as a bank-statement-style list, latest first. No extra scope required.
The seller's Ed25519-signed receipt is a seller-side artifact (retrievable via GET /v0/payments/:id on the seller's project) — useful when you are the seller reconciling income, not when you're the buyer reconciling spend.
The vault is for secrets — API keys, cards, TOTP. Don't put public on-chain data in it.