LOOMAL
For Buyers

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/sdk
pip install loomal-sdk

2. 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 proof
paid = 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 proof

That'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 10

Output 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:

ToolWhat it doesScope
payments_payPay an x402 URL within the active mandatepayments:spend
payments_mandates_createCreate a spend mandatepayments:spend
payments_mandates_listList mandates for this identitypayments:spend
payments_mandates_getFetch one mandate with live counterspayments:spend
payments_mandates_revokeRevoke a mandatepayments:spend
payments_activityBank-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 responsepaid.txHash, paid.cost, paid.recipient, paid.resource, plus paid.mandate.remainingTodayUsdcRaw so you can see what's left in the daily cap.
  • On BasetxHash is 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 Loomalloomal.payments.activity() (or GET /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.

On this page