Fraud & risk controls
A control, not a detector.
Ratchet will never tell you a payment is fraudulent. It has no device signals, no identity graph, and it cannot see inside a payload. What it does is stand in front of the action with the authority to refuse — and that turns out to be the position nobody else occupies.
Card fraud, account takeover and synthetic identity are detection problems on inbound traffic, and there are large companies solving them. This page is about the other half: outbound actions your own automation takes, correctly authorised and completely wrong.
The shift nobody has a product for
The question is changing from “is this transaction legitimate?” to “is this agent still doing what we told it?”
An agent with a payments credential and a poisoned instruction is not a transaction anomaly. Every call it makes is well-formed, correctly signed, and inside its permissions. Nothing in the payload is unusual. The fraud is in the pattern and in the authority, and both of those are visible from exactly one place: in front of the action, holding the rules.
Prompt injection
An agent reads a hostile document, an email, a web page, and starts acting on instructions that are not yours. It still holds your credential.
A stolen agent key
Someone else is now the agent. Nothing about the traffic looks different, because nothing about it is different.
A loop that will not stop
No malice at all: a retry that never terminates, a batch that runs twice, a disbursement job pointed at the wrong file.
A path that never asked
An old cron job, a leaked key, a colleague's script. Not visible to a gate, because it never came through one.
The mechanism
It never learns who. It always knows how much.
A reader told us they had this backwards, so plainly: the amount is not hidden from Ratchet. It cannot be — adding it up is the entire job. What is hidden is who the money is going to.
Why that is safe
The identifier is unusable by us, and unrelated between tenants.
What is kept is HMAC(secret, workspace | name | value), truncated to 128 bits.
It cannot be reversed. The workspace id is inside the MAC, so the same account
number in two customers' workspaces produces two unrelated identifiers — there is no
cross-tenant correlation to leak even by accident.
A declaration is a claim by the caller, and it can only tighten. It adds whatever ceiling policy keys on that dimension; it never removes the workspace, key or effect-type ceilings that already applied. Omitting one that policy requires is refused, so omission is not an escape. And a caller that lies about the value lands in a different bucket without gaining anything it did not already have — your vendor knows the real destination, which is what makes reconciliation the place a lie surfaces.
PUT /v1/policies/payment.refund
{
"required_dimensions": ["counterparty"],
"dimension_limits": {
"counterparty": { "daily_micros": 200000000, "daily_count": 20 }
}
}
daily_count applies to effects that declare no cost at all. Velocity is not a
money question — “no more than five to this address per day” is the
control that matters for outbound messaging.
The ceiling holding
Twenty refunds. One account.
$500 each, all to the same destination, all correctly authorised. A daily ceiling of $2,000 is configured on that dimension. Every attempt is asked; the gate answers.
Not a simulation
The first four go through and the fifth is refused, live.
That ceiling was configured on production and exercised against it. Four $500 refunds to
one blinded counterparty executed; the fifth came back
budget_exceeded. The twenty-attempt arithmetic above is that same rule
continued — it is what the ceiling does, not a measurement of anything else.
Checking the database afterwards, no row contained the account number. What was there was a
single bucket, dim:counterparty:9eb4dace…, holding
spent_micros: 2000000000 and used_count: 4.
Where this wins, and where it does not
Repetition-shaped loss, in two industries that have a lot of it.
The controls above catch a specific shape: the same thing happening more times than it should, to the same place. That shape is not all fraud. It is a great deal of the fraud that automation causes.
Retail — refund and promo abuse
Abuse here is repetition by nature. Declare the customer as a dimension and “same customer, fourteen refunds across fourteen orders” hits a velocity ceiling on the fifth — without you ever handing us a customer id, because only a keyed hash of it is stored. The same shape catches a promo code redeemed past its limit by an agent that does not know it is being farmed.
Banking — payouts, batches, sweeps
Payment initiation, payout batches and treasury sweeps are exactly where an authorised-but-wrong action costs the most and is hardest to reverse. A per-counterparty ceiling bounds the destination; a run budget bounds the job; and the signed receipt chain is the tamper-evident record of what was authorised and why — something a risk function has to produce anyway.
What it will not do
It will not compete on inbound fraud. Card-not-present, account takeover at login, synthetic identity — those are detection problems on traffic that never reaches us, against companies with data we do not have. Anyone telling you an effect gate replaces a fraud engine is selling something.
What would block you today
Two things, and neither is technical: no third-party audit and one operator. For a regulated buyer those are real, and you should hear them here rather than in procurement. The honest sequence is agent platforms and fintech-adjacent first. The full posture.
Blast radius
Assume the agent is already compromised.
This is the design assumption, not the worst case. An agent that can widen its own limits was never limited — so the key a quickstart tells you to paste into an agent holds exactly two permissions: begin an effect, and report its outcome. Everything below is what that agent hits when it tries to do more.
-
PUT /v1/policies/payment.refund { "daily_budget_micros": 999999999 }403Raising its own spend ceiling needs
policies:write. The agent key does not have it, and there is no MCP tool that does. -
POST /v1/circuits/payment.refund/close403Closing the breaker holding it back is an operator action. A console session or an admin key — never the key-only path an agent uses.
-
POST /v1/effects/begin { "dimensions": { "counterparty": "somewhere_new" } }countedA fresh destination is allowed — and immediately gets its own ceiling. A new bucket is not a new allowance: the workspace, key and effect-type limits are untouched by anything the caller declares.
-
POST /v1/effects/begin { }400Declaring nothing is the obvious evasion, so policy can make the dimension mandatory. Omission is
dimension_required, not a free pass. -
retry, retry, retryonceThree attempts at one payment is one payment against a velocity ceiling. And an attempt that crashed mid-flight keeps its count — otherwise killing the agent would be the cheapest way past the limit.
Already built, rarely mentioned
Four controls that do most of the containment.
These are not on the front page and they should be. Each one is a different question about the same agent.
-
"approval_above_micros": 5000000000$5,000A second pair of eyes, above a number. Approval was all-or-nothing per effect type — every refund waits for a human, or none does — which is why operators turn it off. A threshold is the rule they actually have, and beneath it routine work never slows down. It sits under the ceiling that refuses outright, so the policy reads as the sentence you meant: allow, hold, refuse. It measures what the agent declared — so declaring becomes mandatory, and reconciliation checks that number against the vendor's own record.
-
PUT /v1/runs/{run_id}/budget { "limit_micros": 50000000 }$50A wallet for one job. “This task may spend fifty dollars in total” is a sentence nobody could say before. Daily ceilings reset at midnight; a run budget does not reset at all, so a loop cannot wait it out. The agent may read what it has left — that is what lets it choose a cheaper path or stop cleanly — but raising it needs
policies:write, which the agent key does not have. -
"surge_per_hour": 200, "surge_action": "require_approval"heldToo much, rather than too costly. Budgets catch an agent spending more than it should. This catches one doing more than it should — a retry loop sending five thousand emails instead of three, where each one is free. The default action holds the work for a human rather than killing the agent, so nothing irreversible happens and no context is lost.
-
GET /v1/agents/{agent_id}/reliabilityprofileIs this agent behaving like it used to? Reporting rate, decision mix, idempotency-key hygiene, declared-versus-actual cost, how long it holds a lease. A compromised agent behaves differently before anyone notices what it is doing, and deviation from its own baseline needs no payload to see. Operator-only, deliberately: an agent that can read its own grade can work out which signal to stop sending.
Why a wallet, and not a daily limit
A loop cannot wait out a budget that never refills.
The same stuck agent, running for three days, trying to spend far more than either limit allows. On the left it is bounded by a $200 daily ceiling. On the right by a $200 run budget. Both refuse. Only one of them keeps refusing.
The same rule, said plainly
A day is not a task.
A daily ceiling is a promise about calendar time, and a stuck agent has all the calendar time in the world. It waits. At midnight the allowance returns and it spends the same $200 again, and the loop that was supposed to be contained bleeds at exactly the rate the ceiling permits — indefinitely.
A run budget is a promise about one unit of work. It never resets, so there is
nothing to wait for. Set it when you dispatch the job:
PUT /v1/runs/{run_id}/budget, and the gate refuses anything that would
carry that run past the total — before the money moves, which is the only moment
refusing is still cheap.
Lowering a limit below what a run has already spent claws nothing back. The money is
gone and the number does not pretend otherwise; it means nothing further may be spent.
And like every ceiling here, it counts what callers declare in
estimated_cost_micros — it is only ever as good as what they declare.
Both lines above are the arithmetic of those two rules, not a measurement of anything.
Every run, capped or not, is listed in the console — including the ones with no ceiling at all, which are the ones worth looking at.
The one thing that detects
Find the actions that never asked.
A gate only sees the calls that reach it. The old cron job, the leaked key, the script a colleague never mentioned — invisible to us, and invisible to you.
POST /v1/reconcile closes that. Send the idempotency keys or vendor references
for actions your vendor says happened; Ratchet returns which ones it authorised and which
it has never seen. Anything unmatched is a path in your system where a retry can
act twice, and where nobody is watching. That is not a bound on fraud. That is
finding it.
POST /v1/reconcile
{ "idempotency_keys": ["refund:ord_1841", "refund:ord_1842", "…"] }
→ { "authorised": 1841, "unknown": 3 } ← three actions nobody sanctioned
Shape, not volume
Reaching five hundred accounts is not suspicious. Reaching five hundred new ones is.
Fan-out is one job paying many counterparties, and on its own it means nothing. Payroll does it every month and is the healthiest thing in the system. Treating width as guilt would bury you in alerts about your own operations.
What separates payroll from a disbursement into fresh accounts is not how many. It is how many of them are new. Payroll pays the same people; laundering pays accounts nobody has paid before. So novelty is the measure and width is only the floor beneath it — and Ratchet can ask “have I seen this one before?” without ever having been able to read it.
The same people as last month. This is payroll, and nothing is reported.
Same width, no familiar face among them. Reported — as a question, because a first run of anything looks exactly like this too.
Fan-in is the mirror, and it is the one nothing else can see. One counterparty collecting from several separate agents: a refund bot and a payout bot both paying the same account. Each agent is inside every limit it has, because no per-agent ceiling can look across agents. Only something sitting in front of all of them can.
GET /v1/analysis/fan?days=30
→ { "fan_out": [ { "grouping": "run", "id": "disbursement",
"distinct_counterparties": 40, "first_seen": 40, "new_share": 1 } ],
"fan_in": [ { "blinded": "9eb4dace…", "distinct_agents": 3, "effects": 12 } ] }
Same posture as everything else here: a question, not a verdict. A first payroll run is one hundred per cent new counterparties and is indistinguishable from the thing this finds. “Seen before” looks back four windows rather than for ever, so the check does not get slower every day the service runs.
The pattern only we can see
Twenty-three payments of $9,800 against a $10,000 line.
Structuring is keeping amounts just under a threshold so the threshold never fires. Every one of those payments is well-formed, correctly authorised, under every limit, and entirely unremarkable on its own. The pattern is only visible to something that knows what the line is and can see every amount proposed against it.
Your vendor sees the amounts and not your thresholds. Your ledger sees both, afterwards. The gate sees every amount an agent proposed, before it happened, beside the line it was proposed against. That is a strange position to occupy and it is the one Ratchet is in.
No model, and nothing to train. Two adjacent bands of equal width below the line are counted and compared. Real amounts do not crowd the final stretch before a number somebody is trying to stay under — an excess there is the whole signal, and the ratio is reported so you can see its size rather than a score.
GET /v1/analysis/structuring?days=30
→ { "findings": [ { "effect_type": "payment.payout",
"just_below": 23, "control": 2, "excess_ratio": 11.5,
"concentrated_in": [ { "dimension": "counterparty",
"blinded": "9eb4dace…", "count": 14 } ] } ] }
Point it at a line we do not enforce. The threshold that actually gets
hugged is usually not ours — a reporting limit, an internal review limit, a
counterparty's own rule. Set structuring_threshold_micros on a policy and
Ratchet measures against it while refusing nothing. Knowing the amounts is enough; owning
the threshold is not required.
And the honest part: a cap produces this bunching all by itself. Told they may spend up to $10,000, people spend $9,999. What separates that from structuring is intent, which is not visible from here and never will be. So a finding is somewhere to look, and the report says which blinded counterparty it concentrates on — because bunching spread across many destinations is usually a cap, and bunching at one is not.
Evidence
Every decision is signed, and refusals most of all.
A control that cannot prove what it did is a control you have to be believed about. Every decision writes an Ed25519-signed receipt, verifiable offline against a published key without asking us anything. They are hash-chained, so removing one breaks every link after it.
For a risk function that is the useful part: an auditable record of what was authorised, what was refused, and why — produced as a by-product of the control rather than assembled afterwards from logs.
What this does not do
The limits, stated plainly.
It cannot score a transaction
A first-ever payment to a fraudster, for a plausible amount, inside every limit, passes cleanly. Ratchet detects structure and repetition, never intent.
A dimension is a declaration
It can be lied about. Lying moves an effect to a different bucket and grants nothing extra, and reconciliation against the vendor's own record is what exposes it — but nothing here reads a payload to check.
Windows are UTC days
Not rolling. A ceiling can be approached twice across a midnight boundary. Documented rather than hidden, and a rolling window is the fix.
No third-party audit yet
No SOC 2, and Ratchet is operated by one person. For a regulated buyer those are real blockers and we would rather you learn it here than in procurement. The full security posture.
Reconciliation is scheduled, but the fetching is not ours to do. Ratchet has no credentials at your vendors and no way to reach them, so it cannot go and read what really happened. It keeps the calendar instead — it knows when each effect type was last compared, and says so when one is overdue. You still post the vendor's records; it still tells you which it never authorised.
Put a ceiling on it in two calls.
Nothing here needs an integration project. One policy call sets the ceiling; the effect calls your agent already makes start being counted against it.