The Spend Ledger
The Spend Ledger: LLM plans spend real money, and money has rules older than software - integer cents (floats round YOUR money, never theirs), a ledger where every entry has a description, and a budget that stops the spending BEFORE the overdraft, not in the postmortem. The journal already receipts every task; this makes the receipts denominate.
Observability & Ops
Round 16
Noel Rappin
exit 0
bundle exec ruby examples/spend_ledger.rb
a real captured run
THE SPEND LEDGER (budget $45.00, prices in integer cents)
plan status: partial_failure
stopped at: budget: polish:tone costs $9.50 but only $5.10 remains
the invoice (from the ledger, balances to the cent):
item amount running
fetch:tickets $0.00 $0.00
classify:batch $2.40 $2.40
draft:responses $18.75 $21.15
review:drafts $18.75 $39.90
TOTAL $39.90 (budget $45.00)
journal receipts: 4 spends, 1 declined - the money
trail and the work trail live in ONE fsynced file, so 'what did
this run cost' and 'what did this run do' are the same replay.
three rules from every payments postmortem I've read: INTEGER
CENTS (floats round your money eventually, and eventually is
audit season); check affordability BEFORE the spend (a budget
that only notices overdrafts is a historian); and classify
budget-stop as RETRYABLE - tomorrow has a new budget, so the
dead letter office requeues it instead of parking it with the
revoked keys. the plan stopped at $39.90 of $45.00, which is
the entire point: the overdraft that didn't happen is invisible
in every metric except the one that matters.
source
# frozen_string_literal: true # The Spend Ledger: LLM plans spend real money, and money has rules # older than software - integer cents (floats round YOUR money, never # theirs), a ledger where every entry has a description, and a budget # that stops the spending BEFORE the overdraft, not in the postmortem. # The journal already receipts every task; this makes the receipts # denominate. # # bundle exec ruby examples/spend_ledger.rb # # Runs offline; the invoice at the end balances to the cent. require class="s">"bundler/setup" require class="s">"agentic" require class="s">"tmpdir" Agentic.logger.level = class="y">:fatal # Price list in INTEGER CENTS. 0.1 + 0.2 != 0.3 is a cute trivia # question everywhere except billing, where it's a lawsuit. PRICES = { class="s">"fetch:tickets" => 0, # api call, free tier class="s">"classify:batch" => 240, # cheap model class="s">"draft:responses" => 1875, # expensive model, long output class="s">"review:drafts" => 1875, class="s">"polish:tone" => 950, class="s">"render:report" => 0 }.freeze BUDGET_CENTS = 4_500 class SpendLedger attr_reader class="y">:entries def initialize(budget_cents:, journal:) @budget_cents = budget_cents @journal = journal @entries = [] end def spent_cents = @entries.sum { |e| e[class="y">:cents] } def remaining_cents = @budget_cents - spent_cents # The affordability check runs BEFORE the work: a budget that only # notices overdrafts is a historian, not a control def afford!(description, cents) if cents > remaining_cents @journal.record(class="y">:spend_declined, description: description, cents: cents, remaining_cents: remaining_cents) raise Agentic:class="y">:Errors:class="y">:LlmRateLimitError, # budget exhaustion is transient: tomorrow has a new budget class="s">"budget: #{description} costs #{format_cents(cents)} but only #{format_cents(remaining_cents)} remains" end @entries << {description: description, cents: cents} @journal.record(class="y">:spend, description: description, cents: cents, remaining_cents: remaining_cents) end def format_cents(cents) = format(class="s">"$%.2f", cents / 100.0) def invoice puts format(class="s">" %-20s %10s %12s", class="s">"item", class="s">"amount", class="s">"running") running = 0 @entries.each do |e| running += e[class="y">:cents] puts format(class="s">" %-20s %10s %12s", e[class="y">:description], format_cents(e[class="y">:cents]), format_cents(running)) end puts format(class="s">" %-20s %10s (budget %s)", class="s">"TOTAL", format_cents(spent_cents), format_cents(@budget_cents)) end end journal = Agentic:class="y">:ExecutionJournal.new(path: File.join(Dir.tmpdir, class="s">"agentic_spend.jsonl")) File.delete(journal.path) if File.exist?(journal.path) ledger = SpendLedger.new(budget_cents: BUDGET_CENTS, journal: journal) orchestrator = Agentic:class="y">:PlanOrchestrator.new( concurrency_limit: 1, lifecycle_hooks: journal.lifecycle_hooks, retry_policy: {max_retries: 0, retryable_errors: []} ) previous = nil PRICES.each do |name, cents| task = Agentic:class="y">:Task.new(description: name, agent_spec: {class="s">"name" => name, class="s">"instructions" => class="s">"w"}) orchestrator.add_task(task, previous ? [previous] : [], agent: ->(_t) { ledger.afford!(name, cents) class="s">"#{name} done" }) previous = task end puts class="s">"THE SPEND LEDGER (budget #{ledger.format_cents(BUDGET_CENTS)}, prices in integer cents)" puts result = orchestrator.execute_plan puts class="s">" plan status: #{result.status}" failed = result.results.values.find { |r| !r.successful? } puts class="s">" stopped at: #{failed.failure.message}" if failed puts puts class="s">" the invoice (from the ledger, balances to the cent):" ledger.invoice puts state = Agentic:class="y">:ExecutionJournal.replay(path: journal.path) declined = state.events.count { |e| e[class="y">:event] == class="s">"spend_declined" } puts class="s">" journal receipts: #{state.events.count { |e| e[class="y">:event] == "spendclass="s">" }} spends, #{declined} declined - the money" puts class="s">" trail and the work trail live in ONE fsynced file, so 'what did" puts class="s">" this run cost' and 'what did this run do' are the same replay." puts puts class="s">" three rules from every payments postmortem I've read: INTEGER" puts class="s">" CENTS (floats round your money eventually, and eventually is" puts class="s">" audit season); check affordability BEFORE the spend (a budget" puts class="s">" that only notices overdrafts is a historian); and classify" puts class="s">" budget-stop as RETRYABLE - tomorrow has a new budget, so the" puts class="s">" dead letter office requeues it instead of parking it with the" puts class="s">" revoked keys. the plan stopped at #{ledger.format_cents(ledger.spent_cents)} of #{ledger.format_cents(BUDGET_CENTS)}, which is" puts class="s">" the entire point: the overdraft that didn't happen is invisible" puts class="s">" in every metric except the one that matters."