Skip to content
MCP Five

The LedgerProject #542 / 51

One wallet, many spenders

Every draw labelled with who asked — and refusals get rows too, or the ledger looks switched off.

A server's model call could plausibly be billed three ways. Only one of them keeps project #4's seatbelt honest.

  1. Its own budget, per server

    Tidy, and wrong. A per-server pot implies an ownership that does not exist, and it lets a server be comfortably "in budget" while your run is already broke.

  2. Untracked, outside the tree

    How it works if you don't think about it. Your MAX_TREE_TOKENS ceiling then guards everything except the one spender you don't control, which is a strange place to leave a hole.

  3. The same TreeBudget the agents draw from, tagged with who asked

    ← this one.

Why it has to be the same wallet

Project #4 built one wallet shared by reference across an agent tree, so that the sentence "the whole run stops at 600,000 tokens" was true rather than approximately true.

A server's draw goes on that same tab, or that sentence quietly becomes "600k, plus whatever the servers felt like."

ts
options.budget.add(usage);   // the SAME TreeBudget every agent draws from

One line. It is the line that keeps project #4's seatbelt from becoming a comforting decoration.

D12 · one wallet, many spenders
The orchestrator, its workers, and a server that asked for a model call all draw from one TreeBudget shared by reference. Every draw carries a label saying who asked — which is what turns 'how much did that server cost me?' into a group by.

One wallet, many spenders, every draw labelled.

The label is the new work

Sharing the wallet is one line. The label is the feature, and it is what ledger_entries.server_key is for.

sql
create table if not exists ledger_entries (
  id bigserial primary key,
  run_id uuid references runs(id) on delete cascade,
  server_key text not null,          -- THE column. everything else is detail.
  tool_name text, model text not null,
  estimated_cents real not null default 0,
  actual_cents real not null default 0,
  decision text not null check (decision in ('allowed','refused')),
  reason text, round integer not null default 1,
  input_tokens integer not null default 0, output_tokens integer not null default 0,
  cache_write_tokens integer not null default 0, cache_read_tokens integer not null default 0,
  created_at timestamptz not null default now()
);

Which turns a question project #4 simply could not answer into a group by:

npm run ledger
  SPEND BY SERVER
──────────────────────────────────────────────────────────
server        allowed  refused      spent  refused value
kitchen             8        5      13.0¢          24.2¢

The column people leave out

Look at the right-hand one. Refusals get ledger rows too, with actual_cents = 0.

Two schema decisions worth defending

consumed_at timestamptz, not a boolean. "Issued at 14:02, spent at 14:03" is a completely different story from "issued and never used", and a boolean throws that distinction away. The cost of keeping it is one column.

create table if not exists will not add your new columns. Pair every new column with its own alter table ... add column if not exists — this is project #4's gotcha 5, still true, and it fails silently: the table exists, so the statement succeeds, and the column simply isn't there.

sql
alter table runs add column if not exists mode text not null default 'single';

What this buys, in one number

13model calls servers asked foracross project #5's runs
13.0¢allowed, and actually spent8 requests
24.2¢refused — estimated value5 requests · $0.00 actually spent

Nearly twice as much was refused as was spent. Without the refused column that is an invisible fact, and the ceiling looks like an ornament.