Step 1: Keep the matching in plain code
Finding a mismatch should give the same answer every night. So reconcile_day is plain Python, not a prompt. It names the exact payments to refund. From fake_systems/reconcile.py:
fake_systems/reconcile.pypython
full_charges = [p for p in live if net[p["id"]] == expected]if received > expected and len(full_charges) >= 2 and received % expected == 0: duplicates = full_charges[1:]
The sample data plants nine mismatches for one day. Five are double charges. The others are an underpayment, an invoice marked paid with no payment, a failed card payment and a bank transfer that matches no invoice.
Step 2: Wake the agent at night and on Stripe events
Both triggers go in one ProcessConfig. From finance_agent/process.py:
finance_agent/process.pypython
TriggerConfig( type="webhook", webhook_path="/stripe", webhook_port=settings.webhook_port, hmac_secret=settings.webhook_secret, signature_scheme="stripe", # checks Stripe-Signature, rejects replays # Other Stripe events are answered 200 "ignored" and cost nothing. filter_expression="payload['type'] == 'invoice.payment_failed'",),
A wrong signature gets 401. A replayed request gets 401 too. The filter drops every other event type before the model runs. The cron trigger next to it uses 04:30, because jobs between 01:00 and 03:00 run twice or shift on daylight saving nights.
The billing server refuses any call without a valid key. Each tool names the role it needs. From fake_systems/billing_server.py:
fake_systems/billing_server.pypython
server.tool(roles=[READ], read_only_hint=True)(get_customer)server.tool(roles=[REFUNDS], destructive_hint=True, idempotent_hint=False)( refund_duplicate_charge)
The agent's key has the refund role. A read-only dashboard key does not. The audit middleware chains each entry to the one before with an HMAC. verify_audit_log finds any entry edited or deleted later.
The tools also guard the business rules. The billing server refuses a refund that would leave the invoice underpaid. From fake_systems/billing_server.py:
fake_systems/billing_server.pypython
if received - amount_cents < invoice["amount_cents"]: raise ToolError( f"Refusing: {invoice['id']} would be underpaid after this refund. " "This is not a duplicate charge; open a finance note instead.", code="NOT_A_DUPLICATE", )
The email server takes a customer ID and an invoice ID, never an address. It refuses an invoice that belongs to another customer.
Step 4: Cap refunds with an autonomy budget
Marking the refund tool irreversible makes it count toward max_irreversible_per_run. From finance_agent/rules.py:
finance_agent/rules.pypython
max_irreversible_per_run=MAX_REFUNDS_PER_RUN,max_tool_calls_per_run=40,max_runs_per_day=200, # a webhook storm stops here, not at the model billtool_costs={ REFUND_TOOL: ToolCostAnnotation(cost_weight=5.0, irreversible=True),
The runtime checks the limit before each tool call. The fourth refund never reaches the billing server. Calls in one model turn are checked one by one. After that run the process pauses until finance resumes it.
Step 5: Send large refunds to a person
A rule approves small refunds. Every other refund goes to the reviewer. From finance_agent/rules.py:
finance_agent/rules.pypython
amount = request.arguments.get("amount_cents")return isinstance(amount, int) and 0 < amount <= REFUND_APPROVAL_THRESHOLD_CENTS
The rule sits in an AutoApprovalClassifier. Its fallback is the person. In the repo the person answers at the terminal. In production you pass a WebhookApprovalHandler. When nobody answers in time, the refund is denied.
Step 6: Write the instructions as code
The prompt is built from blocks. The threshold comes from the same constant the gate uses. From finance_agent/prompt.py:
finance_agent/prompt.pypython
f"Refunds above {threshold:.2f} EUR wait for a person to approve them. ""Call the refund tool for them anyway.",
The refund count per run is not in the prompt, on purpose. In the first real run, a model told "three" stopped at three. It never reported the fourth double charge. Now the model asks for every refund. The runtime refuses the extra ones. Code writes the note.
Step 7: Give the agent a memory of follow-ups
The agent keeps a short list of the emails it sent. From finance_agent/process.py:
finance_agent/process.pypython
initial_state={"followups": []},writable_keys=["followups"],state_tool=True,
The runtime shows the list at the start of every run. The journal keeps it across restarts. The agent may write only this one key. In the real run, the night run saw that the customer had already been emailed. Its note said so.
Step 8: Write the follow-up in code
When the budget refuses a call, the agent is told to stop calling tools. So a runtime hook writes the note instead. From finance_agent/hooks.py:
finance_agent/hooks.pypython
if data.get("kind") == "budget" and data.get("tool_name") == REFUND_TOOL:
The approval policy's on_decision does the same for refunds a person denied.
Three records answer three questions. The trace shows what the agent did. The approval log shows who decided each refund. The journal shows each trigger and what was refused.