Pay
Pay any x402-protected URL using your project's wallet
Scope: payments:spend
Pay a single x402-protected URL. Loomal runs the full handshake on your project's wallet: discover the seller's HTTP 402 challenge, check your mandate caps, sign an EIP-3009 USDC transfer authorization off-chain on your Kernel smart account, retry with X-Payment, and record the result.
Your agent never holds a signing key — that lives in the project's wallet, gated by the mandate you set in the Pay tab.
Request
import { Loomal } from "@loomal/sdk";
const loomal = new Loomal({ apiKey: process.env.LOOMAL_API_KEY! });
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 settle hashimport os
from loomal import Loomal
loomal = Loomal(api_key=os.environ["LOOMAL_API_KEY"])
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 settle hashcurl -X POST https://api.loomal.ai/v0/payments/pay \
-H "Authorization: Bearer loid-your-api-key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://seller.example.com/search"
}'The endpoint returns HTTP 200 on ok: true, 402 on payment-related failures, and 503 on platform issues. The body always has the same { ok, ... } shape — branch on ok before reading other fields.
Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | The x402-protected URL to pay. Must respond with HTTP 402 and a valid x402 challenge body. |
dryRun | boolean | No | If true, run mandate + balance checks without signing or moving money. Returns the same success/failure shape with txHash: null. |
Response — ok: true
{
"ok": true,
"status": 200,
"content": { "results": [/* ... */] },
"contentType": "application/json",
"cost": {
"amountUsdc": "0.05",
"amountUsdcRaw": "50000",
"network": "base"
},
"txHash": "0x<onchain-settle-tx-hash>",
"payer": "0x<your-project-wallet>",
"recipient": "0x<seller-wallet>",
"resource": "https://seller.example.com/search",
"balanceAfter": {
"usdc": "9.95",
"usdcRaw": "9950000"
},
"mandate": {
"mandateId": "m_abc123",
"spentTodayUsdcRaw": "50000",
"dailyCapUsdcRaw": "1000000",
"remainingTodayUsdcRaw": "950000",
"validUntil": "2027-01-01T00:00:00.000Z"
}
}| Field | Description |
|---|---|
status | HTTP status the seller returned after settlement (typically 200). |
content | Parsed JSON body if the seller returned application/json. |
contentText | Raw body string when the response wasn't JSON. |
cost.amountUsdcRaw | Amount in raw USDC units (6 decimals). Divide by 1,000,000 for the decimal value. |
txHash | On-chain settle hash on Base. null if dryRun: true. |
balanceAfter | Wallet balance after the call landed. |
mandate.remainingTodayUsdcRaw | Remaining spend room for the current UTC day. |
Response — ok: false
{
"ok": false,
"code": "mandate_per_call_exceeded",
"message": "Price 0.50 USDC exceeds maxPerCallUsdc 0.10",
"hint": "Raise maxPerCallUsdc on the mandate or pay a cheaper endpoint",
"resource": "https://seller.example.com/search",
"cost": { "amountUsdc": "0.50", "network": "base" }
}| Field | Description |
|---|---|
code | Stable identifier — see the error code table below. |
message | Human-readable explanation. |
hint | One-line remediation. |
retryAfterMs | Present on transient failures. Wait this long before retrying. |
cost | The price the seller asked for, when discovered. Absent for failures that occur before the challenge is read. |
Error codes
Group these into mandate, wallet, discovery, settlement, and system buckets. The code field is the durable contract — message and hint may change.
code | Bucket | Cause |
|---|---|---|
mandate_not_found | Mandate | No active mandate on the project's wallet. Create one via POST /v0/payments/mandates or the Pay tab. |
mandate_expired | Mandate | The mandate's validUntil is in the past. |
mandate_revoked | Mandate | The mandate was revoked. |
mandate_per_call_exceeded | Mandate | The seller's price exceeds maxPerCallUsdc. |
mandate_daily_cap_exceeded | Mandate | Today's cumulative spend would exceed dailyCapUsdc. |
session_key_not_installed | Wallet | The session key Loomal uses to sign hasn't been installed on the Kernel account yet. Usually transient on first run. |
session_key_install_failed | Wallet | Installation failed. Retry. |
wallet_not_provisioned | Wallet | The project doesn't have a wallet yet. Top up via the Pay tab to trigger provisioning. |
balance_insufficient | Wallet | Wallet doesn't hold enough USDC to cover the seller's price. |
url_not_x402 | Discovery | The URL didn't return a valid HTTP 402 challenge. |
network_unsupported | Discovery | The seller asked for a chain Loomal doesn't support. |
network_mismatch | Discovery | The seller's network doesn't match the wallet's network. |
payment_response_invalid | Discovery | The seller's X-Payment-Response header was malformed. |
settle_failed | Settlement | On-chain settlement failed (gas, revert, etc.). |
facilitator_unavailable | System | The x402 facilitator is temporarily unavailable. Includes retryAfterMs. Returned as HTTP 503. |
payments_disabled | System | Payments are disabled on this Loomal instance. Returned as HTTP 503. |
unauthorized | System | API key lacks payments:spend scope. |
HTTP Errors
These come from auth and request validation, before the pay flow runs — they throw in the SDK rather than returning { ok: false }.
| Status | error | Cause |
|---|---|---|
| 400 | bad_request | Missing or invalid url |
| 401 | unauthorized | Missing or invalid Authorization header |
| 403 | forbidden | API key lacks payments:spend scope |
Next
GET /v0/payments/activity— see what your project has paid and received.- Pay for tools — buyer flow walkthrough with the reference agent.