@monotykamary/pi-ledger
Billing engine for the serverless agency — a pi extension that meters agentic dev work like cloud compute and invoices it like a timesheet.
Package details
Install @monotykamary/pi-ledger from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@monotykamary/pi-ledger- Package
@monotykamary/pi-ledger- Version
0.5.0- Published
- Aug 3, 2026
- Downloads
- 670/mo · 57/wk
- Author
- monotykamary
- License
- MIT
- Types
- extension
- Size
- 471.3 KB
- Dependencies
- 0 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./extensions"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
🧾 pi-ledger
Billing engine for the serverless agency — a pi extension that meters agentic dev work like cloud compute and invoices it like a timesheet.
Per-invocation · duration-based · scale-to-zero idle. A pomodoro human-time wizard, and an invoice-grade receipt.
pi-ledger is the billing engine for the serverless agency — a dev shop that runs on on-demand agents and bills the way serverless compute is billed: per-invocation, duration-based, scale-to-zero idle.
📝 Read the story behind it: Bill the invocation, not the hour — the blog post that introduces the "serverless agency" and the "bill forward progress, not process" thesis pi-ledger is built on.
| Serverless compute | pi-ledger |
|---|---|
| On-demand function | The agent — each turn is an invocation |
| Execution duration | Per-turn agent time (generation normalized to ref TPS + tool time) |
| Scale-to-zero idle | Human idle costs nothing by default |
| Provisioned capacity | Opt-in pomodoro extensions (billed human oversight) |
| Usage report / invoice | /ledger-receipt — an invoice-grade HTML receipt |
The agent is the on-demand function; each turn is an invocation billed by duration, with stalls excluded. Human oversight — review, steering, the next prompt — is metered separately as rolling credit you provision via opt-in pomodoro extensions:
- First prompt & steering — metered from your first keystroke, billed when the message is delivered to the agent (not at submit). Reverting and re-steering bills once; dequeueing and never re-sending bills nothing.
- Idle — costs nothing by default. A window opens only on engagement (first keystroke or extension) after
agent_end, and bills only when your next submit produces agent work (agent_start). Walk away → no bill. - Wizard — prompts at
agent_settledand on/resumewhen no rolling credit remains, and at each exhaustion boundary — but only at true idleness (no genuine keystroke in the last 90s). While you're typing the prompt defers instead of interrupting, and a boundary hit mid-flow rolls a pomodoro block silently; silence never creates the first credit, only rolls a grant you already made. A styled TUI component in the terminal, or aselectdialog in a GUI (the vscode-pi extension runs pi in RPC mode, where the custom component can't render). - Auto-extend — for headless/GUI sessions, provisions a block silently instead of prompting; bills only idle a later submit commits, capped at the block, so walking away never over-bills.
/ledger-receipt then emits the invoice — the cloud-provider usage report for your own work.
Standalone, but pi-tps-aware. pi-ledger works on its own — it measures agent time itself when
@monotykamary/pi-tpsisn't installed. When pi-tps is present, it emits thetps:telemetryevent after every turn and pi-ledger consumes its refined generation/stall numbers (and adds tool-execution time of its own). pi-tps writestpsmarkers to the session JSONL; pi-ledger keeps its own event log in a per-session sidecar. Installing pi-tps is purely an upgrade in fidelity.
Quick start
pi install github:inloopstudio-team/pi-ledger
# optional — better stall detection & per-turn fidelity:
pi install npm:@monotykamary/pi-tps
Then in pi: /ledger-settings to set your rates, work a session, and /ledger-receipt for the receipt.
Demo shortcut: receipt from an existing pi-tps session
Didn't run a full pi-ledger session? /ledger-receipt also works on a session that only has pi-tps markers (e.g. resume an older pi-tps session, set rates with /ledger-settings, then /ledger-receipt). With no live ledger data it converts the tps entries into the receipt — lower fidelity (no tool time; no human time — markers carry no credit/commit info, so only agent time is billed) but enough to demo the output.
Commands
| Command | What it does |
|---|---|
/ledger |
Show running totals: agent/human hours, costs, total. |
/ledger-settings |
Configure billing (rates, pomodoro, project, author, currency, auto-wizard, auto-extend). TUI: a searchable settings list; GUI: a select→input flow. |
/ledger-extend [m] |
Open the human-time wizard to extend the window by m minutes (default: pomodoro length); confirm or stop in the dialog (TUI component or GUI select). Engages a window if none is open. |
/ledger-receipt |
Export a self-contained HTML receipt for the session and open it. |
How time is measured
| Phase | Bracket | Billed as |
|---|---|---|
| Agent | agent_start → agent_end (sum of turns) |
Agent time |
| Human | idle after agent_end (engaged → committed at agent_start) |
Human time |
| Human | first-prompt composition (first keystroke → first agent_start) |
Human time |
| Human | steer/followUp composed during a run (typing bursts, billed at delivery) | Human time |
A provider-error turn (
stopReasonerror) opens no human window — its retry/queue backoff isn't human idle and isn't billed (see below).
Agent time is the billable agent work per turn — generation normalized to a reference TPS plus real tool-execution time — summed across turns and priced at the agent rate:
agent_ms_per_turn = (output_tokens / reference_tps × 1000) + tool_ms
agent_hours = Σ agent_ms_per_turn / 3_600_000
agent_cost = agent_hours × agent_rate_per_hour
(× 1000 carries token-seconds to ms; 3_600_000 ms = 1 hour.)
- Generation is billed by output tokens at a reference TPS (frontier-model average, default 75), so model speed can't change the bill — a fast model and a slow one producing the same output tokens bill the same, and the faster model no longer punishes the contractor.
referenceTpsis configurable in/ledger-settings. - Tool execution is the agent doing the work (running bash, reading files, …) — billable, measured as the union wall-clock of tool calls within the turn (parallel tools don't double-count). It isn't token-bound, so it's billed as real time.
- Stalls (mid-stream inference pauses) drop out automatically — a stall produces no tokens, so token-normalized billing never counts it (the abuse vector a slow/queued provider could inflate). The real wall-clock
generationMs/stallMsare still recorded on the event for audit. - Source is either
tps(high-fidelity, from pi-tps's event) orfallback(self-measured). Exactly one segment is written per turn regardless of extension load order — afallbackmay be corrected by a latertpsentry for the same turn, and rehydration keeps the last per turn (no double-count).
Human time is the idle window between when the agent hands control back (agent_end) and when the user takes it again (agent_start), capped by the rolling extension credit you've provisioned. The engagement prompt (and the window it opens) is armed at agent_settled — when the run is fully settled and no auto-retry, compaction, or queued follow-up will continue — and gated on idleness, so it lands between thoughts, never inside one.
A turn that auto-continues (a provider error a retry extension sleeps with backoff then re-prompts, or an overflow pi-core compacts and retries, or a queued follow-up) never reaches agent_settled, so no window opens and the backoff/compaction wait is never billed as human time (scale-to-zero: a slow/queued provider is a retry, not billable). The window reopens at the next agent_settled.
The first prompt is special — nothing precedes it — so an initial window opens on your first keystroke (not at session_start) and closes at the first agent_start, metering the time you spend composing (or reviewing a resumed session before your next prompt) under the same cap:
billed_human = min(engaged_idle, granted_budget) # only when committed at agent_start
granted_budget = remaining_extension_credit
Steering while the agent runs is also human time, and — like idle — it's commit-gated on an agent outcome. A thin input-editor wrapper stages every keystroke during a run; when you submit a steer or queued followUp (the input event's streamingBehavior), the composition becomes pending (queued to the agent) and is billed at delivery — the message_start user message that means the queued composition reached the agent — as the sum of its typing bursts (consecutive keystrokes within a gap threshold), not the wall-clock from the first keystroke. A single key, or keys spread minutes apart, bills nothing; only sustained typing that's actually delivered to the agent bills, under the same credit cap as any window.
Billing at delivery closes the revert/re-steer abuse: reverting a queued message back to the editor (alt+up, "restore queued messages") carries its composition forward to the next submit, so reverting then re-steering bills the original typing once at the re-steer's delivery — never twice, never free. A composition you dequeue and never re-send never reaches the agent, so it bills nothing (no agent outcome) and is abandoned at session_shutdown. Typing never submitted never reached the agent either, so it's discarded — it bills nothing and can't inflate the post-turn idle window.
Idle time is engagement-gated and commit-gated. Between turns an idle window opens only when you engage — the first keystroke you type, or the first extension (the wizard's extend / /ledger-extend, which both grant capacity and count as engagement). It bills wall-clock from that onset (capturing thinking, not just keystrokes), capped at your rolling credit — but only when your next submit produces agent work (agent_start commits it). Pure idle (no keystroke, no extension) opens no window and bills nothing; idle you walk away from (no submit) is abandoned at session_shutdown and bills 0. Idle with no output is wasted time.
- Engaged idle bills against your rolling credit only. No engagement → no window → no bill; no credit → bills 0 even when committed (the wizard prompts you to extend first).
- The initial window opens on your first keystroke (not at
session_start) and commits at the firstagent_start, metering first-prompt composition under the same credit cap. Review time before the first keystroke has no signal and bills nothing — so on/resumethe wizard pops to let you extend (engaging) and bill that review. - Extensions are rolling credit:
remaining_extension_creditis the provisioned pomodoro balance carried across agent turns. All billed idle and steering time consumes it; the remainder rolls forward to the next idle window (like provisioned capacity). - At
agent_settled, a wizard pops inline (the same pi-core settings style as/ledger-settings, so the status bar stays visible) only when no rolling credit remains — to prompt engagement (an extension both engages and grants capacity) — and only once you're idle (no genuine keystroke in the 90s presence window): while you're typing, the prompt defers and re-checks, further typing slides it, and the nextagent_startdisarms it. In the TUI it renders a custom component; in a GUI (RPC) it falls back to aselectdialog; with auto-extend on, it skips the prompt and provisions a block silently.agent_settledfires once the run is fully settled (no auto-retry, compaction, or queued follow-up left), so the wizard never pops mid-retry or mid-continuation — and never mid-typing. With credit, it stays silent and arms to fire when the engaged window's credit is exhausted; an exhaustion hit while typing rolls a pomodoro block silently (with a non-blocking notice), and only true idleness pops the extend/stop prompt (theextend + extend + extendchain). - When @monotykamary/pi-retry is installed, its backoff sleep can make
agent_settledfire mid-retry; pi-ledger captures pi-retry'sstarted/completed/cancelledevents and defers the prompt until the retry genuinely settles — it pops oncompleted, never oncancelled, never while one is in flight. /ledger-extend [m]opens the wizard manually — with or without an open window (no window → extend engages one) — offering to extend bymminutes; confirm in the dialog, or stop.- The status bar and receipt total the entire session up to now — they include the in-progress engaged window's idle (capped at its remaining credit) and, for a pi-tps-only session, no human time (markers carry no credit/commit info). Unlike pi-tps (per-turn), this is the full session so far.
Because billing is min(engaged_idle, budget) and only commits on an agent action, the time you spend deciding in the wizard is unbilled if you decline — and unused extension credit isn't forfeited when you re-engage the agent after a short idle; it rolls into the next window. But idle you never commit (walk away, dismiss, quit) bills nothing.
Design principles
pi-ledger bills forward progress, not process — four choices shape the whole engine:
- Bill the outcome, not the time spent. Idle bills only when a submit produces agent work; steering bills only typing actually delivered to the agent (billed at delivery, not submit). Thinking that led nowhere (you dismissed, walked away, or the agent did nothing) costs nothing. We charge for collaboration that moved the session, not for minutes the human spent.
- Detect engagement instead of demanding a button. A window opens on your first keystroke or extension — engagement is observed, not self-reported — yet a single stray key bills nothing; only a real typing burst counts. You don't start a timer, but you do have to actually be there. The wizard follows the same rule: it asks only once your hands have left the keyboard, and observed typing at an exhaustion boundary rolls an already-granted budget forward — silence never creates the first credit, and never interrupts a flow either.
- You set the budget; the engine spends it. Billable idle is capped at the pomodoro credit you provision, and it drains as you use it — no billable idle accrues beyond what you authorized, and the leftover rolls forward so a short productive idle isn't forfeited.
- Noise and stalls drop out. Generation is token-normalized (model speed can't move the bill), stalls emit no tokens so they're never billed, and unsubmitted typing never reached the agent so it's discarded. The receipt tracks value delivered, not wall-clock spent.
The shape is a deliberate hybrid: Toggl's you-decide-the-budget control with the convenience of automatic tracking, gated on a real outcome — so it never bills the process of working, only the work that landed.
Settings
/ledger-settings opens a pi-core-style bordered, searchable list. Rate and text fields open an inline input on Enter; currency and the auto-wizard toggle cycle through presets. Settings persist to the per-session sidecar (see Data model) and rehydrate on resume and /tree navigation.
| Setting | Default | Notes |
|---|---|---|
| Agent rate | 60 |
$/hour billed for agent work |
| Human rate | 60 |
$/hour billed for human work |
| Pomodoro minutes | 20 |
Minutes added per extension |
| Reference TPS | 75 |
Output tokens/sec to normalize generation to (frontier avg) |
| Project | (cwd) | Shown on the receipt; falls back to the cwd name |
| Author | (user) | Shown on the receipt; falls back to your OS user |
| Currency | USD |
Symbol for amounts |
| Auto-wizard | on |
Idle-gated popup at agent_settled (no credit) and on /resume; mid-typing exhaustion rolls credit silently |
Receipt / invoice
/ledger-receipt writes a self-contained HTML file to ~/.cache/pi-ledger/receipt-<session>-<timestamp>.html and opens it.
- White background, white receipt card with a hairline border and a whisper shadow.
- Geist Mono throughout.
- Values stream in autoregressively — each field types out character-by-character like an LLM token stream, with a blinking cursor tracking the active field.
- A grouped invoice, not a flat receipt. Two groups — Agent and Human — each at its hourly rate, with itemized sub-lines that roll up to the group subtotal and corroborate the pricing (every sub-line is its hours at the group rate, summing to the group total):
- Agent → Compute (generation, token-normalized) + Tool execution (wall-clock) + Stalls ($0, not billed), then a Subtotal.
- Human → Review / think (committed idle) + Steering + Queuing (followUp) + Idle abandoned ($0, not billed), then a Subtotal.
- A Total sums the two subtotals, followed by a footer with the provisioned capacity (extensions granted · used · remaining) and the session span vs. billed hours.
- The
$0lines are the audit story made visible: time the extension captured but the commit pattern excluded (walked away → no submit → no bill; stalls).
The HTML is fully self-contained (inline CSS + JS, Geist Mono via Google Fonts) and prints cleanly to PDF (⌘P) — the cursor hides for print.
Data model
pi-ledger keeps a per-session sidecar event log at ~/.cache/pi-ledger/sessions/<sessionId>.jsonl — the source of truth, outside the session JSONL so it survives compaction (which discards old custom entries) and accumulates across all branches of the session. Events:
settings— a settings snapshot (last one wins on replay).agent— one per turn:{ id, turnIndex, agentMs, generationMs, stallMs, toolMs, tokens, model, source, supersedes?, timestamp }.agentMsis the billable time (generation normalized to the reference TPS + tool time);generationMs/stallMsare the real wall-clock (audit). A'tps'turn maysupersedean earlier'fallback'for the same turn (load-order race) so it isn't double-counted.human-open— on engagement (the first keystroke you type after a turn, or the first extension — both open the window), and re-recorded on each wizard extend:{ openedAt, engagedVia, grantedBudgetMs, extensions, extensionBudgetMs, timestamp }.openedAtis the engagement onset;engagedViais"keystroke"or"extension"(audit);grantedBudgetMsis the window's cap =extensionBudgetMs(the rolling credit carried into the window). Nohuman-openis written atsession_start,agent_end, oragent_settled— the window is engagement-gated.human-close— on the nextagent_start(committed = your submit produced agent work) or onsession_shutdown(abandoned = you left without submitting):{ openedAt, closedAt, billedMs, idleMs, keystrokes, committed, grantedBudgetMs, extensions, extensionBudgetMs, timestamp }. Committed billsmin([onset, agent_start], credit); abandoned bills 0 (idle with no output is wasted).keystrokesis the composition-density count while the window was open (after held-key collapse; idle bills wall-clock, so it's analytics, not a billing input).committeddefaults totrueon legacy events. ItsextensionBudgetMsis the rolling credit remaining after this window's consumption (carried forward). Legacy events lackingextensionBudgetMsare backfilled on replay.steer— a steer/followUp composed while the agent ran, billed at delivery (themessage_startuser message = the agent outcome), not at submit:{ startedAt, submittedAt, durationMs, billedMs, keystrokes, behavior, grantedBudgetMs, extensionBudgetMs, timestamp }.submittedAtis the (re-)submit time;timestampis the delivery/commit time;billedMsisminof the typing-burst sum andcredit(not the wall-clock span —durationMsis the span, kept for audit);keystrokesis the staged count;behavioris"steer"(mid-stream interrupt) or"followUp"(queued). Billed as human time, consuming rolling credit (same rule as an idle window). A pending composition (submitted but not yet delivered) is in-memory only — never persisted; one dequeued and not re-sent, or interrupted by reload/shutdown, is abandoned (bills 0; no agent outcome).
On session_start (fresh load/reload), pi-ledger replays the sidecar to rebuild totals, settings, and the rolling extension credit. An unclosed window from a prior session was never committed by an agent action, so it's abandoned (a human-close with committed: false, billed 0) rather than restored — it isn't carried forward. No window is opened at session_start; the next one opens on engagement. /tree branching stays in the same session, so the live in-memory totals are kept as-is (not re-read — never reset to $0). Totals are global across branches; idle you commit is retained across exit/re-enter, idle you abandon is not.
Architecture
session_start → rehydrate from sidecar (rebuild totals, settings,
rolling credit; ABANDON any unclosed prior window —
uncommitted idle bills 0, not restored). Open NO
window (engagement-gated). On /resume (or /reload) pop
the wizard to prompt engagement (bill review via
extend); startup/new stay silent. Wrap the input editor
(TUI) to stage keystrokes — during a run for a
steer/followUp burst, and between turns the FIRST
keystroke engages an idle window at its onset.
turn_start → reset per-turn tool + fallback accumulators
tool_execution_start → tool depth counter (union timing)
tool_execution_end → (parallel tools don't double-count)
message_start/update/end → fallback generation + stall gate (self-sufficient)
tps:telemetry (pi-tps)→ record 'tps' agent segment = (output tokens / ref TPS) + tool
└ corrects a 'fallback' already written this turn
turn_end (no pi-tps) → record 'fallback' agent segment from own measurement
agent_end → discard any uncommitted in-run typing (a steer never
submitted never reached the agent → bills 0). Open NO
window here (engagement-gated); the engagement prompt
is armed at agent_settled (below), not here — agent_end
fires per run, and Pi may still auto-retry, auto-compact
and retry, or continue with a queued follow-up.
agent_settled → the run is fully settled (no retry/compaction/follow-up
left). Open NO window here either (engagement-gated):
└ no credit left → engagement prompt, IDLE-GATED (pops
once hands-off for the 90s presence window; typing
slides it, agent_start disarms it; deferral grants
nothing — first credit is always an explicit extend)
└ extend → engage + grant a pomodoro, arm at boundary
└ dismiss/ignore → no engagement, no window, no bill
└ rolling credit > 0 → stay silent (arm at engagement)
└ also re-offers the prompt after a retry storm exhausts
(the run has settled and the human must take over)
engage (idle) → first keystroke OR first extension opens the idle
window at onset (rolling credit); an extension
also grants capacity. Arms the exhaustion boundary:
hit mid-typing → roll a block silently; hit while
idle → pop the wizard (extend / stop billing).
agent_start → COMMIT the engaged window: billed =
min([onset, agent_start], credit), committed;
consume credit = billed (rolls the rest).
No engagement → no window → bills nothing.
input (steer/followUp)→ stage the composition as PENDING (queued to the agent):
snapshot the typing bursts (a prior dequeue's
dequeuedBuffer prepended), clear staging — NOT billed
yet. Interactive sources only; pass-through (never
transform the input). A no-typing submit with no prior
dequeue stages nothing. Held keys (auto-repeat)
collapse, so they can't fake a burst; idle keystrokes
are counted (composition density).
dequeue (alt+up) → a queued composition reverts to the editor (the
app.message.dequeue action, observed via the editor
wrapper): merge all pending compositions into
dequeuedBuffer, carried forward to the next submit
(the re-steer/re-queue). Not abandoned — re-steering
bills it once at delivery.
message_start (user) → a queued steer/followUp DELIVERED to the agent (the
agent outcome): commit the front pending composition as
a `steer` event — billed = the typing-burst sum (not
wall-clock), capped at credit; consume credit. The
initial/normal prompt stages no pending (no
streamingBehavior), so it's a no-op for them.
session_shutdown → ABANDON any open window (committed: false, billed 0 —
idle with no submit is wasted; nothing retained) AND
any pending/dequeued steer composition (never delivered
→ no agent outcome → bills 0).
Agent timing prefers pi-tps's tps:telemetry (generationMs, stallMs, tokens.output); when pi-tps is absent, pi-ledger measures generation + a basic stall gap gate itself at turn_end. Either way generation is billed by output tokens at the reference TPS (speed-invariant); the real generation/stall ms are recorded for audit. Tool-execution time is always measured locally, billed as real time, and paired with the turn. The wizard is driven entirely by the extension (the agent is unaware): it auto-pops at agent_settled when no rolling pomodoro credit remains (and on /resume, to prompt engagement for review) once the human is idle — recent typing defers it — and is disarmed on the next agent_start or session_shutdown. An exhaustion boundary hit mid-typing rolls a pomodoro block silently; idle, it pops. No window opens at session_start, agent_end, or agent_settled — an idle window opens only on engagement (first keystroke or extension) and bills only when committed by a submitted prompt at agent_start; abandoned idle (shutdown without a submit) bills 0. All billed idle and steering time consumes the rolling extension credit; the remainder carries forward to the next idle window. State is stateless: everything is rebuilt from the per-session sidecar on session_start (fresh load/reload); /tree keeps the live in-memory totals (branching stays in the same session, so the status never resets to $0). A 'tps' agent event supersedes the 'fallback' it replaces, so the same turn isn't double-counted. The status and receipt compute the whole session up to the current moment, including the in-progress open human window, from the sidecar — so they survive compaction and branching.
Testing
pnpm install
pnpm test # vitest run (208 tests)
pnpm run typecheck # tsc --noEmit
pnpm run lint:dead # knip
License
MIT