Administration

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/usage

Platform administrators inspect all organization usage through the console or:

GET /v1/admin/usage

Usage 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 positionGET /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 countGET /v1/me/usage/tokens. Tokens, not money: what a per-user plan allowance is measured in. No role.
  • Everyone, over a windowGET /v1/usage/users. An accounting report for a billing cycle, ranked by spend. Requires org-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_at are 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 empty data array, not a 404 — Deeplinq stores no user directory, only spend.
  • unattributed_micro_usd is 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, so sum(data[].spend_micro_usd) + unattributed_micro_usd reconciles to spend_micro_usd on an unfiltered, complete page.
  • The two top-level totals always describe the window, never the page and never the filter. Adding user_id or paging narrows data; it does not change spend_micro_usd or unattributed_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. limit defaults to 100 and caps at 1000; when has_more is true, pass next_cursor back as cursor.
  • as_of is 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:

CallerTypical use
A user's token from the hosted browser logina user reading their own balance in your product
A user's token from the CLI device grantdeveloper 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_tokens is input_tokens + output_tokens. Cached input is already counted inside input_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 with 400: 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/usage and 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 limit 100, maximum 1000. An organization's model list is never truncated. next_cursor is opaque — hand it back verbatim.
  • as_of is 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}/limits

The 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}/credits is a different mechanism with different authority);
  • a token quota or a plan allowance from your own billing catalog.

Reading and writing.

  • GET returns both fields, always. null means that cap is not set, and an organization with no stored default returns 200 with both null — the normal unlimited state, not a missing resource. It never falls back to, or discloses, a per-user override.
  • PUT states the whole policy. An omitted or null field clears that cap rather than keeping the stored one, at least one field must be non-null, neither may be negative, soft may not exceed hard, and an unknown member is a 400. Values are integers: 0 is a valid cap, and no decimal string or float is accepted or returned.
  • DELETE removes the default and returns 204; a second one returns 404. Every per-user override survives it — those rows are the tenant's, written by their own org-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-reconcile

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

On this page