---
name: usdf
description: Buy and spend USDF compute on your own. Wrap USDG into USDF, fund a gateway account, create a capped API key, quote a request, call the OpenAI-compatible endpoint, read receipts, pause and resume a key, or pay per request with x402 and no account. Every figure comes from the gateway, the ledger or the chain.
---

# USDF: autonomous buy-and-spend

USDF is a compute dollar on Robinhood Chain (chain id 4663): 1 USDF = $1 of compute, redeemable 1:1 for USDG at any time. The gateway is OpenAI-compatible and meters every request against a prepaid balance, or takes payment per request with x402. Base URL: `https://api.usdf.fi` (GATEWAY below). Money is counted in base units: 1 unit = $0.000001 = 0.000001 USDF; every amount in a response is `{ "units", "usd", "usdf" }`.

Rules that hold on every path:

- A request is held for its worst case before it runs and billed from the provider's reported usage at the published price sheet. A refusal charges nothing. A request that fails after its hold was taken is refunded whole (`status: "failed_refunded"`).
- Every served request returns a `receipt` and is a leaf of a public usage log; `GET /v1/receipts/{id}` resolves it to its on-chain settlement once settled.
- Never guess a price, a balance or a cost: read `GET /v1/pricing`, the `quote` tool, the `balance` tool and the receipt.

## 0. Addresses, from the gateway

```
GET GATEWAY/v1/reserve      -> token.address (USDF), usdg.address (USDG), chain_id, custody.hot.address, fully_backed
GET GATEWAY/v1/pricing      -> version, models[].id, models[].price {input, output, cached_input} in USD per 1M tokens, models[].available
GET GATEWAY/v1/models       -> data[].id: what a key can call right now
GET GATEWAY/llms.txt        -> the same map in plain text
```

Known on 2026-10-02: USDF `0x4A35a5eC2A50A4d2957B2F2366E017f526cAB318`, USDG `0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168`, Permit2 `0x000000000022D473030F116dDEE9F6B43aC78BA3`, x402 exact proxy `0x402085c248EeA27D92E8b30b2C58ed07f9E20001`, RPC `https://rpc.mainnet.chain.robinhood.com`. Confirm them against `/v1/reserve` and the 402 quote rather than trusting this file.

## 1. Get USDF: wrap USDG

USDF is a stock OpenZeppelin ERC20Wrapper over USDG, 6 decimals, no owner, no fee. Two transactions from a wallet that holds USDG and a little ETH for gas on chain 4663:

1. `USDG.approve(USDF, amount)` (selector `0x095ea7b3`).
2. `USDF.depositFor(yourAddress, amount)` (`depositFor(address,uint256)`, selector `0x2f4f21e2`): pulls `amount` USDG into the token contract and mints `amount` USDF to `yourAddress`.

Redeem at any time with `USDF.withdrawTo(yourAddress, amount)` (`withdrawTo(address,uint256)`, selector `0x205c2878`): burns USDF and returns the same USDG. Both are permissionless and cannot be paused. Check `GET /v1/reserve`: `reserve_usdg.units >= supply.units` and `fully_backed: true`.

## 2. Fund a gateway account

Accounts are issued by the auth service the gateway names; the gateway never sees a password:

```
GET GATEWAY/v1/auth -> { configured, url, publishable_key, sign_in, keys }
```

Sign in (email and password; the account is created on the web app):

```
POST {url}/auth/v1/token?grant_type=password
headers: apikey: {publishable_key}, content-type: application/json
body: {"email": "...", "password": "..."}
-> { access_token, refresh_token, expires_in }
```

`access_token` is the SESSION; send it as `Authorization: Bearer SESSION` to the gateway's account endpoints. Renew with `grant_type=refresh_token` and `{"refresh_token": "..."}` before `expires_in` seconds pass.

Link the wallet that will fund the account (deposits from unlinked addresses are held until that address is linked):

```
POST GATEWAY/v1/account/wallet/nonce   {"address": "0x..."}                 -> { nonce, message, expires_in_seconds }
sign `message` with the wallet (EIP-191 personal_sign)
POST GATEWAY/v1/account/wallet         {"address", "signature", "nonce"}    -> { wallet_address, credited_units }
```

Deposit: `GET GATEWAY/v1/account` returns `deposit.to` (the gateway's hot wallet) and `deposit.token` (USDF). Send USDF from the linked wallet to `deposit.to` with a plain `transfer(address,uint256)`; the balance is credited once the transfer has its confirmations, usually within a minute or two. `GET GATEWAY/v1/account/overview` shows `balance.balance`, `balance.held` (what open requests reserve) and `balance.available`.

Withdraw later with the ledger's `request_withdrawal` RPC on the auth service (`POST {url}/rest/v1/rpc/request_withdrawal`, body `{"p_units": N}`, SESSION bearer and the `apikey` header): minimum 0.10 USDF, at most 3 open and 10 per day, paid to the linked wallet once the account's deposits are final at the chain's safe head (usually 10 to 15 minutes).

## 3. Create a capped key

```
POST {url}/rest/v1/rpc/create_api_key
headers: apikey: {publishable_key}, Authorization: Bearer SESSION, content-type: application/json
body: {"p_label": "agent", "p_budget_units": 5000000, "p_rate_limit_rpm": 60}
-> [{ "key_id": "<uuid>", "api_key": "sk_<64 hex>", "prefix": "sk_..." }]
```

`api_key` is shown once; the ledger keeps its hash. `p_budget_units` is a lifetime spend limit (null for none); at most 20 active keys per account and 20 created per hour. Then set trailing-window caps on the gateway:

```
PATCH GATEWAY/v1/keys/{key_id}
Authorization: Bearer SESSION
{"spend_limit_hour_units": "250000", "spend_limit_day_units": "2000000", "spend_limit_month_units": "20000000", "allowed_models": ["deepseek-v3.2", "qwen*"]}
-> the limits body: spend.hour / spend.day / spend.month {spent_units, limit_units, at_limit}, paused {state, reason, auto_resume}
```

Other fields: `no_logs` (prompt and completion text never written anywhere; token counts still recorded), `private_tier` (attested-hardware routes only; implies `no_logs`), `webhook_url` (https on a public host: every pause and resume of the key is POSTed there as `key.paused` / `key.resumed`, signed with `x-gateway-signature`). `allowed_models` takes model ids or a prefix ending in `*`; `null` allows every model, `[]` none. Unknown fields are refused. Read the state at any time: `GET GATEWAY/v1/keys/{key_id}/limits`. Revoke: `POST {url}/rest/v1/rpc/revoke_api_key {"p_key_id": "<uuid>"}`.

## 4. Quote before calling

No key needed. MCP tool over Streamable HTTP, one JSON-RPC message per POST:

```
POST GATEWAY/mcp
headers: content-type: application/json, accept: application/json, text/event-stream
body: {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"quote","arguments":{"model":"deepseek-v3.2","prompt":"Hello","max_tokens":256}}}
-> SSE: data: {"jsonrpc":"2.0","id":1,"result":{"structuredContent":{ model, price_version, max_tokens, prompt_tokens_held, max_cost {units,usd,usdf}, price, x402 {amount} }}}
```

`max_cost` is the hold: the prompt at two bytes per token plus `max_tokens` at the sheet price. A completed request is billed the provider's reported usage, normally less. `x402.amount` is what the same request costs with no account (prompt at one token per byte, never below $0.01). Over MCP `max_tokens` is capped at 4096; for more, compute the hold from `/v1/pricing`: `ceil((prompt_tokens * input + max_tokens * output) / 1e6)` units with prices in USD per 1M tokens, prompt tokens = ceil(UTF-8 bytes of the messages / 2) + 8 per message + 16.

Balance with the key (the same call with `"name":"balance"` and `"arguments":{}`, header `Authorization: Bearer sk_...`): `available`, `balance`, `held`, `key.budget_left`, `rate_limit_rpm`.

## 5. Call

```
POST GATEWAY/v1/chat/completions
Authorization: Bearer sk_...
{"model":"deepseek-v3.2","messages":[{"role":"user","content":"Hello"}],"max_tokens":256}
-> OpenAI chat completion + "receipt": { request_id, model, provider, status "ok"|"partial", usage {prompt_tokens, completion_tokens, cached_tokens}, price {version, input, output}, cost {units,usd,usdf}, balance {...}|null, settlement {status "unsettled", next_batch_after}, data_policy }
```

The completion's `id` is the request id (also the `x-request-id` header). `max_tokens` bounds the hold; without it the model's default output budget is held, shrinking to what the balance covers. With `"stream": true` the chunks are standard SSE and the receipt is the last comment line, `: receipt {...}`; standard clients drop it, so read it afterwards by id. `"stream_options": {"include_usage": true}` adds one final chunk with the provider's usage.

Privacy per request: `"provider": {"zdr": true}` (no retention), `{"data_collection": "deny"}` (no training), `{"private": true}` (attested hardware, charged the model's `price_private`), or a `:private` / `:cheapest` / `:fastest` suffix on the model id. A request no route can satisfy is refused `404 no_route_for_policy`, nothing charged. A request served on attested hardware carries `data_policy.attestation` (`pending`, then `verified` or `failed` on the lookup) and `attestation_lookup` (`GET /v1/privacy/receipts/{id}` with the same key: the provider's signed receipt, archived).

A job at the upstream's batch rate (half price on the models whose `price` carries `batch_input`): send `"model": "<id>:batch"` (no stream). The answer is `202` with a job `id` (`batch_...`); poll `GET GATEWAY/v1/batches/{id}` until `status` is `done`, then `GET GATEWAY/v1/batches/{id}/result` (the completion with its receipt, `batch: true`).

Refusals are `{ "error": { "message", "type", "code" } }`: `invalid_api_key` 401; `insufficient_balance` 402 (the message says what is available; fund the account, or lower `max_tokens`); `spend_limit_exceeded` 402 (a trailing-window cap; the message says how much was spent in which window); `key_paused` 403 (the owner paused it); `model_not_allowed` 403 (the key's allowlist); `rate_limited` 429 with `retry-after`; `model_not_found` 404; `model_unavailable` 503; `context_length_exceeded` 400; `upstream_rejected` 400 (nothing charged); `upstream_unavailable` 502. Do not retry a 400, 402 or 403 unchanged.

## 6. Read receipts and settlement

```
GET GATEWAY/v1/receipts/{request_id}
Authorization: Bearer sk_...   (the key that made the request)
-> { request_id, model, provider, status, created_at, usage, price_version, cost {units,usd,usdf}, settlement, log }

GET GATEWAY/v1/receipts?limit=20
Authorization: Bearer sk_...   (this key's requests; a SESSION lists the account's)
-> { data: [{ request_id, created_at, model, cost, status, settlement, receipt }], page { next_before } }
```

`settlement.status` is `unsettled` until the next batch (hourly, sooner at 50 USDF unsettled), then `pending` / `sent` / `confirmed` with `batch`, `tx_hash`, `explorer` and `rows` (`GET /v1/settlements/{batch}` lists every row behind the digest in the transaction's calldata). `log` is the request's leaf in the usage log; `GET /v1/log/proof/{request_id}` is its inclusion proof and `GET /v1/log/checkpoints` the roots anchored on-chain.

## 7. Pause and resume

A key pauses by itself at a cap (`hour_limit`, `day_limit`, `month_limit`) or at zero balance, and resumes by itself when the window moves on or a deposit lands; nothing to reset. The owner's switch:

```
PATCH GATEWAY/v1/keys/{key_id}   Authorization: Bearer SESSION   {"paused": true}    -> paused.state true, reason "manual"
PATCH GATEWAY/v1/keys/{key_id}   Authorization: Bearer SESSION   {"paused": false}
```

A manual pause is never lifted by the gateway. While paused, requests get `403 key_paused`.

## 8. x402: pay per request, no account

Ask without paying to get the quote and the accepted payments:

```
POST GATEWAY/x402/v1/chat/completions   {"model":"deepseek-v3.2","messages":[...],"max_tokens":256}
-> 402, header PAYMENT-REQUIRED = base64(JSON): { x402Version: 2, resource, accepts: [
     { scheme "exact", network "eip155:4663", amount "<units>", asset USDF, payTo <hot wallet>, maxTimeoutSeconds 300, extra { assetTransferMethod: "permit2" } },
     { scheme "exact", network "eip155:4663", amount "<units>", asset USDG, payTo <hot wallet>, maxTimeoutSeconds 300, extra { name: "Global Dollar", version: "1" } } ] }
   body: { quote: { amount, accepted: [{symbol, method}] }, x402 }
```

Pay in USDF (Permit2). Once per wallet: `USDF.approve(Permit2, amount)`, an ordinary approve and the only transaction the payer ever sends; read `USDF.allowance(payer, Permit2)` first. Per request, sign EIP-712 with domain `{ name: "Permit2", chainId: 4663, verifyingContract: Permit2 }`, primary type `PermitWitnessTransferFrom`, types `PermitWitnessTransferFrom(TokenPermissions permitted, address spender, uint256 nonce, uint256 deadline, Witness witness)`, `TokenPermissions(address token, uint256 amount)`, `Witness(address to, uint256 validAfter)`, message `{ permitted: { token: USDF, amount }, spender: <x402 exact proxy>, nonce: <random uint256>, deadline: now + maxTimeoutSeconds, witness: { to: payTo, validAfter: 0 } }`. Payload:

```
{ "x402Version": 2, "resource": <from the 402>, "accepted": <the USDF accept, verbatim>,
  "payload": { "signature": "0x...", "permit2Authorization": { "from": payer, "permitted": { "token": USDF, "amount": "<units>" }, "spender": <proxy>, "nonce": "<decimal>", "deadline": "<seconds>", "witness": { "to": payTo, "validAfter": "0" } } } }
```

Pay in USDG (EIP-3009, no approval): domain `{ name: "Global Dollar", version: "1", chainId: 4663, verifyingContract: USDG }`, primary type `TransferWithAuthorization(address from, address to, uint256 value, uint256 validAfter, uint256 validBefore, bytes32 nonce)`, message `{ from: payer, to: payTo, value: amount, validAfter: 0, validBefore: now + maxTimeoutSeconds, nonce: <random bytes32> }`; payload `payload: { signature, authorization: { from, to, value, validAfter: "0", validBefore, nonce } }` with the USDG accept as `accepted`.

Retry the same request with header `PAYMENT-SIGNATURE: base64(JSON payload)`. The gateway settles on-chain first (the payer pays no gas), then serves; the response carries `PAYMENT-RESPONSE` (base64 JSON with `transaction`) and a receipt with `paid` (the quote, which is what was charged), `usage_cost`, and `payment { tx, payer, asset, method }`. Refusals: `permit2_allowance_required` (approve first; the address is refused for a minute after this), `insufficient_funds`, `authorization_used` (a replayed signature), `payment_invalid`, `quote_mismatch` (the quote changed; ask again). If the gateway or provider fails after payment, the full amount is refunded in the asset paid; if the provider rejects the request itself, $0.01 is kept as the settlement fee.

Every other endpoint is paid the same way: the same body as the keyed endpoint, the same two accepts, the same retry with `PAYMENT-SIGNATURE`.

```
POST GATEWAY/x402/v1/completions             {"model", "prompt", "max_tokens"}
POST GATEWAY/x402/v1/embeddings              {"model", "input"}
POST GATEWAY/x402/v1/images/generations      {"model", "prompt", "n", "size"}
POST GATEWAY/x402/v1/images/edits            multipart: model, prompt, image (and mask, n, size)
POST GATEWAY/x402/v1/audio/transcriptions    multipart: model, file (and language, response_format)
POST GATEWAY/x402/v1/audio/speech            {"model", "input", "voice", "response_format"}  -> the audio itself; its receipt by the x-request-id header
POST GATEWAY/x402/v1/rerank                  {"model", "query" or "queries", "documents"}
POST GATEWAY/x402/v1/videos/generations      {"model", "prompt", "seconds"}  -> 202 with a job and its read_token: poll GET GATEWAY/x402/v1/videos/{id} with Authorization: Bearer <read_token> until completed, then GET .../content the same way
GET  GATEWAY/x402/v1/receipts/{request_id}   -> the public receipt of any request, no key: its log line as fields, the payment, the settlement, the proof
```

Each 402 quotes the most the request can cost in its own unit, exactly the hold the keyed endpoint places: tokens at one per UTF-8 byte (embeddings, rerank), the longest the uploaded audio can decode to (transcriptions), the requested or uploaded image size (images), the characters (speech), the clip length asked for (videos); never below $0.01. `quote.units_bound` says how many units. Send a multipart request whole both times: its quote is read from the file. The receipt adds `modality` and `billed { units, unit }`.

Over MCP with no key, `chat` is paid the same way: the call answers `isError` with the payment requirements as `structuredContent` (`x402Version`, `resource`, `accepts`); sign one accept and call again with the payload in `_meta["x402/payment"]` (or as the `payment` argument, or in the `PAYMENT-SIGNATURE` header); the result carries `_meta["x402/payment-response"]`. `receipt` takes any request id and returns its public receipt; `balance` with `{"address": "0x..."}` returns that wallet's USDF and USDG and its USDF allowance to Permit2.
