Skip to content

Repository files navigation

ATTEST

It won't move money it can't prove.

ATTEST financial control — the native kernel reconciling 250 settlements on held-out seed 555001: 39 proven and eligible to automate, 210 ambiguous and held, 1 contradicted.

The live demo, on held-out evaluation seed 555001 — one of the two seeds in the benchmark panel, neither of which the policy was calibrated on. The headline is lower on held-out data than on a calibration seed. That is the point of holding it out.

A payment gateway does not pay a merchant order by order. It sends one lump sum covering hundreds of orders at once, minus its fees. Somebody has to establish what that credit was composed of, and getting it wrong marks the wrong customer as paid while the books balance either way.

Where a gateway's own recon report carries order-level references — Razorpay's does — much of that composition can be read off the report rather than reconstructed. ATTEST does not implement that join, and does not claim to: the adapter reads order_id and settlement_id to name and count the records it normalises, and every row then goes to the same engine. What ATTEST does is reconstruct and verify composition from amounts, fees and dates — the work that remains when the linkage does not close the explanation: a bank credit matching no single settlement, a period with no recon available, adjustments with no linked entity, or a merchant reconciling against a bank statement rather than against the gateway. In those cases composition has to be derived from the records rather than read off them, and the alternative today is a person in a spreadsheet.

ATTEST does that reconstruction, reports what it could prove, and when the evidence genuinely cannot tell two answers apart it says so instead of picking one — handing that case to a person with the exact thing that would settle it.

AI can decide. Your ledger can't guess.

ATTEST is a financial control layer that verifies AI-proposed actions before they can change financial state. Settlement reconciliation is the first workflow that demonstrates the control model.

That is not a slogan about safety, it is the shape of the system. An advisor that cannot be wrong is an advisor nobody would let near a real problem; the useful thing is to let it be wrong cheaply, in a place where being wrong costs a wasted search rather than a wrong customer's balance.

Four parties to every decision, in order of authority:

ADVISE   proposes investigative signals · cannot move money
VERIFY   independently reconstructs and proves the explanation
PERMIT   decides whether a proven result is safe to automate
RECORD   records only permitted execution

The primary object is a decision record: one financial event, those four parties, and what actually happened to the money. The front door opens on one; the workspace opens on one; the seven instruments are deeper cuts of the four rows rather than seven things to choose between.

The advisor in this build is a deterministic capture-batch ranking heuristic, and it is named as one on every screen — model_version reads none in every provenance record the product emits. It is honest about being weak: measured over 1,250 pools it offered an answer on 63 and was right on 27, which is below a coin flip, and it is disabled as a resolver for exactly that reason. It still runs, on every case, in the advisory slot — because the slot is the product and a slot nobody can inspect is a claim rather than an architecture. A language model implements the same signature and nothing downstream changes, because nothing downstream trusts it.

Both halves of that boundary are demonstrated on held-out data, in the product and below:

financial event advisor proposes verifier proves policy permits ledger records
action setl_000233 ₹6,523.53 3 orders, a capture batch 2,328 → 4 → 1 · PROVEN, residual 0 paise expected loss ₹247.82 < ₹250 → AUTO-POST ₹6,523.53 posted, balanced
hold setl_000225 ₹23,922.07 3 orders, and it would pick one of four 2,328 → 23 → 4 · AMBIGUOUS not eligible at any price → REVIEW no entry, and the missing field named

On the second one the advisor has an opinion — its anchor sits inside exactly one of the four surviving explanations, so a system that listened to it would post ₹23,922.07 today. It is right 27 times in 63. A 43% opinion is not evidence, so ₹12,630.27 is reported as settled whichever explanation is right, ₹30,107.39 is held, and the operator is handed the one field that ends the argument.

₹51,35,744.40   processed
₹51,348.48      posted without a person, 17 settlements, none of them wrong
₹47,97,685.11   stopped at verification
210             settlements blocked by one missing piece of evidence

For a reviewer — the five-minute path

If you only open three files: attest/verdict.py is the 35-line proof kernel and it does not import the solver — the solver imports it. attest/eval/baseline_panel.py is the benchmark that ATTEST loses on precision. attest/adapters/razorpay.py is the only module that knows Razorpay exists; it is read-only and has no write scope. Everything on screen describes generated data, always labelled as such.

Track 04 asks for an agent that closes one finance-ops loop across a 50+ record batch of synthetic data, reporting its match rate and the exceptions it could not resolve. ATTEST runs 250 settlements on the native kernel (both execution paths are tabled below), and reports exact set recovery of 16.0% over the 500-settlement held-out panel — the figure the evaluation is scored on, not the one a calibration seed flatters. The exceptions it could not resolve are the product rather than a footnote — and they leave the system as a work queue, with the contested orders, the blocker and the evidence that would settle each one:

curl "http://127.0.0.1:8420/api/export/queue?run=<id>&format=csv"

That export is a read. tests/test_export_safety.py pins it: the run, the ledger and the filesystem are compared before and after, and the source is read for the names of every mutating call.

Reproduce the demo

git clone https://github.com/kunalKumar-13/attest && cd attest
python3.13 -m venv .venv && ./.venv/bin/pip install -e .
cd native && maturin develop --release && cd ..   # optional kernel — see below
./run-demo

Two execution paths, and the demo uses the first.

solver envelope this run of 250
Native kernel — the recorded demo ₹2,00,000 39 proven · 210 ambiguous · 1 contradicted
Portable — no Rust toolchain needed ₹30,000 38 proven · 170 ambiguous · 1 contradicted · 41 insufficient

The 41 are not failures. They are settlements whose candidate space exceeds what the portable solver will attempt, so it reports INSUFFICIENT rather than searching a space it cannot finish — the same refusal the rest of this document is about, applied to compute instead of evidence.

./run-demo prints which path is active before any portfolio figure, so the two can never be confused.

The canonical case is identical on both. setl_000225 — ₹23,922.07, AMBIGUOUS, 2,328 → 23 → 4, four surviving explanations, the model/solver/ engine boundary, and the anchoring benchmark (C-006 in docs/CLAIMS.md). It was chosen for that reason. Only portfolio-wide counts diverge.

1. See it ./run-demo — opens the investigation at /, the instrument at /app. Every figure on both is read from the running engine at load.
2. Architecture docs/ARCHITECTURE.md · docs/ARCHITECTURE-DIAGRAM.md
3. Measurement docs/EVALUATION.md · benchmark/baselines.json — four methods over 500 held-out settlements, and exact_only beats us on precision
4. Razorpay boundary docs/RAZORPAY-INTEGRATION.md — read-only adapter, no live account contacted, and the engine's 21 modules contain no reference to the provider
5. What we refuse to claim docs/CLAIMS.md · docs/FAILURE-STORY.md

Fifteen documents in docs/. The working record — phase audits, design explorations, reversed decisions — is in docs/archive/ and is not part of the reading path.

The problem

Reconciliation systems are measured on how many cases they resolve, so they are built to resolve more. But a wrong financial explanation is worse than no explanation: candidate order sets discharge receivables against different customers, so posting the wrong one moves money against the wrong account while the books still balance.

Measured on this repository's benchmark, over 500 settlements:

                decided   wrong    false-proof rate
exact_only         22       0            0.0%     safe, and nearly useless
ATTEST             84       4            4.8%
greedy            462     439           95.0%     useful, and catastrophic

exact_only beats ATTEST on precision and we say so. The claim is not most accurate. It decides 84 settlements against exact_only's 22, at a false-proof rate one twentieth of greedy's — and it says which of its answers it could not establish.

The insight

AI should investigate. Deterministic systems should prove. Policy should control action.

One case

₹23,922.07       a bank credit
23 candidates    after the reductions, two of which are conventions
4 explanations   satisfy the amount exactly

₹12,630.27       settled whichever one is right
₹30,107.39       turns on which one is, across 17 orders

The model proposed an anchor — three orders captured together on the densest day in the window. The solver found it in all four explanations, so it separates nothing. The engine abstained.

AMBIGUOUS  →  UNPRICED  →  REVIEW  →  LEDGER UNCHANGED

An AI system could have picked one of those four. ATTEST refuses to.


ATTEST treats settlement reconciliation as a constrained proof problem, not as fuzzy matching. A bank credit is the net of some subset of the orders a merchant captured — after per-transaction fees, GST on those fees, refunds, and the T+2 settlement calendar. Finding that subset is subset-sum: NP-complete.

A merchant's bank statement shows one credit: ₹47,382.19. It is the net of some subset of the 400 orders they captured that week — minus per-transaction fees, minus GST on those fees, offset by a refund, shifted T+2 by the settlement calendar.

Which orders?

This is resolved by hand, in Excel, daily, at effectively every merchant in India.


The engine does not emit a confidence score

97% is not interpretable. It does not say what would have to be true for the answer to be wrong, it cannot be audited, and it invites a threshold — and a threshold invites posting an entry nobody can defend.

Instead every settlement receives a decidable property of the constraint system:

🟢 PROVEN exactly one assignment satisfies every constraint
🟡 AMBIGUOUS two or more do — the engine reports them and stops
🔴 CONTRADICTED none does — the engine reports which constraints conflict

These are counted, not estimated. The solver saturates its subset counter at two, because the question is never how many explanations exist, only whether the explanation is unique. Two bits per reachable sum decides the verdict.

The trusted kernel

The prover is large: blocking, a counting DP over the amount axis, constraint propagation, model-proposed hypotheses. The verifier is 35 lines (attest/verdict.py::check), depends on none of it, and recomputes every value from source records rather than trusting a single field on the proof.

A proof is accepted only if the kernel accepts it.

A bug anywhere in the prover can cost recall. It cannot post a wrong entry.

What this does not claim

Stated here rather than in an appendix, because a system that reports only what it wins has not been evaluated.

  • No live Razorpay account has ever been contacted. fetch performs a real authenticated request and has never been called with real credentials. The product says so itself, on the Trust lens, unprompted.
  • The bank statement is simulated. Every synthetic credit matches its settlement by construction, so the adapter cannot exercise the one case the engine exists for — a credit that corresponds to no single settlement.
  • exact_only is more precise than ATTEST: 0.0% false proofs against 4.8%. It achieves that by answering 22 settlements of 500 and declining 478. Published in the product as claim C-004.
  • The numbers describe generated data. That is what makes a false-proof rate knowable at all — the generator holds ground truth — and it is a population ATTEST created.
  • Coverage is 16%, not 90%. Most settlements are correctly refused rather than resolved.

docs/RAZORPAY-DEMO.md separates IMPLEMENTED from SIMULATED from NOT VERIFIED, capability by capability.

Same reason a proof assistant separates its kernel from its tactics.


Measured

Ground truth is exact by construction: orders are generated first, and settlements derived from them. Fifteen hazard families (attest/generate/taxonomy.py) are frozen — written before the matcher, so the benchmark cannot be tuned to flatter the engine.

2 held-out seeds × 250 settlements
calibrated on [20260821, 314159, 271828], evaluated on [555001, 999983]

RESOLUTION
  exact set recovery           16.0%   complete truth recovered
  coverage                     16.8%   resolved outright
  ambiguity rate               82.4%   correctly refused

SAFETY
  proof precision              0.952   right when it claims sure
  false proof rate             0.80%   ← the number that moves money

ACCOUNTED FOR
  settled (undisputed)     ₹67,66,131.23   agreed by every explanation
  disputed                 ₹75,73,097.75
  accounted for                68.8%   of all processed value

MONEY
  processed              ₹1,02,04,411.89
  auto-posted               ₹2,52,431.44
  protected                ₹99,51,980.45   refused, deliberately
  wrongly auto-posted              ₹0.00

NORTH STAR
  safe resolution rate          6.6%   resolved without a human

Every figure is read from benchmark/results.json, regenerated by python -m attest.eval.benchmark. Nothing about this engine is quoted from a second place — "precision 1.000" survived six days past the measurement that refuted it because a number had been typed into a README and never re-derived.

Read accounted for, not exact set recovery

16.0% complete recovery reads like an engine that fails five times out of six. That reading is wrong, and accounted for is why.

Most abstentions are not "we do not know". When several explanations survive, the orders appearing in every one of them belong to that settlement whichever explanation is right — so the engine can state that part as settled and name the exact remainder that is in dispute. Across the panel, 47% of ambiguous value turns out not to be in dispute at all.

A real case from the held-out seed: setl_000200, ₹92,666.62, with four surviving explanations. Twenty-three orders worth ₹91,599.60 appear in all four — 99% of the money is settled whichever explanation is right. Only ₹3,435.87 across eleven orders is contested — and the next step is not "investigate", it is "a reference on any one of those eleven settles the rest."

Coverage falls as portfolio density rises, and it is a limitation rather than a tuning knob: more orders sharing a window means more disjoint subsets land inside tolerance, so more settlements genuinely have more than one arithmetic explanation and are reported AMBIGUOUS. The absolute number of proofs still rises with size; the rate falls because the question gets harder faster. The false-proof count does not rise with it — the extra ambiguity is absorbed as refusal, not as guessing. Measured at three sizes, with the caveat that nothing here measures beyond 1,200 settlements in a 90-day window: docs/EVALUATION.md.

One number cannot describe this engine. 16.0% exact recovery and 0.952 proof precision measure different things: the first is how often the complete truth is recovered, the second how often the engine is right when it claims to be sure. Blending them into "95.2% accurate reconciliation" would be selling the second while doing the work of the first.

A decline is a correct outcome. The engine is built to refuse rather than guess, so declined is a feature and WRONG is the only real failure.

Against reference matchers, same data, same pools

  matcher      coverage   decided   wrong   false proof       pair prec
------------------------------------------------------------------
  attest          16.0%        84       4          4.8%        95.9%
  exact_only       4.4%        22       0          0.0%       100.0%
  fuzzy            3.6%        30      12         40.0%        60.0%
  greedy           4.6%       462     439         95.0%        16.5%

  500 settlements over seeds [555001, 999983], identical datasets and identical scoring

exact-only is more precise than ATTEST and answers a quarter as often. Precision alone is trivially winnable by declining, which is why coverage sits beside it. Greedy answers almost everything and is wrong almost every time — that is what a matcher with no way to abstain does.

Read the WRONG column. greedy declines 5% of the time and is wrong 90% of the time — 226 of 250 settlements posted against orders that did not produce them. That is what a matcher with no way to abstain actually does. fuzzy, the industry default, scores below doing nothing clever and buys it with 11 false proofs.

Greedy fails structurally, not by tuning: taking the largest order that fits is a local decision, and subset-sum has no greedy-choice property. One early take consumes an order a correct explanation needed and there is no way back.

Full methodology in eval/BASELINES.md.

A feature that was measured and then disabled

Cross-settlement constraint propagation (attest/evidence.py) works — an order belongs to exactly one settlement, so settlements are evidence about each other. It is off by default:

                  exact     WRONG    precision      (seed 20260821)
off               20.8%      0.0%      1.000
on                24.0%      3.6%      0.829

Three points of exact-set match, bought by going from zero false proofs to nine. One bad seed amplifies across the population, because propagation is only as sound as what it propagates from. A change that raises exact-set match by three points is not an improvement if it raises false proofs by the same amount. See FAILURES.md, D4.


Run

python3.13 -m venv .venv && ./.venv/bin/pip install -e .

./run-demo                              # the demo: engine + UI on :8420
./.venv/bin/python -m attest.web        # the same server, without the wrapper
#   http://127.0.0.1:8420/              the investigation — the front door
#   http://127.0.0.1:8420/app           the instrument workspace
./.venv/bin/python -m attest.eval.adversarial   # 35 attacks, source to ledger
./.venv/bin/python -m attest 250 --sweep   # the five-seed panel — report THIS
./.venv/bin/python -m attest 250        # a single seed
./.venv/bin/python -m attest 250 --html # emit report.html
ATTEST_PROP=1 ./.venv/bin/python -m attest 250   # reproduce the D4 ablation

The UI runs a generated portfolio or takes your own orders.csv and settlements.csv. Stdlib only — no framework, no build step, and nothing leaves the machine. It leads with rupees rather than row counts, because the question a merchant has is never "what is your exact-set match rate", it is how much of my money is accounted for, and what happened to the rest.

Navigation is four verbs, and depth lives inside a mode rather than beside it:

SUBJECT  ×  LENS  ×  CONTEXT

CONTROL      Where did the money stop?       JOURNAL      What entered the books?
EVIDENCE     Can the explanation be proved?  ACTIVITY     What actually happened?
INVESTIGATE  What would separate them?       TRUST        What can I believe?
POLICY       What is safe to automate?

A subject is what you are looking at — the portfolio, or one settlement. A lens is the question you are asking about it. A context is something inspected inside that state without leaving it. All three live in the URL, so every view is addressable and Back means what it says.

The case rail carries the subject through every lens: amount, verdict, where it stopped, what is agreed against what is disputed, and what to do next. Changing lens does not change the case, and opening a context moves neither.

CONTROL opens on the work, ranked by what each item unlocks rather than by what is stuck — in the native-kernel run, 210 ambiguous settlements are one action, not 210, because they are ambiguous for the same missing field. Each row states where it is blocked, why, what would unblock it, and whether ATTEST can do that itself. Three of them say it cannot.

The state spine runs down the rail on every lens — SOURCE → MATCHING → VERIFICATION → POLICY → ACTION — drawn as the proportion that survives each stage, so the collapse is legible before any number is read. Clicking a stage opens the instrument that owns it.

Four screens demonstrate rather than assert. Activity sends webhook deliveries through the same verify/de-duplicate/scope path the HTTP endpoint uses and reports what came back. Policy runs the permission pipeline against the current run, so a refused capability is something the code did. Trust reads the same benchmark files the build reads, so it cannot report a pass CI would fail. Investigate runs the hypothesis loop, discards its verdict, and shows the measurement that disabled it — read from benchmark/anchoring.json rather than transcribed.

⌘K reaches any lens and any settlement in the attention queue.

See docs/archive/UX-AUDIT.md for what the audit found and the defects that reading the screens turned up.

Optional: build the Rust kernel for the wider envelope and the 52× DP.

cd native && maturin develop --release

Layout

attest/model.py        fee model, tolerance derivation, records
attest/verdict.py      PROVEN/AMBIGUOUS/CONTRADICTED + the 35-line kernel
attest/blocking.py     calendar-inverted candidate generation, escalating
attest/subsetsum.py    counting DP over the amount axis
attest/evidence.py     cross-settlement propagation (off by default)
attest/generate/       hazard taxonomy + generator — FROZEN
attest/policy.py       Wilson-priced risk, the auto-post inequality
attest/ledger.py       the journal entry a proof implies, balanced to the paisa
attest/actions.py      the work ranked by value unlocked per step
attest/eval/          harness, baselines, ablations, regression gates
attest/rules.py        content-hashed rule set + run provenance
attest/agents.py       capabilities, and the four granted to nothing
attest/whatchanged.py  run-to-run diff with computed attribution (CLI)
attest/webhooks.py     raw-byte HMAC, idempotency on id AND payload hash
attest/api.py          the JSON API behind the Case Desk
attest/adapters/       source adapters; money.py reads amounts exactly or refuses
attest/ui/             the Case Desk — one subject, seven lenses, three axes
eval/cpsat_study.py    the CP-SAT measurement that rejected set packing
native/                Rust port of the DP hot path

The kernel

The DP is three ALU ops per cell and re-reads an array the width of the credit once per order. It is bandwidth-bound, not compute-bound, so the only optimisation that matters is making the array smaller.

Two bits is the floor — the verdict only distinguishes none / one / more than one. Rather than interleaving 2-bit lanes, which forces lane-masking on every shift, the counter is split into two bitplanes of one bit per sum: one[s] and many[s], mutually exclusive, so together they encode 0/1/2 exactly and each shifts with a plain bit-shift. The saturating add becomes six bitwise operations over 64 sums at a time:

both  = (one | many) & (s_one | s_many)     // both sides non-zero ⇒ ≥ 2
many' = many | s_many | both
one'  = (one | s_one) & !both
credit        numpy      native    speedup
₹20,000      275.6 ms   17.11 ms     16.1×
₹80,000    1,342.8 ms   25.46 ms     52.7×

DP footprint at ₹200,000:  4.8 MB   (one byte per sum would be 19.5 MB)

The speedup widens with credit size, which is what a bandwidth-bound kernel should do. Verified byte-identical to the numpy reference over 1,777 instances and 1.32 × 10¹¹ DP cells, across all fifteen hazard familiestobytes() compared, not array_equal, because solve sums a slice of this array and a dtype difference would change the sum.

This is not a benchmark flourish. The Python reference could only afford a ₹30,000 envelope, which silently skipped 14.8% of the portfolio — including every large bundle — before a single subset was examined. The port is what made those settlements reachable at all. Without the extension the engine falls back to numpy and a narrower envelope, and still runs correctly.

Documents

  • PRD.md — the problem, the algorithm, the tolerance derivation, the plan
  • FAILURES.md — what broke, daily. Four entries and counting
  • AGENTS.md — the working agreement, enforced by .githooks/pre-commit
  • docs/ARCHITECTURE.md — layers, the trusted kernel, search-space integrity
  • docs/ALGORITHMS.md — the tolerance derivation, the counting DP, why greedy and Hungarian both fail
  • docs/EVALUATION.md — ground truth, the metric vocabulary, baselines, the seed panel
  • docs/DECISIONS.md — fifteen ADRs, including the five that rejected work already built
  • native/BENCH.md — parity methodology and benchmarks

Evidence and boundaries

About

Financial reconciliation that refuses to invent certainty — the model proposes, a solver tests, and every settlement it cannot prove is reported as an exception rather than hidden. Python with an optional Rust kernel.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages