Response envelope
Every /api/* response, success or failure, wraps its payload in three fields: data, error, meta.
{
"data": { ... },
"error": null | { "code": <HTTP status int>, "message": "string" },
"meta": { ... }
}
Success
{
"data": {
"chain_id": "0x1",
"block_number": "0x14abcde",
"gas_price": "0x3b9aca00"
},
"error": null,
"meta": {
"endpoint": "/api/chain/network-info",
"request_id": "00000000abcdef01",
"cost_usdc": "0.001",
"payment_chain": "base",
"payment_token": "USDC"
}
}
data: the endpoint's actual response body. Shape varies by endpoint; see the API Reference for each.error:nullon success.meta: per-call telemetry; see below.
Failure
{
"data": null,
"error": {
"code": 400,
"message": "address param required"
},
"meta": {
"endpoint": "/api/chain/live-balance",
"request_id": "00000000abcdef02"
}
}
dataisnull.error.codeis the HTTP status as an integer; branch on it like you would on the status line.error.messageis human-readable English from the handler. Log it, don't parse it.meta.request_idis logged on the server side; include it in support requests.
Common statuses:
| Status | Typical cause |
|---|---|
200 | Success |
400 | Missing or malformed parameter |
401 | Missing or invalid Authorization (returned by the gateway, not the envelope shape; see Error codes) |
402 | Payment required (x402 / MPP challenge; see Wallet setup) |
403 | Sandbox key on a paid endpoint, or subscription doesn't include this endpoint (gateway shape) |
404 | Resource not found on-chain (transaction, block, NFT, etc.) |
5xx | Upstream RPC node, facilitator, or unhandled service-side failure |
429 is not currently emitted; rate limits are advisory via headers today. See Rate limits and quotas.
meta fields
| Field | Notes |
|---|---|
endpoint | Always present. The matched route, e.g. /api/chain/network-info. |
request_id | Always present. Hex identifier; mirrors the server-side log entry. |
cost_usdc | Present when the call was paid (x402 / MPP). The exact USDC amount settled. |
payment_chain | Present on paid calls: base for x402, tempo for MPP. |
payment_token | Present on paid calls: USDC for x402, USDC.e or pathUSD for MPP. |
Subscription Bearer-key calls don't emit cost_usdc / payment_chain / payment_token because settlement is billed monthly by Stripe, not per-call.
Patterns
Branch on HTTP status, not the message
error.code mirrors the HTTP status. error.message is freeform handler text that may change wording; never parse it.
const { data, error } = await res.json();
if (error) {
switch (error.code) {
case 400: throw new ValidationError(error.message);
case 404: return null; // resource not found on-chain
case 402: return handlePaymentChallenge(res);
default: if (error.code >= 500) return retryWithBackoff();
throw new UnknownAPIError(error);
}
}
return data;
The gateway uses a different shape (flat {error, message}) for auth and routing failures. See Error codes for the full pattern that handles both.
Always log request_id
When something looks off, having the meta.request_id in your logs lets the team find the corresponding server-side trace in one query. This is the single most useful thing to log.
Treat data as the only payload
Don't depend on error being absent; always read error first and fall through to data only when error === null. The envelope is uniform across success and failure, so you can decode it once and dispatch.
Full error reference
The two error shapes (envelope and gateway), HTTP status meanings, and recovery patterns are documented in Guides → Error codes.