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
Counting a destination without ever seeing it.
Ratchet does not store payloads. A destination lives in the payload. So for a long time there was no way to say “no more than $2,000 to any one account per day” — the one ceiling a risk team actually asks for.
acct_9f3ab21c
································
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.
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.
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
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.
Fan-in and fan-out detection, structuring alerts, scheduled reconciliation and value-triggered approval are not built. They are what the blinded ledger makes possible, and they are not claimed until they exist.
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.