Credits and spend limits
Fund organizations, cap monthly usage, and reconcile the append-only ledger.
Deeplinq owns a USD-pegged, append-only credit ledger. Credits and debits use integer micro-USD. The platform reserves a conservative amount before provider work and captures or releases it after actual usage is known.
Fund an organization
Platform administrators can grant credits directly:
curl --user "$ADMIN_USER:$ADMIN_PASSWORD" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-contract-2026-07" \
-X POST "$BASE_URL/v1/admin/orgs/$ORG_ID/credits" \
-d '{"amount_micro_usd":100000000}'The example grants USD 100. Reuse the same idempotency key when retrying the same business operation.
For a billing application, configure BILLING_TOKEN and call:
POST /v1/billing/topups
Authorization: Bearer <BILLING_TOKEN>
Idempotency-Key: <payment-provider>:<transaction-id>The billing service verifies and owns payment-provider processing. Deeplinq records the paid amount and reference, but does not charge cards or call a PSP.
Monthly limits
An org-admin can set organization-wide defaults:
curl -H "Authorization: Bearer $ORG_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$BASE_URL/v1/limits" -d '{
"soft_limit_micro_usd":50000000,
"hard_limit_micro_usd":100000000
}'Or override one attributed user:
curl -H "Authorization: Bearer $ORG_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$BASE_URL/v1/limits/$PRINCIPAL_ID" -d '{
"soft_limit_micro_usd":5000000,
"hard_limit_micro_usd":10000000
}'Soft limits warn without blocking. Hard limits return 402 before provider
execution. Per-user limits apply only when the request carries an end user — that is, an
end-user token. A machine token has none, so its spend is organization-level.
The {user_id} in the path is the engine principal, which a caller reads
for themselves at GET /v1/me/usage.
GET /v1/limits
DELETE /v1/limits
DELETE /v1/limits/{user_id}Reading limits is org-admin-only, with no team-scoped middle ground today.
GET /v1/limits (the org default plus every per-user override) requires the
org-admin role, same as writing it — there is no read-only or
supervisor-scoped variant. Each user reads their own effective caps instead,
at GET /v1/me/usage,
which needs no role. If your integration has a supervisor tier that manages
its own team's caps, elevating that supervisor to org-admin is not the
supported answer — it also grants every other org-admin capability. That gap
is tracked (docs/backlog.md, blocked on a manager-relationship +
limits.read/limits.write permission split the engine does not have yet),
not silently absent.
Inspect usage
Tenants read their balance and month-to-date breakdown:
GET /v1/usagePlatform administrators inspect all organization usage through the console or:
GET /v1/admin/usageUsage includes request counts, token counts, spend, and per-model breakdown.
A self-hosted model priced at 0/0 still produces real usage. Requests
and tokens are metered the same as any other model; only spend_micro_usd
comes out at zero, because pricing — not activity — is what is zero. If your
application builds a rebilling or cost-attribution view from spend alone, a
zero-priced model reads as free rather than as unused. Rebill on tokens (or
request count), not only on spend, so self-hosted usage still shows up.
Attribute usage per end user
If you are building an application on Deeplinq, your users are not our users. Make each request on someone's behalf with their end-user token, and the platform attributes spend to the principal it resolves to. A machine token carries no end user, so its spend is organization-level and attributed to no one in particular.
POST /v1/chat/completions
Authorization: Bearer <that user's access token>Three endpoints read that attribution back. They answer different questions and deliberately do not return the same numbers.
- One user's live position —
GET /v1/me/usage. What you reach for most: spend so far this month against that user's caps. Any credential, no role. - One user's own token count —
GET /v1/me/usage/tokens. Tokens, not money: what a per-user plan allowance is measured in. No role. - Everyone, over a window —
GET /v1/usage/users. An accounting report for a billing cycle, ranked by spend. Requiresorg-admin.
Where did the money go — GET /v1/usage/users
An accounting report over any window, from the ledger. Requires org-admin.
curl -H "Authorization: Bearer $ORG_ADMIN_TOKEN" \
"$BASE_URL/v1/usage/users?starting_at=2026-07-15T00:00:00Z&ending_at=2026-08-15T00:00:00Z&limit=100"{
"period_start": "2026-07-15T00:00:00Z",
"period_end": "2026-08-15T00:00:00Z",
"as_of": "2026-08-04T09:12:44Z",
"spend_micro_usd": 15790000,
"unattributed_micro_usd": 2400000,
"data": [
{
"user_id": "3f9a2c18-5e7b-4c31-9d0a-1b2c3d4e5f60",
"spend_micro_usd": 9100000,
"input_tokens": 812000,
"output_tokens": 240500,
"billed_operations": 1204
}
],
"has_more": false,
"next_cursor": null
}starting_at/ending_atare RFC 3339 and half-open —[start, end). Omit both for the current UTC month. Use them to match your own billing cycle rather than the calendar.user_id=<principal>narrows the report to one person. An unknown principal returns an emptydataarray, not a404— Deeplinq stores no user directory, only spend.unattributed_micro_usdis spend from organization-level requests — those made with a machine token, which carries no end user. It is a separate total rather than a row, sosum(data[].spend_micro_usd) + unattributed_micro_usdreconciles tospend_micro_usdon an unfiltered, complete page.- The two top-level totals always describe the window, never the page and
never the filter. Adding
user_idor paging narrowsdata; it does not changespend_micro_usdorunattributed_micro_usd, so you can always see one subject's spend against the organization's for the same period. - Rows are ordered by spend descending.
limitdefaults to 100 and caps at 1000; whenhas_moreis true, passnext_cursorback ascursor. as_ofis the instant the page was read, and the cursor carries it forward. Every page of one walk therefore describes the same closed window: a user who spends while you are paging cannot climb past the cursor and disappear from the export. Start a new walk to pick up spend that landed since.- The report carries no limits. Limits are configuration and live at
/v1/limits.
What has this user spent so far — GET /v1/me/usage
The live position against the caps, for the current month, from the same state the platform enforces against.
me is the principal the token resolves to. No role is required: the
authority to act as someone is the authority to see what acting as them cost.
The response also reports that principal id, which is the value to use when
naming the person in a grant, a team, or a spend limit.
# a request made with one of your users' own tokens
curl -H "Authorization: Bearer <that user's access token>" \
"$BASE_URL/v1/me/usage"{
"user_id": "3f9a2c18-5e7b-4c31-9d0a-1b2c3d4e5f60",
"period_start": "2026-08-01T00:00:00Z",
"period_end": "2026-09-01T00:00:00Z",
"as_of": "2026-08-04T09:12:44Z",
"spend_micro_usd": 9100000,
"reserved_micro_usd": 500000,
"projected_micro_usd": 9600000,
"soft_limit_micro_usd": 5000000,
"hard_limit_micro_usd": 10000000
}The principal resolves from the token, and the response is the same however the user obtained it:
| Caller | Typical use |
|---|---|
| A user's token from the hosted browser login | a user reading their own balance in your product |
| A user's token from the CLI device grant | developer tooling acting as one person |
A principal with no spend returns a zero position, not a 404 — Deeplinq
stores spend, not a user directory. A machine token is refused with 400:
it carries no end user, so there is no "me" to resolve.
Render projected_micro_usd next to a cap, not spend_micro_usd. The
platform reserves a conservative amount before provider work and captures it
afterwards, so a request in flight is already counted against the hard limit.
projected_micro_usd is spend_micro_usd + reserved_micro_usd — the exact
quantity compared to hard_limit_micro_usd. Showing settled spend alone will
tell a user they have room and then refuse them with a 402.
The organization balance is deliberately not in this response — the gauge
is subject-scoped, safe to forward to an end user as-is. The balance still
gates every call independently of per-user caps (a user under their cap is
refused when the organization balance is empty); read it from
GET /v1/usage, which the same credential can call.
There is deliberately no GET /v1/usage/{user_id}. The live gauge describes
the principal authenticated by the end-user token; accepting another principal
in the path would turn a self-service read into an enumeration surface.
How many tokens has this user used — GET /v1/me/usage/tokens
Tokens, for the calling subject, for the current UTC month. A plan allowance is
usually denominated in tokens rather than money, and neither endpoint above can
answer that: the gauge reads the enforcement projection, which stores money
only, and the report is org-admin because it enumerates everyone.
curl -H "Authorization: Bearer <that user's access token>" \
"$BASE_URL/v1/me/usage/tokens"{
"period_start": "2026-08-01T00:00:00Z",
"period_end": "2026-09-01T00:00:00Z",
"as_of": "2026-08-30T12:00:00Z",
"input_tokens": 812430,
"output_tokens": 91004,
"total_tokens": 903434
}- No parameters. The organization and the subject come from the token, the
window is the current UTC month, and there is no user, period, cursor or
limit to pass. A query string is
400, not ignored — a caller asking a different question should not get numbers that answer this one. total_tokensisinput_tokens + output_tokens. Cached input is already counted insideinput_tokens; the cache-category counters your provider reports are a subset view of it, so adding them again double-counts.- Settled only. Deeplinq records the usage a provider reported. A stream that fails before any usage-bearing chunk settles nothing, even though provider work happened — so treat these counts as the ledger's truth, not as a promise of parity with a provider invoice.
- One organization. A principal is organization-specific, so a person signed into two organizations has two counts. There is no combined total.
- A month with no activity is three zeros beside a real period, not a
404. A machine token is refused with400: there is no "me" to count for. - No money, cap, reservation, model breakdown, operation count or principal id
is in this body. Read spend at
GET /v1/me/usageand caps at/v1/limits.
Why the two disagree, and when that is correct
The report reads the ledger — the financial record — so it counts only what has
settled. The gauge reads the enforcement projection, which also counts what is
reserved right now. For a user with a request in flight, the gauge's
projected_micro_usd is legitimately larger than the report's
spend_micro_usd. Use the report for accounting and the gauge for anything
shown beside a limit.
One thing to get right
Bill a person by using their token, not by naming them. There is no header to assert an identity with any more: spend, limits and ownership follow the principal inside the token, which the identity provider signed. That also means roles come from the provider's project-roles claim — a backend cannot widen its own authority by sending a header.
Read your whole application's revenue
GET /v1/application/usage reports what the organizations belonging to your
application spent over a window: application totals, a page of organization
totals, and each returned organization's per-model breakdown. It is the revenue
side of /v1/application/pricing — what you earned from the models you price.
Self-scoped, on the application-credential lane. There is no application id in
the path, none accepted in the query, and none echoed in the response: the
credential names the application, so reading another one's revenue is not
something the API can express. A platform administrator, a tenant, or an
organization administrator is refused; an application that is no longer active
answers 404.
curl -H "Authorization: Bearer $APPLICATION_CREDENTIAL" \
"$BASE_URL/v1/application/usage?starting_at=2026-07-01T00:00:00Z&ending_at=2026-08-01T00:00:00Z&limit=100"{
"period_start": "2026-07-01T00:00:00Z",
"period_end": "2026-08-01T00:00:00Z",
"as_of": "2026-08-30T12:00:00Z",
"spend_micro_usd": 1550,
"input_tokens": 224,
"output_tokens": 112,
"billed_operations": 5,
"data": [
{
"org_id": "0a000000-0000-0000-0000-000000000002",
"org_name": "Beta",
"spend_micro_usd": 900,
"input_tokens": 30,
"output_tokens": 15,
"billed_operations": 1,
"by_model": [
{
"model": "gpt-example",
"spend_micro_usd": 900,
"input_tokens": 30,
"output_tokens": 15,
"billed_operations": 1
}
]
}
],
"has_more": false,
"next_cursor": null
}Four parameters, and only four: starting_at, ending_at, limit, cursor.
Anything else is a 400 naming the field, so a misspelled boundary is never
silently answered as a different question.
- The window is half-open —
[starting_at, ending_at), RFC 3339, UTC. Omit both for the current UTC month, resolved from the database clock. Ask for any historical range you like; there are no server-generated time buckets, and a month-by-month view is a request per month. - The totals describe the window, never the page. They are identical on every page of one cursor walk, so a partial export always says what fraction of your revenue it holds. Walk a complete set of pages and the organization and model figures reconcile with them exactly.
- Paging bounds organizations only, sorted by spend descending then
organization id ascending; default
limit100, maximum 1000. An organization's model list is never truncated.next_cursoris opaque — hand it back verbatim. as_ofis pinned into the cursor. Every page after the first re-reads the same closed instant, so spend arriving mid-walk cannot push a row past the cursor and out of your export.- Integer micro-USD and integer token counts. No floating-point money and no formatting: locale and currency are yours.
- Settled debits only. Credits, balances, holds, reservations and spend limits are absent — those are enforcement state, not revenue. An organization with no debit in the window has no row at all.
- A model recorded without a name reports as
"", so an organization's model rows always sum to its own row. - An organization you deleted stays visible while it has in-window spend. The delete is reversible and its ledger rows survive it; dropping the row would stop your own totals reconciling the moment a customer left.
Set a tenant's default caps from your own backend
Everything above is the tenant's own surface: an org-admin holds those routes,
and your backend does not hold their token. If you run the SaaS that owns these
organizations, you set the same organization default from your application
credential — the one that reads your revenue above — with the organization
named in the path:
curl -H "Authorization: Bearer $APPLICATION_TOKEN" \
-H "Content-Type: application/json" \
-X PUT "$BASE_URL/v1/application/orgs/$ORG_ID/limits" -d '{
"soft_limit_micro_usd":5000000,
"hard_limit_micro_usd":10000000
}'GET /v1/application/orgs/{id}/limits
PUT /v1/application/orgs/{id}/limits
DELETE /v1/application/orgs/{id}/limitsThe organization must be one of yours. Unlike /v1/application/pricing and
/v1/application/usage, this path names a resource, so holding
application-admin is not enough on its own: the engine resolves the id and
compares its owning application with your credential's. An organization that
belongs to another application, does not exist, is spelled wrong, or has been
deleted all answer the same 404 — you cannot tell which, deliberately, because
telling them apart would let anyone enumerate another application's tenants.
What these caps are. The organization default per attributed end user,
in integer micro-USD — exactly the row PUT /v1/limits writes, reached by a
different authority. They are not:
- an aggregate ceiling for the tenant (enforcement loads them only for a request carrying an end user, so a machine token's spend is unaffected);
- a credit grant, a top-up, or a balance (
POST /v1/admin/orgs/{id}/creditsis a different mechanism with different authority); - a token quota or a plan allowance from your own billing catalog.
Reading and writing.
GETreturns both fields, always.nullmeans that cap is not set, and an organization with no stored default returns200with bothnull— the normal unlimited state, not a missing resource. It never falls back to, or discloses, a per-user override.PUTstates the whole policy. An omitted ornullfield clears that cap rather than keeping the stored one, at least one field must be non-null, neither may be negative,softmay not exceedhard, and an unknown member is a400. Values are integers:0is a valid cap, and no decimal string or float is accepted or returned.DELETEremoves the default and returns204; a second one returns404. Every per-user override survives it — those rows are the tenant's, written by their ownorg-admin, and they keep winning field by field over whatever default you set.
Both mutations are audited in that organization's own chain as
application.limit.set and application.limit.clear, with your application id
taken from the credential rather than from anything you send.
Reconciliation
Run the read-only reconciliation command against production data:
make billing-reconcileIt reports drift between the append-only ledger and maintained projections without changing financial state. Treat any drift as an incident and preserve the request IDs and audit export before remediation.
Failure behavior
- insufficient organization balance returns
402; - a user hard limit returns
402; - missing pricing returns
403 model_not_priced; - a failed provider call may still settle reported partial usage exactly once;
- concurrent calls cannot spend the same scarce balance because reservations are transactional.