Tally API
Tally is a pay-per-request API on Robinhood Chain. Agents pay per call with x402: a 402 names the price, the agent signs a Permit2 transfer of USDG, Tally verifies it and settles it on-chain. No account, no API key.
…Agent quickstart: pay per request
Any x402 v2 client works against POST /api/v1/chat. USDG on Robinhood Chain isn't one of the SDK's default assets yet, so allow it in spendControls. USDG pays through Permit2: the wallet approves the Permit2 contract once (a small ETH fee), and after that every payment is a signature and Tally pays the gas.
import { privateKeyToAccount } from 'viem/accounts'; import { x402Client, wrapFetchWithPayment } from '@x402/fetch'; import { ExactEvmScheme } from '@x402/evm/exact/client'; const account = privateKeyToAccount(process.env.AGENT_KEY as `0x${string}`); const USDG = '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168'; const client = x402Client.fromConfig({ // Robinhood Chain schemes: [{ network: 'eip155:4663', client: new ExactEvmScheme(account) }], spendControls: { allowedAssets: [ { network: 'eip155:4663', asset: USDG, maxAmountPerPayment: '1000000' }, // ≤ 1 USDG ], }, }); const pay = wrapFetchWithPayment(fetch, client); const res = await pay('…/api/v1/chat', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'hi' }] }), });
API keys: fund once, let the agent spend
A key carries a prepaid USDG balance. Fund it with an x402 payment to POST /api/v1/credit/{amount} (amounts: 1, 5, 20 USDG): any x402 client can pay it, and the demo page does it with a browser wallet. Without a key in the request a new key is issued; with Authorization: Bearer tally_sk_… the credit lands on that key.
200 OK { "api_key": "tally_sk_…", // only when a new key was issued "key_id": "…", "balance": "1000000", // 6-decimal USDG units → 1 USDG "price_per_request": "5000", "requests_left": 200, "base_url": "…/api/v1" }
Every request with the key debits price_per_request and returns the remainder in a Tally-Balance header (and the charge in Tally-Cost). An empty key gets 402 insufficient_balance; an altered key gets 401 invalid_api_key. GET /api/v1/me with the key returns balance, spent and request count. The OpenAI SDK works unchanged: set baseURL and apiKey.
curl …/api/v1/chat/completions \
-H 'Authorization: Bearer tally_sk_…' -H 'content-type: application/json' \
-d '{"model":"tally-router","messages":[{"role":"user","content":"hi"}],"stream":false}'Models and their per-request prices are listed at GET /api/v1/models; stream: true returns server-sent events in the OpenAI chunk format. Keys are signed tokens: copy yours when it is shown, it is not stored in the clear anywhere.
OpenAPI
The full spec is served at /api/v1/openapi.json. Paste that URL into any OpenAPI client or an agent framework's tool loader to get the endpoints, parameters and auth scheme.
The router endpoint
An OpenAI-compatible chat completion at POST /api/v1/chat. It costs 0.005 USDG per request. Every IP gets 5 free requests a day by sending Tally-Free: 1; once spent, the same request returns 402 and any x402 client pays as usual.
curl …/api/v1/chat \
-H 'Tally-Free: 1' -H 'content-type: application/json' \
-d '{"messages":[{"role":"user","content":"Explain HTTP 402 in one sentence."}],"max_tokens":200}'200 OK Tally-Free-Remaining: 4 Tally-Free-Resets: 2026-09-17T15:00:00.000Z { "id": "chatcmpl-…", "object": "chat.completion", "model": "tally-router", "choices": [{ "index": 0, "message": { "role": "assistant", "content": "…" }, "finish_reason": "stop" }], "usage": { "prompt_tokens": 12, "completion_tokens": 31, "total_tokens": 43 } }
Body: messages (up to 32, roles system / user / assistant, 8,000 characters total) and optional max_tokens (1–512). Paying clients use the agent quickstart above with method: 'POST'; the payment path is identical to any other x402 route. A deployment without an upstream model configured returns "simulated": true replies so the payment path can still be exercised.
Networks
The networks this deployment settles on right now.
GET /supported
The payment kinds Tally accepts and the relayer address that submits settlements.
curl …/supported…
POST /verify
Checks the signature, amount, recipient, time window and payer balance, then simulates the transfer. It never changes chain state, so call it before doing expensive work.
POST …/verify Content-Type: application/json { "x402Version": 2, "paymentPayload": { "x402Version": 2, "accepted": { /* the requirement the client chose */ }, "payload": { "authorization": { "from": "0xPayer", "to": "0xMerchant", "value": "10000", "validAfter": "0", "validBefore": "1760000000", "nonce": "0x…32 bytes" }, "signature": "0x…" } }, "paymentRequirements": { "scheme": "exact", "network": "eip155:4663", "amount": "10000", "asset": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168", "payTo": "0xMerchant", "maxTimeoutSeconds": 60, "extra": { "assetTransferMethod": "permit2" } } }
200 OK { "isValid": true, "payer": "0xPayer" }POST /settle
Same body as /verify. Tally verifies again, simulates, submits the Permit2 transfer through the x402 proxy and waits up to 60 s for the receipt. USDG moves directly from the payer to payTo. Settling the same signature twice fails with invalid_exact_evm_nonce_already_used; the nonce is spent on-chain.
200 OK
{
"success": true,
"payer": "0xPayer",
"transaction": "0x5b1c…e9a2",
"network": "eip155:4663"
}GET /health
Whether each enabled network's RPC is reachable, with its latest block. 200 when everything is healthy, 503 otherwise. The same data is drawn on the status page.
Errors & limits
| Reason | Meaning |
|---|---|
| malformed_request | The body failed x402 v2 schema validation (HTTP 400). |
| unsupported_x402_version | Only x402Version: 2 payloads are accepted (HTTP 400). |
| unsupported_network | The requirements target a network this deployment doesn't settle on (HTTP 400). |
| rate_limited | Too many requests: 120 per minute, per IP, per endpoint (HTTP 429). |
| invalid_exact_evm_* | Signature, amount, recipient, window, nonce or balance check failed. |
| unexpected_*_error | RPC or relayer problem. Safe to retry (HTTP 500). |
Self-hosting
Tally is one Node process. Generate a relayer key, fund it with a little ETH on Robinhood Chain for gas, start it.
npm install npm run gen-key # writes RELAYER_PRIVATE_KEY to .env and prints the address npm start # http://localhost:4020 npm test # the full payment flow against a local chain
Charging for your own routes (optional)
The facilitator Tally uses for its own settlement is a standard x402 v2 facilitator and it is public, so you can put a price on routes of your own with the x402 server packages. Register the exact EVM scheme for Robinhood Chain and point the facilitator client at it. The SDK has no default asset for Robinhood Chain yet, so each route names its USDG amount explicitly and marks it as a Permit2 asset.
npm i @x402/express @x402/core @x402/evm
import express from 'express'; import { paymentMiddleware } from '@x402/express'; import { x402ResourceServer, HTTPFacilitatorClient } from '@x402/core/server'; import { ExactEvmScheme } from '@x402/evm/exact/server'; const facilitator = new HTTPFacilitatorClient({ url: '…' }); const server = new x402ResourceServer(facilitator) .register('eip155:4663', new ExactEvmScheme()); const app = express(); app.use(paymentMiddleware({ 'GET /weather': { accepts: { scheme: 'exact', network: 'eip155:4663', // Robinhood Chain payTo: '0xYourMerchantAddress', price: { amount: '10000', // 0.01 USDG (6 decimals) asset: '0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168', extra: { assetTransferMethod: 'permit2' }, }, }, description: 'Current weather', }, }, server)); app.get('/weather', (_req, res) => res.json({ temp: 24 })); app.listen(4021);
The middleware writes the 402, hands the signed payment to the Tally /verify and /settle, and only then runs your handler. Your handler never sees a payment.