You are about to let software spend your money without asking.

Kairos catches a failing payment method in ninety seconds, steers live checkouts off it, and recovers the customers already lost. Four questions first.

SCROLL▼
WHAT KAIROS IS

A payment-health defence that runs for one merchant, under a mandate that merchant signed — a sealed document setting out exactly what it may do with their money, and what it may not.

A payment method is one way to pay through one bank — netbanking through HDFC, UPI through SBI, a card on one network. A busy merchant offers dozens of them, and any single one can start failing on its own while every other one looks perfectly healthy.

PAYMENT ATTEMPTS
KAIROS
CHECKOUT
MESSAGING
RETRY
LEDGER
SEALED MANDATE
WHERE IT SITSUNBOUNDED

IT MAY

  • Reorder a live checkout — move a failing method down, or off
  • Send a message, on the channel the customer last used
  • Retry a payment with nobody present
  • Do all of it across a fleet, concurrently

IT CANNOT

  • Spend past the campaign budget — not by one paisa
  • Contact anyone past the cap: three in seven days
  • Act during quiet hours, 21:00 to 08:00 IST
  • Run at all while the kill switch is engaged

The left column is what a mandate grants. The right is what it withholds — and the right column is the product.

CLOCK 14:14
WALKED AWAY 0
↑ CARD MACHINE
ONE SHOP · ONE AFTERNOONTRADING NORMALLY

A bank's payment method starts failing. One payment in three.

Customers tap Pay, get an error, and leave.

Nobody notices for forty minutes.

Kairos sees it in 93 seconds.

Which was never the hard part.

WHAT IT'S WORTH TO THE BUSINESS

Three separate pieces of money — and only one of them is the one every recovery tool talks about.

The loss you never take

11–50percentage points fewer failed payments, among the customers who would have landed on the broken method

The range is two different incidents: UPI through one bank failing 42% of its attempts, and netbanking through another failing 55%. The worse a method is behaving, the more there is to save.

The money you get back

59×recovered for every rupee spent trying — and that rupee already prices every customer lost to an opt-out

Counted as incremental: what came back because of the system, on top of what would have come back on its own. Most dunning numbers are gross, which makes them roughly five times larger and not a measurement.

The customers you keep

59%fewer opt-outs than a fixed reminder schedule — which sent nearly twice as many messages and still recovered less

Against that schedule: 17% more money on 46% fewer messages. An opt-out is permanent, so the channel is an asset you spend, and almost nothing prices it.

AND THE REASON IT CAN RUN WITH NOBODY WATCHING

The obvious alternative doesn't just overspend. It messages one customer fourteen times.

same budget · 64 workers running at once Kairos a plain if (spent < budget) check
of the budget, spent99.6%120%
over budgetnothingone fifth of it again
times it broke the contact cap0271
most messages to any one person, against a cap of 3314

A merchant can leave this running because it cannot spend their money twice or burn a customer they will need next month. That is the whole economic unlock: no approval queue. An approval queue is what makes every other version of this too expensive to operate, because it needs a person who is awake.

WHAT IT DOES ABOUT IT — THREE JOBS
BROKEN
HEALTHY

PREVENTION

Moves customers off the failing method before they ever see an error.

STRANDED
A LATER TRAIN

RECOVERY

Knows why each payment died. An expired card needs a fix-link, not a retry.

FLIPPING

LANGUAGE

468 message variants in four languages, written once per situation.

QUESTION 01 OF 04

How do I know it won't overspend?

LEVER 1
LEVER 2
LEVER 2 · FREE
MECHANICAL INTERLOCKINGBOTH LEVERS FREE

₹500 budget. 64 workers at once.

₹0 over.

A plain budget check spends ₹600.

QUESTION 02 OF 04

How do I know it actually helped?

TREATED
HOLDOUT
GAP —
THE ONLY HONEST MEASUREMENTBOTH ARMS EQUAL

A holdout is always kept back.

Counted as incremental, never gross.

17% more money on half the messages.

QUESTION 03 OF 04

How do I know what it did?

WINDOW ON THE LINE
FOUR LEVERS · 0 PULLED
SIGNAL BOX · UNMANNEDAT REST

Every action carries signed authority.

Every refusal names the bound.

No figure without a record.

QUESTION 04 OF 04 · THE ONE NOBODY ASKS

What don't you know?

NETBANKING · HDFC
SEEDED RUN
WHAT THE BAND ALLOWS
CHART RECORDERRATE 10% · QUIET

Every figure here is a seeded simulation, not production traffic.

The copy advantage rests on a readability weight nobody has measured.

One band is wider than the value it guards.

All three are on the scorecard, not in a footnote.

SIGNAL AT DANGER
HOME SIGNAL · DAWNDANGER
WHY THAT NAME

Chronos is clock time. Kairos is the moment that still works.

Every dunning system in the world retries on chronos.

github.com/AmeyaBorkar/kairos →

01  THE FILM

THE CASE, END TO END

The film is the argument. The system itself is next door — a recorded run of the real detector, kernel and ledger, steppable a decision at a time: open the demo →

1080p60 · H.264 · 40 MB · download this cut. Mastered at 3840×2160, which is too large to serve from here — the 4K master is on Google Drive.

02  SEE IT WORK

Kairos is a backend service with no interface of its own. It sits behind a checkout somebody else built and works a queue nobody watches, so there is no product screen to show you and this page is not a screenshot of one. It is a worked simulation: the real detector, kernel and ledger, replayed from a recorded run and drawn out so decisions taken in milliseconds can be read at the speed a person reads.

One payment method starts failing. Here is what Kairos does about it, a step at a time — and, at the end, what it refuses to do. Every number on this page comes out of that run. Nothing here is computed to make a point.

RECORDED RUN · simulated traffic · seed 20260826 · replayed, not live

It watches every way to pay, separately.

WHAT YOU ARE LOOKING AT

1 / 7

WHAT TO WATCH FOR

THE SAME RUN, AS AN OPERATOR SEES IT

Rail health, open incidents, which bound is binding, and the audit trail — the dense view, for reading during an incident rather than for learning the system.

--:--:-- FRAME 1 / 48
SIM CLOCK--:--:--
INCIDENTS OPEN0
LEDGER RECORDS0
WORST RAIL—
STEERED0
REFUSALS0
RAIL HEALTH — WHAT THE DETECTOR BELIEVES

The bar under each rail is the detector's running statistic against its threshold. The simulator knows the ground truth; this panel deliberately does not show it, because an operator will never have that comparison.

AUDIT TRAIL — NEWEST FIRST
◆ LEDGER HASHES END TO END · 0 RECORDS
OPEN INCIDENTS
BOUNDS IN FORCE
RECOVERY

Not driven by this run. The recovery arm is measured in the benchmark, where it recovers ₹6,82,069 — but the demo does not yet step it, so contact-cap and campaign-budget never bind here. That gap is why budget-exhaustion below never exhausts its ₹20: a steer reserves ₹3 and reconciles to zero.

04  HOW IT WORKS

One incident, from the minute a payment method starts failing to the minute the ledger closes on it. Everything below happens without anybody approving anything — which is why the bounds matter more than the capabilities.

ONE INCIDENT, MINUTE BY MINUTE
  1. 14:14Netbanking through HDFC starts failing about half its attempts. Nothing is down; it is just losing every second customer.
  2. 14:15The detector's statistic crosses its threshold and an incident opens. 93 seconds from the changepoint, which is the number the benchmark measures.
  3. 14:16The controller proposes steering. It is a proposal, not an action: only the kernel can authorise one.
  4. 14:16Terminus checks the mandate, reserves 300 paise against the campaign budget, and signs the record. The money is committed before it is spent.
  5. 14:18Checkout reorders. UPI and cards move above netbanking for customers whose payment would have landed on the failing method. A holdout is deliberately left on it.
  6. 14:41The customers it did not save in time are classified by why they failed — expired card, insufficient funds, issuer down. Only about one in eight can be retried unattended; the rest need a message.
  7. 14:52Messages compose from the library — the right words for that failure, on that method, in a language the customer reads. No model is called here.
  8. 15:30REFUSED. A customer has already had three messages in seven days. The contact cap binds and the send does not happen. The refusal is recorded with the bound that caused it.
  9. 17:02The method recovers. Steering is released and the reservation reconciles against what was actually spent.
  10. 20:30The campaign closes. Every reservation is reconciled, every action is signed, and the ledger hashes end to end.

Two of those steps are the whole argument. At 14:16 the money was committed before it was spent, and at 15:30 something the system wanted to do simply did not happen. Scrub a real run of this →

WHERE THE MODEL IS — AND WHERE IT IS NOT

OFF THE PATH · ONCE · ₹8.15

A model writes the message library: 468 validated variants across four languages, one call per situation rather than per customer. 180 calls in total. The output is committed to the repository and reviewed as a diff, like any other code.

It also writes the operator's explanations — and refuses to publish one if any figure in it does not appear, character for character, in an audit record.

ON THE PATH · NEVER

At runtime, choosing what to send is a lookup, not a call. Nothing on the path where money moves waits on a provider, a quota, or a sampled token.

A model you cannot reach at 14:14 cannot be prompt-injected, cannot rate-limit you during the incident, and cannot invent a refund amount. That is not caution about AI; it is where this particular tool is worth its latency and where it is not.

THE PATH OF ONE ACTION
traffic→ detect→ policy→ TERMINUS→ adapter→ ledger

Nothing skips Terminus. The policy layer proposes; only the kernel can authorise, and it writes the record either way — an allowed action and a refusal cost the same to audit.

THE CODE — FOURTEEN PACKAGES

◀ DRAG ▶  ·  FOURTEEN OF THEM, NONE OF THE CORE ONES IMPORTING A NETWORK CLIENT

@kairos/domain

Money, ids, time, rails. Paise as a branded integer, so a rupee float cannot reach a budget.

depends on: nothing

@kairos/detect

One-sided CUSUM changepoint over a method's failure rate, with a frozen baseline and a rollup across slices.

depends on: domain

@kairos/terminus

The kernel. Sealed mandates, reservations, reconciliation, and a refusal that names its binding axis.

depends on: domain

@kairos/policy

Proposes steers. Declines when the failing method is no worse than where customers would land.

depends on: domain, detect

@kairos/recover

Classifies why each payment died and picks retry, fix-link or message accordingly.

depends on: domain, terminus, reason

@kairos/reason

The amortised copy library and the honesty check that verifies every figure against a record.

depends on: domain

@kairos/explain

Retrieve, redact, describe the bounds, ask — and refuse rather than caption.

depends on: domain, reason

@kairos/ledger

Hash-chained audit records, verified on read rather than remembered.

depends on: domain

adapters/simulator

Seeded traffic, rail degradations, a customer population with a language mix.

test surface only

adapters/razorpay

The real ports: Checkout ordering, messaging, retries. Template copy lives here as the control arm.

depends on: domain

apps/bench

Five arms, eight seeds, regression bands. The gate CI runs.

depends on: everything

apps/console

The read model behind the demo, plus explain.

depends on: everything

adapters/postgres

The casualty queue, durable. An atomic lease is what turns the worker from one instance into a fleet.

depends on: domain, recover

apps/mandate

A form that authors a mandate and a command that signs it — deliberately two programs, so the key stays where it is.

depends on: domain, recover, terminus
RUN IT YOURSELF — NO API KEY REQUIRED
# the console, against simulated traffic
$ export KAIROS_MANDATE_SECRET=$(openssl rand -hex 32)
$ pnpm --filter @kairos/console run start
$ curl localhost:8788/api/scenarios
$ curl -X POST localhost:8788/api/step \
    -H 'content-type: application/json' \
    -d '{"ticks":20}'

The client drives the clock rather than the server running a timer, so an incident can be stepped through by hand while recording.

# why did it treat this one that way?
$ pnpm --filter @kairos/console run explain -- --list
$ pnpm --filter @kairos/console run explain -- cas_9f21

# the benchmark gate CI runs
$ pnpm bench:gate

explain prints the audit records with or without a model. Prose is added only if one is configured and every figure in it came from a record.

STANDING CONSTRAINTS

NO INFERENCE ON THE MONEY PATH

468 message variants are written once, offline, for ₹8.15 and committed as a reviewed diff. At runtime it is a lookup, not a call.

CLEAN AND BISECTABLE

Every commit builds, lints, typechecks and tests standalone — verified in a clean worktree, not just at HEAD.

THE GATE IS THE SPEC

Regression bands come from an eight-seed study. A change that moves a figure outside its band does not merge.

03  PUT IT IN FRONT OF YOUR CHECKOUT

Kairos is a service you run beside the gateway you already have. It never sits in the payment path: it answers one question before the page renders, reads the outcomes you already receive, and works the casualties afterwards. Everything below runs offline, with no API key, and sends nothing to anybody until you say so.

WHAT YOU NEED

NODE ≥ 22 · PNPM ≥ 11

That is the whole toolchain. The benchmark is seeded and offline and the language-model adapter replays a committed recording, so nothing here needs a key to run or to test.

A GATEWAY

The Razorpay adapter ships, and its client and inbound webhook have both been run against the live API in test mode. Gateway and Messenger are ports — one method each, charge and send — so another gateway is an adapter rather than a fork. Nothing in the core imports a network client.

POSTGRES — OPTIONAL

Without it the recovery worker is a single instance holding its own queue. With it, the queue and the spend authority both live in the database and the same command is a fleet.

RUN IT
$ git clone https://github.com/AmeyaBorkar/kairos
$ cd kairos && pnpm install && pnpm build

# the whole gate: lint, deps, types, 1,099 tests
$ pnpm check

# the signing key. there is no development default —
# a mandate signed with a known key is one anyone can forge
$ export KAIROS_MANDATE_SECRET=$(openssl rand -hex 32)
$ pnpm --filter @kairos/sentry start
→ listening on :8080

Two processes, and they do not talk to each other. sentry is stateless and horizontally scalable: it ingests outcomes, runs detection, and answers the checkout. recover-worker drains the casualty queue. They share a mandate and a ledger, not a connection.

The benchmark, the operator console and the copy library all run from the same checkout with no credentials at all — which is the point of keeping every outside system behind a port.

1 · THE CHECKOUT ASKS WHAT TO RENDER

One call, on the page you already have, before you render the method list. It answers in Kairos vocabulary and as the config object Razorpay Checkout takes, so a merchant on another gateway needs no translation layer — and neither reading is derived from the other, because that derivation is where a translation bug would live.

# request
$ curl -X POST localhost:8080/plan \
    -H 'content-type: application/json' \
    -d '{"customer":"cus_9f3b2a71c4e8d012",
         "sequence":["upi","card","netbanking"]}'
# response
{ "sequence": ["upi","card","netbanking"],
  "suppress": [], "demote": [],
  "steered": false, "arm": "treated",
  "checkout": { "display": { … } },
  "maxAgeMs": 15000,
  "reason": "nothing in force" }
fieldwhat to do with it
sequenceRender the methods in this order. Exhaustive over what you offered.
suppressInstruments to leave off the page entirely — a specific bank, a specific card issuer.
demoteMethods pushed down the list. Nothing is removed.
checkoutThe same decision as Razorpay's config. Pass it through unchanged.
armEcho this back on the outcome. It is how the holdout stays honest at an integration that has never heard of one.
steeredFalse means this response changes nothing — including when Kairos failed and declined to guess.
maxAgeMsHow long the answer keeps. Anything fresher is a request the service did not need to answer.
reasonWhy it says what it says, in words. Always present. Log it.

It always answers 200. A body that does not parse gets your own ordering back with the offending field named in reason — visible on the first call an integrator makes, and harmless to the customer waiting on the page. The failure mode of a payment-health tool must never be "no payments".

2 · RETURN THE OUTCOMES YOU ALREADY GET
$ curl -X POST localhost:8080/outcomes \
    -H 'content-type: application/json' \
    -d '{"attempts":[{
      "id":"pay_1", "orderId":"order_1",
      "customer":"cus_9f3b2a71c4e8d012",
      "amountPaise":120000,
      "method":"upi", "issuer":"hdfc",
      "status":"failed", "at":1756900000000,
      "arm":"treated" }]}'

→ {"accepted":1,"opened":0,"incidents":0}

The same events your webhook already delivers, batched up to a thousand at a time. Unlike the plan route this one rejects what it cannot parse rather than coercing it: an outcome of the wrong shape is evidence about the integration, not about the rails, and quietly accepting it would put a slice in the detector that nothing else can reason about.

customer must be at least sixteen characters. That is a structural guard rather than a formality — a keyed hash always clears it and a bare phone number never does, so a raw identifier cannot reach the detector, the ledger or a model prompt by accident.

2 · OR POINT RAZORPAY STRAIGHT AT IT
# Dashboard → Settings → Webhooks
$ RAZORPAY_WEBHOOK_SECRET=… \
  LEDGER_PII_HASH_KEY=… \
  pnpm --filter @kairos/sentry start

POST /webhooks/razorpay

→ {"event":"payment.failed",
   "method":"card","status":"failed",
   "failure":{
    "code":"BAD_REQUEST_ERROR",
    "source":"business",
    "step":"payment_initiation",
    "reason":"international_
     transaction_not_allowed"}}

Report nothing at all: point the dashboard at Kairos and the gateway delivers. Three things must be true before a delivery changes any state — the signature verifies against the raw bytes, the event is recent, and it is not one already handled. Anything failing one of them is answered 200 with the reason in the body, because a gateway retries a non-2xx for hours and a bad signature does not become good on the fourth attempt.

What the gateway says about the failure is passed through untranslated. source, step and reason are the triple the classifier reads, and re-coding them into a private enum would put a lossy mapping between the gateway's account of what went wrong and the decision made about it.

A payment carries email and contact. The translation takes a pseudonymiser as a required argument, so a phone number cannot reach the detector by omission — the reference above is a keyed hash and there is nowhere else the raw value goes.

3 · WORK THE CASUALTIES
# decides everything, sends nothing
$ KAIROS_MERCHANT_ID=acme \
  KAIROS_LINK_BASE=https://pay.acme.test \
  pnpm --filter @kairos/recover-worker start

# one instance → a fleet
$ export KAIROS_DATABASE_URL=postgres://…
$ pnpm --filter @kairos/postgres run schema \
    | psql "$KAIROS_DATABASE_URL"

Dry-run delivery is the default and live is opt-in, because getting that backwards is unrecoverable: a message sent by mistake cannot be unsent, and a merchant who meant to observe for a week and instead messaged their customers has a problem no rollback fixes. Every decision is real; nothing leaves the process.

With a database, two things move out of memory and they are not the same protection. Authority stops two workers double-spending. The queue's atomic lease stops them double-sending — idempotent authority hands both workers the same grant for the same casualty.

AUTHORISE IT — NOTHING SPENDS WITHOUT A MANDATE

A signed mandate is unreadable by design: paise and epoch milliseconds, and the two numbers that matter most are not in it but are products of numbers that are. Nobody authorising spend can be asked to do that arithmetic in their head, so the tool does it and prints it before it signs.

# author one in a browser, on loopback — or without one
$ pnpm --filter @kairos/mandate start form
$ kairos-mandate seal spec.json > mandate.json   # reading to stderr, mandate to stdout

MONEY
  Ceiling for the whole campaign         ₹5,000.00
  Most one action may cost                   ₹0.60
  Most that can be committed at once         ₹9.60   (16 actions in flight)
  Actions the budget buys, worst case        8,333

PEOPLE
  At most 3 messages to one person per 7 days.
  Nothing sent between 21:00 and 08:00 at UTC+05:30.

SIGNATURE
  bb498b0eeb882107c7a1…f063820eec3c6
  Verified. Every field above is covered by this signature and none of them has been altered.

The page collects a spec — rupees, days, clock times — and posts it back for conversion, so there is exactly one implementation of "₹5,000 for 30 days" and the form calls it rather than re-deriving it. It never holds the signing key: the process serving it seals, or says it cannot and refuses. Loopback only, and not configurable.

CONFIGURATION — EVERY DEFAULT IS THE ONE IN THE CODE
variabledefaultwhat it decides
KAIROS_MANDATE_SECRET—Required, ≥32 chars. Refused rather than defaulted: a development fallback here is a key that ships to production the first time somebody forgets a variable.
KAIROS_MERCHANT_ID—Required by the worker. Namespaces the budget and the contact caps.
KAIROS_DATABASE_URLunsetUnset is a single instance. Set is a fleet. That is the whole difference.
KAIROS_DELIVERYdry-runAnything else needs a live gateway and a DLT-registered sender, and refuses to start without them.
KAIROS_BUDGET_PAISE100000The campaign's lifetime ceiling — ₹1,000. Not a rate.
KAIROS_MAX_IN_FLIGHT16Blast radius. Granted by the mandate, so adding workers cannot raise it.
KAIROS_CONTACT_LIMIT3Messages to one person per window, across every casualty of theirs.
KAIROS_CONTACT_WINDOW_MS7 daysRolling, not calendar — a calendar week allows six messages in twelve hours.
KAIROS_RESERVATION_TTL_MS120000How long authority survives unreconciled. Must outlast the slowest action.
KAIROS_DRAIN_INTERVAL_MS15000How often the worker looks at the queue.
KAIROS_DB_MIGRATEonoff hands the schema to a deployment that owns its own migrations.
PORT8080Where sentry listens.

Quiet hours are not configurable. 21:00–08:00 IST, and a merchant who wants to message people at three in the morning wants something Kairos does not do.

WHAT IT DOES WHEN SOMETHING GOES WRONG

Every one of these is a code path with a test, not an intention. The rule underneath all of them: a request that cannot be evaluated must not be acted on, and a checkout that cannot be advised must still render.

whenwhat happens
Kairos is unreachable, or slowYour checkout renders your own ordering. That is what it would have done without any of this.
A plan takes longer than 50 msSame. The budget is measured, not hoped for, and blowing it is a fallback rather than a wait.
A customer reference does not parseSame, with the fault named in reason. Still a 200.
The shared store cannot be readThe contact allowance reads zero. A policy that cannot tell how much it has already spoken to somebody assumes it has spoken to them enough.
The kill switch cannot be readCounted as engaged. Not knowing whether you have been told to stop is being told to stop.
The audit sink failsThe admission is refused. A decision that cannot be recorded is not taken.
A mandate does not verifyEvery action refused by name — including one whose budget was raised after signing.
A worker dies mid-actionThe reservation expires and its authority returns to the pool; the lease expires and the next pass picks the casualty up unchanged. Nothing leaks and nothing is sent twice.
Either kill switch is thrownEverything stops. The signed one cannot be cleared by anyone who cannot sign; the store-backed one propagates fleet-wide within one admission with no redeploy.
WHAT IS NOT BUILT — SO YOU FIND OUT NOW

LIVE DELIVERY

The recovery arm needs a Razorpay account able to charge saved tokens and a DLT-registered sender. It refuses to start pretending otherwise.

ONE AUDIT CHAIN PER WORKER

A fleet shares its queue and its budget but not its ledger. Every record is still written and still tamper-evident; "show me everything" means interleaving N chains.

MANDATE DISTRIBUTION

A sealed mandate is a file somebody carries to a worker. There is no control plane that hands one out, rotates it, or revokes it short of the kill switch.

The full list is eighteen entries long and lives in docs/ARCHITECTURE.md under honest gaps, next to the things that do work. And the numbers →

05  MEASURED, NOT ASSERTED

Every figure here was produced by a seeded benchmark anybody can re-run, and carries a regression band derived from an eight-seed variance study. Nothing on this page is an estimate.

DETECTION — TIMED FROM THE CHANGEPOINT, NOT THE ALARM
93smedian detection latency
83%of degradations caught
0.14false alarms per hour — two, across fourteen hours of healthy traffic
2.5minutes, median, from the rail recovering to the incident closing
100%of incidents still open at the worst moment of the outage they describe

Measuring from the alarm rather than the changepoint would make any detector look instant. It is the single most common way this number is inflated.

The false-alarm rate rests on two observed alarms across fourteen hours of healthy traffic, so the interval around it is wide. Read it as roughly one an evening, not as two decimal places.

SPEND — 64 CONCURRENT WORKERS AGAINST A ₹500 BUDGET
₹0overspend, Terminus
₹100overspend, a plain budget check — 20% on ₹500, 185% on ₹60
₹500 BUDGET · 64 CONCURRENT WORKERS · 4,000 CASUALTIES
Terminus — reserve, then reconcile₹500
a plain budget check₹600

A budget check is if (spent + cost > budget) return. That is a rule, and rules race: two workers read the same number and both spend it. Terminus reserves before acting and reconciles against what actually happened — the software version of a locking bar that jams the conflicting lever. Not ₹524. Not "about ₹500."

RECOVERY — FIVE ARMS, ONE POPULATION, ONE SEED

Every arm ran against the same simulated merchant: four hours of traffic, 300 payment attempts a minute, a pool of 12,000 customers, with a netbanking rail and a UPI rail degraded partway through. The rupee totals below are only meaningful against each other — they belong to that population and to no real merchant.

armincrementalmessagesconsent lost₹ per message
do nothing—00—
message everyone₹1,40,7365,54467₹25
fixed ladder₹5,84,00610,681132₹55
kairos · template copy₹6,13,8435,81758₹106
kairos · generated copy₹6,82,0695,74954₹119

Incremental — gross, minus what came back unaided. The fixed ladder recovers 14% less than Kairos while sending 1.9× as many messages and losing 2.4× the consent.

THE FINDING WE DIDN'T WANT
Generated copy was expected to win on writing — naming the rail, saying what to do next. Swept against the readability weight it rests on, across four seeds, it wins on readability alone: at an illegibility penalty of 1.00 the advantage falls from ₹1.05 lakh to ₹10,367, on a range that straddles zero.

The generated column does not move across the sweep at all. What is reported here is the sweep, not the headline.
VARIANCE — WHY THE BANDS ARE THE WIDTH THEY ARE

recover.incrementalPaise

cv
4.18%

Tight enough that a real regression is visible.

recover.generatedGainPaise

cv
37.6%

Which is exactly why the generated-copy claim above is stated as a sweep and not a number.

A single-seed sweep once came out non-monotonic — a penalty of 0.25 scoring 4.6% below a penalty of 0.00, when it should have scored above. Against a measured run-to-run variation of 4.18%, that was the noise floor and nothing else. The fix was four seeds and a reported worst case, not a better-looking chart.

WHERE THESE NUMBERS COME FROM

Every figure on this site was produced by a benchmark in the repository, against simulated payment traffic. None of it is from a live merchant. This is a Razorpay AI Buildathon project and it will not pretend otherwise.

Simulated does not mean invented. The traffic is seeded, so the same seed gives the same run on any machine; the components under test are the real detector, the real kernel and the real ledger; and the harness is in the repo for anybody who wants to disagree with it.

  • Recovery — 4 hours of traffic, 300 attempts a minute, a pool of 12,000 customers, with two of those methods degraded partway through
  • Prevention — 45 minutes observed per incident, 400 attempts a minute, 20,000 customers
  • Detection — 6 failure shapes × 4 seeds, plus 12 runs where nothing is wrong at all, which is how the false-alarm rate is measured
  • Spend — the same campaign replayed at 1, 2, 8, 32 and 64 workers at once
  • Every figure carries a band from an eight-seed variance study, and CI refuses a change that moves one outside it

All of it, with the caveats →