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 reads USDF_SESSION_TOKEN, and the command line stores one with usdf login --token -.
  • GET /v1/auth names 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
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.

Request

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

WayHowCredited
Send USDFFrom the linked wallet to deposit.to in GET /v1/accountAfter confirmations, usually within a minute or two
Add USDG without gasGET /v1/account/deposit/usdg returns typed data to sign; POST it back with the signature200 credited, or 202 while the gateway finishes it
Automatic top-upApprove USDF to Permit2 once, then sign one Permit2 allowance. The gateway pulls when the balance runs low. /v1/topup, /permit, /pulls, /clearAs each pull confirms
The Buy page/buyAs the transaction confirms
Any tokenPay with any tokenAs 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
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
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:

EndpointWhat it does
POST /v1/account/keysCreates a key: label, budget_units (a lifetime spend limit), rate_limit_rpm.
GET /v1/account/keysActive 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.

ParameterTypeNotes
spend_limit_hour_units, spend_limit_day_units, spend_limit_month_unitsString of units, or nullTrailing hour, day and 30 days. Null clears the cap.
allowed_modelsArray, or nullModel ids, or a prefix ending in *. Null allows every listed model; [] allows none.
pausedBooleanA manual pause is never lifted by the gateway. Requests get 403 key_paused.
no_logsBooleanPrompt and completion text are never written anywhere.
private_tierBooleanEvery request runs on attested routes. Implies no_logs.
webhook_urlString, or nullHttps 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
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
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
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:

ReasonResumes when
zero_balanceA deposit lands.
hour_limit, day_limit, month_limitThe trailing window moves on.
manualThe 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 typeSet withCarriesDeduplicate on
key.paused, key.resumedwebhook_url on PATCH /v1/keys/{id}The key, the reason, the trailing spend and the capsevent_id
approval.decidedThe agent's webhook_urlThe agent, the action and the owner's decision (status)approval_id
batch.done, batch.failed, batch.expiredThe job's webhook_urlThe job, its cost, and its result once doneid, the job id
balance.lowlow_balance_units and low_balance_webhook_url in the account settingsThe available balance and the thresholdevent_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:

EventRead
key.paused, key.resumedGET /v1/keys/{id}/limits
approval.decidedGET /v1/agents/{slug}/approvals
batch.done, batch.failed, batch.expiredGET /v1/batches/{id}
balance.lowGET /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.

FieldEffect
zdrZero 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_denyProvider ids to allow or exclude.
allow_fallbacksfalse: the first route only, with no failover.
max_price{ input, output } in USD per 1M tokens.
no_logs_default, private_tier_defaultFlags for keys created after they are set.
low_balance_units, low_balance_webhook_urlThe low-balance alert.

Change the defaults

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

Next: routing defaults in detail

Everything the dashboard shows is an endpoint. All take the session. On the dashboard: Activity and Analytics.

EndpointReturns
GET /v1/account/overviewBalance, held, available, recent spend, top models, the last deposit and withdrawal.
GET /v1/account/activity, .csvEvery request, newest first, with filters: key, model, status, from, to.
GET /v1/account/usage, /usage/unsettledUsage rows, and what is not yet in a settlement batch.
GET /v1/account/depositsDeposits, newest first.
GET /v1/account/withdrawalsWithdrawals, newest first.
GET /v1/statements/{YYYY-MM}.csvA monthly statement, also as .pdf or .json.
GET /v1/analytics, .csvSpend 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.

EndpointWhat it does
GET /v1/orgsThe teams you belong to.
POST /v1/orgsCreates a team with a name. You become its owner.
GET /v1/orgs/{id}Members, invites, keys, usage and funding.
POST /v1/orgs/{id}/membersInvites by email or account_id, as admin or member, or changes a member's role. Owner or admin.
GET /v1/orgs/invitesThe open invites addressed to you.
POST /v1/orgs/invites/{invite}/accept, /declineJoins 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}/fundMoves units of your available balance into the team's. Owner or admin.
POST /v1/orgs/{id}/defundMoves units of the team's available balance back to the owner. Owner.
POST /v1/orgs/{id}/keysCreates 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 toScopeLimitError code
Loading…

Next: limits on requests and payments