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.

agent.ts
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' }] }),
});
Requests are paid in USDG on Robinhood Chain, or free within the daily allowance. Free access for token holders is coming soon.

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.

request · with a key
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.

request · free allowance
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.

request
curl /supported
200 OK · live from this deployment

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

ReasonMeaning
malformed_requestThe body failed x402 v2 schema validation (HTTP 400).
unsupported_x402_versionOnly x402Version: 2 payloads are accepted (HTTP 400).
unsupported_networkThe requirements target a network this deployment doesn't settle on (HTTP 400).
rate_limitedToo many requests: 120 per minute, per IP, per endpoint (HTTP 429).
invalid_exact_evm_*Signature, amount, recipient, window, nonce or balance check failed.
unexpected_*_errorRPC 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.

terminal
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.

terminal
npm i @x402/express @x402/core @x402/evm
server.ts
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.