Get started
Accounts, keys and balances.
Sign in, link a wallet, fund a balance, and give each key its own limits.
On this page
Sign in at /auth. Each section below names the dashboard tab that does the same with no code.
The samples show the calls themselves. Each one takes the signed-in session's access token as a Bearer token:
Authorization: Bearer <session token>
- Samples read the token from
$SESSION. The SDK readsUSDF_SESSION_TOKEN, and the command line stores one withusdf login --token -. GET /v1/authnames the sign-in in force and the endpoints it takes.- A session lasts about an hour. An expired or signed-out one gets
401: sign in again. - An API key cannot manage keys, withdraw or create links. Those need a session.
A linked wallet is where deposits come from and withdrawals go to. Linking it takes one signature. On the dashboard: Balance.
1. Ask for the message
Request
curl -X POST https://api.usdf.fi/v1/account/wallet/nonce \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{"address": "<wallet address>"}'Response
{ "nonce": "<nonce>", "message": "<message to sign>", "expires_in_seconds": … }2. Sign it
Sign message with personal_sign from the wallet.
3. Link the wallet
Request
curl -X POST https://api.usdf.fi/v1/account/wallet \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{
"address": "<wallet address>",
"signature": "<personal_sign of the message>",
"nonce": "<nonce>"
}'Response
{
"wallet_address": "<wallet address>",
"credited_units": "<units>",
"signature": { "kind": "eoa" }
}The rules:
- One wallet per account, and a wallet belongs to one account.
- It can be replaced only while the account and the agents it owns together hold under 0.10 USDF. Withdraw first, then link the new wallet.
- Contract wallets (a Safe, a smart account) link through ERC-1271.
- USDF sent from a wallet that is not linked is held until that wallet is linked.
Five ways to add to the balance. Sending USDF is on the dashboard under Balance.
| Way | How | Credited |
|---|---|---|
| Send USDF | From the linked wallet to deposit.to in GET /v1/account | After confirmations, usually within a minute or two |
| Add USDG without gas | GET /v1/account/deposit/usdg returns typed data to sign; POST it back with the signature | 200 credited, or 202 while the gateway finishes it |
| Automatic top-up | Approve USDF to Permit2 once, then sign one Permit2 allowance. The gateway pulls when the balance runs low. /v1/topup, /permit, /pulls, /clear | As each pull confirms |
| The Buy page | /buy | As the transaction confirms |
| Any token | Pay with any token | As the swap confirms |
The USDG deposit needs at least 1 USDG and costs no fee: the gateway pays the gas of both transactions. GET /v1/account/overview shows balance, held and available.
Withdraw unspent balance to the linked wallet, as USDF. The minimum is 0.10 USDF. On the dashboard: Balance.
Request a withdrawal
curl -X POST https://api.usdf.fi/v1/account/withdrawals \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{"units": "1000000"}'Response
{
"withdrawal_id": "<id>",
"amount": { "units": "1000000", "usd": "1.000000", "usdf": "1.000000" },
"note": "Sent to the linked wallet once the account's deposits are final …"
}A withdrawal is sent once the account's deposits are final: the chain's safe head, the newest block it treats as settled, has passed them. That is usually 10 to 15 minutes after the request. An account has at most 3 open and 10 per day. GET /v1/account/withdrawals lists them.
Create keys on the dashboard (API Keys), or with the API under a session. A key is shown once; only its hash is kept.
Create a key
curl -X POST https://api.usdf.fi/v1/account/keys \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{
"label": "research",
"budget_units": "5000000",
"rate_limit_rpm": 60
}'Response, 201
{
"object": "api_key",
"key_id": "<key_id>",
"api_key": "sk_…",
"prefix": "<prefix>",
"note": "Shown once: store the key now. It spends from this account's balance."
}The key endpoints:
| Endpoint | What it does |
|---|---|
POST /v1/account/keys | Creates a key: label, budget_units (a lifetime spend limit), rate_limit_rpm. |
GET /v1/account/keys | Active keys. ?state=revoked lists revoked ones; ?count=1 counts them. |
PATCH /v1/account/keys/{id} | Changes label, budget_units or rate_limit_rpm; clear_budget removes the budget. |
DELETE /v1/account/keys/{id} | Revokes the key. |
An account holds at most 20 active keys, with 20 keys created per hour. The requests-per-minute limit is set per key: read live from GET /v1/limits.
Each key can carry hourly, daily and monthly caps, a model allowlist, a pause, privacy flags and a webhook. Set them with PATCH /v1/keys/{id} under the session. On the dashboard: API Keys, then Key controls.
| Parameter | Type | Notes |
|---|---|---|
spend_limit_hour_units, spend_limit_day_units, spend_limit_month_units | String of units, or null | Trailing hour, day and 30 days. Null clears the cap. |
allowed_models | Array, or null | Model ids, or a prefix ending in *. Null allows every listed model; [] allows none. |
paused | Boolean | A manual pause is never lifted by the gateway. Requests get 403 key_paused. |
no_logs | Boolean | Prompt and completion text are never written anywhere. |
private_tier | Boolean | Every request runs on attested routes. Implies no_logs. |
webhook_url | String, or null | Https on a public host. Receives pause and resume events. |
Unknown fields are refused. A request for a model the list does not allow gets 403 model_not_allowed and costs nothing.
Set caps and an allowlist
curl -X PATCH https://api.usdf.fi/v1/keys/<key_id> \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{
"spend_limit_hour_units": "1000000",
"spend_limit_day_units": "5000000",
"spend_limit_month_units": "50000000",
"allowed_models": ["<model>", "qwen*"],
"webhook_url": "https://example.com/usdf-hooks"
}'Pause a key
curl -X PATCH https://api.usdf.fi/v1/keys/<key_id> \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{"paused": true}'Read a key's state with GET /v1/keys/{id}/limits:
Read a key's limits
curl https://api.usdf.fi/v1/keys/<key_id>/limits \ -H "Authorization: Bearer $SESSION"
Response, trimmed
{
"key_id": "<key_id>",
"spend": {
"hour": {
"window": "…", "spent_units": "<units>", "limit_units": "1000000",
"remaining_units": "<units>", "at_limit": false
},
"day": { … },
"month": { … }
},
"models": { "allowed": ["<model>", "qwen*"], "enforced": true, … },
"paused": { "state": false, "at": null, "reason": null, "auto_resume": null },
"privacy": { "no_logs": false, "private_tier": false, … },
"webhook": { "url": "https://example.com/usdf-hooks", … }
}A key pauses by itself at a cap or at zero balance, and resumes by itself:
| Reason | Resumes when |
|---|---|
zero_balance | A deposit lands. |
hour_limit, day_limit, month_limit | The trailing window moves on. |
manual | The owner sets paused to false. |
The gateway posts JSON events to https URLs you set. On the dashboard, set a key's under API Keys and the low-balance alert's under Settings. An agent's is set with PATCH /v1/agents/{slug}.
| Event type | Set with | Carries | Deduplicate on |
|---|---|---|---|
key.paused, key.resumed | webhook_url on PATCH /v1/keys/{id} | The key, the reason, the trailing spend and the caps | event_id |
approval.decided | The agent's webhook_url | The agent, the action and the owner's decision (status) | approval_id |
batch.done, batch.failed, batch.expired | The job's webhook_url | The job, its cost, and its result once done | id, the job id |
balance.low | low_balance_units and low_balance_webhook_url in the account settings | The available balance and the threshold | event_id |
- Each event is a JSON POST. A success status is delivery; anything else, a redirect included, is a failure.
- Failed deliveries are retried, then given up. Deduplicate on the field above: a retry can repeat an event.
- A URL must be https on a public host.
One body of each kind. A key.resumed event has the same fields, with spend set to null. Money is { "units", "usd", "usdf" }, as everywhere.
key.paused
{
"type": "key.paused",
"event_id": "<event id>",
"key_id": "<key_id>",
"key_prefix": "<prefix>",
"label": "research",
"reason": "day_limit",
"spend": { "hour": { … }, "day": { … }, "month": { … } },
"limits": { "hour": { … }, "day": { … }, "month": { … } },
"month_read_at": "<time>",
"at": "<time>",
"gateway": "https://api.usdf.fi"
}approval.decided
{
"type": "approval.decided",
"approval_id": "<id>",
"agent_id": "<agent id>",
"action": "Run the weekly research batch",
"status": "approved",
"decided_at": "<time>",
"gateway": "https://api.usdf.fi"
}batch.done, trimmed
{
"type": "batch.done",
"id": "batch_…",
"status": "done",
"model": "<model>",
"provider": "<provider>",
"usage": { … },
"cost": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"error": null,
"result": { "choices": [ … ], "receipt": { … }, … },
"poll": "https://api.usdf.fi/v1/batches/batch_…",
"result_url": "https://api.usdf.fi/v1/batches/batch_…/result",
"receipt": "https://api.usdf.fi/v1/receipts/batch_…",
"ended_at": "<time>",
"gateway": "https://api.usdf.fi"
}balance.low
{
"type": "balance.low",
"event_id": "<event id>",
"account_id": "<account id>",
"available": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"threshold": { "units": "<units>", "usd": "<usd>", "usdf": "<usdf>" },
"at": "<time>",
"gateway": "https://api.usdf.fi",
"note": "…"
}Before acting on an event, read the state it reports from the gateway itself:
| Event | Read |
|---|---|
key.paused, key.resumed | GET /v1/keys/{id}/limits |
approval.decided | GET /v1/agents/{slug}/approvals |
batch.done, batch.failed, batch.expired | GET /v1/batches/{id} |
balance.low | GET /v1/account/overview |
GET /v1/account/settings reads the account's defaults and PATCH changes them. They apply to the next request from every key on the account. On the dashboard: Settings.
| Field | Effect |
|---|---|
zdr | Zero data retention: only routes whose published policy keeps no request content. |
data_collection | "deny": only routes that do not train on content. |
sort | "price" or "latency": the order routes are tried in. |
provider_allow, provider_deny | Provider ids to allow or exclude. |
allow_fallbacks | false: the first route only, with no failover. |
max_price | { input, output } in USD per 1M tokens. |
no_logs_default, private_tier_default | Flags for keys created after they are set. |
low_balance_units, low_balance_webhook_url | The low-balance alert. |
Change the defaults
curl -X PATCH https://api.usdf.fi/v1/account/settings \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{
"zdr": true,
"data_collection": "deny",
"sort": "price",
"no_logs_default": true,
"low_balance_units": "2000000",
"low_balance_webhook_url": "https://example.com/usdf-hooks"
}'A request's own provider field overrides these.
Everything the dashboard shows is an endpoint. All take the session. On the dashboard: Activity and Analytics.
| Endpoint | Returns |
|---|---|
GET /v1/account/overview | Balance, held, available, recent spend, top models, the last deposit and withdrawal. |
GET /v1/account/activity, .csv | Every request, newest first, with filters: key, model, status, from, to. |
GET /v1/account/usage, /usage/unsettled | Usage rows, and what is not yet in a settlement batch. |
GET /v1/account/deposits | Deposits, newest first. |
GET /v1/account/withdrawals | Withdrawals, newest first. |
GET /v1/statements/{YYYY-MM}.csv | A monthly statement, also as .pdf or .json. |
GET /v1/analytics, .csv | Spend by day, model and tool. ?days= and ?key=. A key can read its own. |
A team (an org) is a balance its members share. Its keys spend from the team's balance. Every team route takes a session. On the dashboard: Teams.
| Endpoint | What it does |
|---|---|
GET /v1/orgs | The teams you belong to. |
POST /v1/orgs | Creates a team with a name. You become its owner. |
GET /v1/orgs/{id} | Members, invites, keys, usage and funding. |
POST /v1/orgs/{id}/members | Invites by email or account_id, as admin or member, or changes a member's role. Owner or admin. |
GET /v1/orgs/invites | The open invites addressed to you. |
POST /v1/orgs/invites/{invite}/accept, /decline | Joins the team with the invite's role, or declines. |
DELETE /v1/orgs/{id}/invites/{invite} | Withdraws an open invite. Owner or admin. |
DELETE /v1/orgs/{id}/members/{account_id} | Removes a member (owner or admin), or leaves (yourself). |
POST /v1/orgs/{id}/fund | Moves units of your available balance into the team's. Owner or admin. |
POST /v1/orgs/{id}/defund | Moves units of the team's available balance back to the owner. Owner. |
POST /v1/orgs/{id}/keys | Creates a team key: label, budget_units, rate_limit_rpm. Owner or admin. |
DELETE /v1/orgs/{id}/keys/{key_id} | Revokes a team key. The owner revokes any; an admin, any the owner did not create. |
Roles are owner, admin and member. The owner is the creator and cannot be removed. Membership is by invite: an account joins only when it accepts with its own session.
Session and dashboard calls are limited per account, read live from GET /v1/limits:
| Applies to | Scope | Limit | Error code |
|---|---|---|---|
| Loading… | |||