Fetching a costed row
Read a finished ledger row back from your admin plane, and handle the four ways the call can decline.
On this page
The receipt tells you what happened. The ledger row tells you what it cost. This page covers the call that gets it, and the four ways it can decline.
ledger.fetch is the only part of the SDK that opens a socket you did not already have. It is
also the only part that touches a credential, which is why it is built the way it is.
Fetch one row
Pass a receipt or a bare request id. wait_s bounds how long the server may take to finish
writing the row, which matters when you ask about a request that completed moments ago.
from tokentriage import ledger
row = ledger.fetch(rcpt, wait_s=3) # or ledger.fetch("tt_vjuoh…")
print(row.cost.render())
print(row.trace["agent"], row.usage["total_tokens"])const row = await tt.ledger.fetch(rcpt, { waitS: 3 }); // or fetch("tt_vjuoh…")
console.log(tt.renderCost(row.cost));
console.log(row.trace.agent, row.usage.total_tokens);$0.000027 (snapshot litellm@2026-07-14#179affb)
planner 120
Python’s ledger calls are synchronous; TypeScript’s return promises. That is an idiom choice per language rather than a capability difference, and it is the one shape difference you will feel most often.
The call is explicit. Nothing hydrates rows in the background, nothing polls, and wait_s is a
bounded wait inside one logical fetch rather than a retry loop.
What a row carries
RowView projects the server’s record verbatim. No field is renamed, and nothing is derived.
| Block | Holds |
|---|---|
cost |
the three-state cost |
would_cost |
per-tier counterfactuals, same three states |
trace |
trace id, step id, parent id, agent, step class |
dims |
user, tenant, feature, and your tags |
usage |
token counts and their source |
decision |
mode, tier, upstream, rewrite, cache outcome |
latency, http, request, outcome |
timing and transport detail |
record |
the whole underlying record, for fields with no named accessor |
Note that agent lives on trace, not on dims. Dimensions are the business labels; the agent
is part of identity.
row.trace {'trace_id': '925467c2…', 'step_id': 'c055085e…', 'parent_id': '',
'agent': 'planner', 'step_class': 'user_turn', …}
row.dims {'user': 'u-42', 'feature': 'chat'}
row.usage {'input_tokens': 100, 'output_tokens': 20, …, 'source': 'provider'}
ledger.fetch_trace takes a trace id and returns the whole tree, with per-node and rollup
figures. Those totals are server fields projected as they arrived; the SDK does not sum money
across nodes, for the reason on cost in the SDK.
The credential protocol
The admin token comes from TOKENTRIAGE_ADMIN_TOKEN and from nowhere else. There is no keyword
argument, on any function, in either language.
That is a containment decision. A credential with one source has one place to leak from, and the
SDK keeps it out of logs, exception messages and reprs. The module that holds it is imported
lazily, so import tokentriage alone opens nothing.
Four rules govern every admin call, including the write paths:
- Probe before auth. The first request carries no
Authorizationheader. The token is attached only after a 401 or 403 whose body parses as the admin plane’s own typed error shape, so the far end has identified itself before the credential moves. - Refuse a plane collision. If
admin_urlandbase_urlresolve to the same origin, the call fails before any socket opens. - Follow no redirects. Any 3xx is an error, and the message does not echo the
Location. - Env-only sourcing, as above.
What probe-before-auth does not prove
Probing proves the peer on the first connection was admin-plane-shaped. It cannot prove the same of the second connection that carries the token, because two requests mean two name resolutions. A DNS rebind between them, or a front that answers one connection from the admin plane and the next from something else, defeats the inference.
That is inherent to any two-request protocol rather than an implementation gap, and it is stated here instead of being closed, because closing it would mean pinning a resolved address across both legs — reaching under the transport in the one module that has to stay small enough to audit.
What the protocol does guarantee holds regardless: the token is not sent on first contact, not sent to the data-plane origin, not followed to a redirect target, and not echoed into any message. The token path is two connections, with no retry ladder. If you need a stronger guarantee, terminate the admin plane on loopback or behind mTLS, where connection identity is not inferred from DNS at all.
When the call declines
Four typed failures, each naming what to do.
When the two planes collide
admin_url resolving to the same origin as base_url fails immediately, before any request:
PlaneCollision: admin_url resolves to the same origin as base_url (http://127.0.0.1:3915) -
refusing to send an admin request to the DATA plane, which would forward it (and any auth)
upstream (SDK-R-050). base_url=http://127.0.0.1:3915 admin_url=http://127.0.0.1:3915
Both settings are named in the message so you can see which one is wrong. Fix whichever is not pointing at its own plane.
When the admin plane wants a token
AdminAuthRequired: the admin plane requires authentication (dashboard.auth) - set
TOKENTRIAGE_ADMIN_TOKEN (there is deliberately no token kwarg)
Set the variable in your environment. Emission is unaffected by this — only ledger reads need the token — so instrumentation keeps working while you sort it out.
When the row is not there yet
A request that has not finished being written raises NotFound. Give wait_s a second or two;
that is what it is for. A request id that never existed raises the same thing, since the ledger
cannot tell the difference between “not yet” and “never”.
When something else goes wrong
Anything else is a LedgerError carrying the server’s message. URL-embedded credentials are
scrubbed from these messages before they are raised, so a token in an admin_url does not
reappear in your logs.
Checking the wiring first
check() runs the same containment transport against GET /tt/api/status and reports what it
found, without probing the data plane. Run it once when you wire the ledger up rather than
debugging a failed fetch:
[ok ] TT-SDK-016 admin plane reachable at http://127.0.0.1:7498 - mode=route, tiers=1,
upstreams=1, pricing_snapshot=present
Under a plane collision the probe is skipped rather than sent, so the token stays in the process. Install and configure has the full output.