# Fetching a costed row

Read a finished ledger row back from your admin plane, and handle the four ways the call can decline.

Source: https://tokentriage.com/docs/sdks/ledger

---
import Callout from '../../../components/Callout.astro';
import NextSteps from '../../../components/NextSteps.astro';
import Details from '../../../components/Details.astro';
import CodeTabs from '../../../components/CodeTabs.astro';

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.

<CodeTabs>
  <Fragment slot="python">
  ```python
  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"])
  ```
  </Fragment>
  <Fragment slot="typescript">
  ```ts
  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);
  ```
  </Fragment>
</CodeTabs>

```text
$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.

```python
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](/docs/sdks/cost).

## 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:

1. **Probe before auth.** The first request carries no `Authorization` header. 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.
2. **Refuse a plane collision.** If `admin_url` and `base_url` resolve to the same origin, the
   call fails before any socket opens.
3. **Follow no redirects.** Any 3xx is an error, and the message does not echo the `Location`.
4. **Env-only sourcing**, as above.

<Callout type="key">
Rule 1 is what stops a misdirected admin call from carrying your token to a provider. The data
plane is a catch-all that forwards unknown paths upstream, so an admin request sent there would
be relayed, credential included.
</Callout>

<Details summary="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.

</Details>

## 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:

```text
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

```text
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.

<Callout type="warning">
Ledger reads are the one place the SDK will raise into your code. `fetch` and `fetch_trace` are
shielded like the rest of the surface, but the errors above are typed results you are expected to
handle, not internal faults. Wrap the call if a missing cost figure should not fail your request.
</Callout>

## 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`:

```text
  [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](/docs/sdks/install#verify-before-you-spend) has the full output.

<NextSteps items={[
  { title: 'Cost in the SDK', href: '/docs/sdks/cost', description: 'The three states the cost on that row can be in.' },
  { title: 'Outcomes and feedback', href: '/docs/sdks/outcomes', description: 'The write paths that use this same credential protocol.' },
  { title: 'The management client', href: '/docs/sdks/management', description: 'The generated client for everything else on the admin plane.' },
  { title: 'Decision ledger fields', href: '/docs/reference/ledger-fields', description: 'Every field on the row, and what the server means by it.' }
]} />
