Skip to content

Solution narrative

~4 min read · section overview · the three self-grade paragraphs are the thesis

TAKE-HOME BRIEF

The architecture axis asks: where is the LLM-versus-deterministic boundary and can you defend it; what is the data model; how do you handle async and stateful workflows; how is multi-tenant isolation enforced; and what is the integration surface. The three self-grade paragraphs below map directly to those questions. The short version: code orchestrates — the model is a typed tool, not the orchestrator.

System diagram

One event becomes one logged decision. The blue band is the only place a language model runs; the gold band is the autonomy gate. Everything else is code.

Inputs
Card authReap card spend webhook
Bill arrivalInbox or AP import
Payout / FXUSDC corridor event
ScheduleCron tick for the pulse
Ingest
Zod-typed unionFinanceEventOne canonical shape. Every skill reads from it. Tenant identifier is first-class.
Routing
Skill registryEach registered skill is asked whether the event is relevant for this tenant.
Cognition
DeterministicVendor-rule short-circuitTenant memory hit → skip the rest, log the decision.
DeterministicRules / scheduler / arithmeticPolicy engine, AP planner, pulse analyzer — all code.
Model · only hereLLM behind Zod schemaAuto-tagging classification. Output validated; on failure → refuse(model_unsafe).
Decision
OutcomeautoApplyAbove threshold; gated by autonomy rung.
OutcomesuggestRouted to operator queue.
OutcomerefuseStructured reason code. Not a silent failure.
OutcomeescalateAP money-movement, overdue, dual-control trigger.
Gate
Per-tenant, per-skillAutonomy rungShadow → Suggest → Assisted → Autonomous. The skills propose; the gate disposes. AP can never reach Autonomous.
Persistence
Versioned + idempotentDecision logOutcome · evidence · confidence · rationale · prompt/model/rules/CoA versions · idempotency hash of the full version tuple.
Surfaces
Operator queueReview, accept, override. Overrides become vendor rules + eval cases.
Bills / treasury / forecastRead models over the same decision log.
Morning briefPulse recommendations link back into queue rows.

TL;DR

A unified agentic platform for finance ops. Auto-tagging is built end-to-end. Policy enforcement, the Accounts Payable agent, and a scheduled Daily CFO Pulse run on the same platform — shared runtime, audit, autonomy ladder, and override learning loop. The framing: three reactive skills sit on one platform, and a proactive CFO layer now speaks first.

The honest positioning: this is not yet a full autonomous CFO. It is the control plane and first four finance skills that make an AI CFO credible: a typed financial event substrate, bounded cognition, proactive review surfaces, a cash-risk view, refusal-first safety, audit, evals, and a learning loop. To become a true AI CFO, the same spine needs live Reap rails, accounting writes, richer forecasts, and hardened scheduled risk monitoring.

"I treat the LLM as a constrained tool inside a deterministic workflow, not as the orchestrator. Every decision is versioned, logged, and reversible. The agent ships with four outcomes — apply, suggest, refuse, escalate — and a four-rung autonomy ladder that lets a tenant adopt at their own pace. Overrides feed three loops: per-vendor rules, retrieval index, and the eval set. Multi-tenant isolation is enforced at the data layer. The MVP runs end-to-end on mocked integrations with a real eval harness, and the production path swaps the synchronous runner for Inngest and the mocks for live adapters without rewriting the agent logic."


Contents


What it takes to become a true AI CFO

The bar is higher than "a chatbot over transactions." A real AI CFO has to notice problems before the finance team does, explain the evidence, and either take the safe action or escalate early enough that humans can still change the outcome.

The two contracts in code

The architecture is two type signatures. Everything else is implementation.

The Outcome union — every skill returns exactly one of these, no nulls, no silent skips
ts
// src/lib/refusal.ts
export const Outcome = z.discriminatedUnion("kind", [
  AutoApply,  // { kind, payload, confidence }
  Suggest,    // { kind, payload, confidence }
  Refuse,     // { kind, reasonCode, reasonDetail, candidates? }
  Escalate,   // { kind, toRole, reason, payload? }
]);

// Refusal reason codes are a closed set — the queue, evals, and
// override-capture all switch on these. Adding one is an architecture change.
const REASON_CODES = [
  "insufficient_evidence",  // no retrieval hits, no rule, low signal
  "ambiguous_match",        // two+ candidates with comparable confidence
  "missing_input",          // upstream data missing (e.g. no receipt for >$500)
  "policy_conflict",        // rule says one thing, retrieval says another
  "out_of_distribution",    // pattern not seen in training/cohort
  "model_unsafe",           // Zod validation failed N times
] as const;

model_unsafe is the one I care about most: when the LLM returns something that fails Zod validation N times, the skill refuses rather than guessing. That's the boundary between "the model is a tool" and "the model is the orchestrator."

The Skill interface — one shape lets the platform stay generic across four cognition styles
ts
// src/skills/types.ts
export type Skill<PayloadSchema extends z.ZodTypeAny = z.ZodTypeAny> = {
  id: SkillId;
  label: string;
  payloadSchema: PayloadSchema;
  triggers: (evt: FinanceEvent) => boolean;
  run: (
    evt: FinanceEvent,
    opts: { tenantId: string; modelOverride?: string },
  ) => Promise<SkillRunResult>;
  goldenPath: string;
};

export type SkillRunResult = {
  outcome: Outcome;          // the four-outcome contract above
  telemetry: SkillTelemetry; // modelId, promptVersion, rulesVersion, coaVersion,
                             // tokensIn, tokensOut, costMicrocents, rationale, evidence
};

run is a pure function of event + tenant context. That is what lets the runtime swap from synchronous to Inngest as a deploy, not a rewrite (see ADR-005). goldenPath is what lets the eval harness operate skill-by-skill without skills knowing the harness exists.

Self-grade against the three axes

Product thinkingController + CFO personas, governance-substrate wedge, reversal rate as the north-star, override-as-trust-surface.↓ §1
Architecture & cognitionFinanceEvent → skill registry → bounded cognition → decision log. The model is a typed tool, not the orchestrator.↓ §2
Production-readyVersioned decision rows, idempotency hash, structured refusal taxonomy, dual control on AP ≥$10k.↓ §3

Product thinking

I designed for two readers inside the product. The controller or accounts-payable clerk lives in the queue every day; the chief financial officer touches the dashboard, treasury cockpit, forecast, and morning brief. The wedge is finance-agent infrastructure on Reap rails: multi-currency fiat and United States Dollar Coin spend and bills, a typed refusal contract, and an autonomy ladder that lets each tenant graduate skill by skill. Every row carries confidence, evidence, and an override path because the "Why" panel is the trust surface. The north-star metric I would watch is reversal rate — auto-applied entries later overridden — plotted against token cost so improvement is visible, not asserted.

Architecture & cognition

Code orchestrates; the large language model is a tool. I made the vendor-rule short-circuit bypass the model entirely, and when the model is used its job is typed classification behind a Zod schema. All Reap-shaped events flow through one FinanceEvent union. The runtime is synchronous today and durable-target tomorrow through Inngest; the skill interface is shaped so that swap is a deployment change, not a rewrite. Tenant identifier is first-class on every table and query. Decision payloads are already shaped for accounting and payment adapters, but the real ledger and real payment rails remain explicit seams.

Production-ready

For production readiness, I focused on the failure modes that would break trust first. Every decision is a versioned row: prompt, model, chart-of-accounts, rules versions, evidence, confidence, rationale, and idempotency key. Refusal is a first-class outcome with a structured reason code. Schema failure becomes refuse(missing_input), off-chart-of-accounts output becomes refuse(out_of_distribution), and accounts-payable money-movement holds escalate instead of guessing. Idempotency is a hash of the full version tuple, so replays across prompt, model, chart of accounts, and policy edits are safe. Decisions are immutable by convention, vendor rules are append-only through supersession, and overrides are the rollback channel. Accounts Payable at or above $10k United States dollar-equivalent requires dual control; the second approval is the only path to execution.

Submission pack — Reap Chief Financial Officer Agent take-home