Approach
Decisions that cost something
Anything can be made to work once. What makes a permission layer worth putting in front of real money is the set of choices that were harder than the alternative, and stayed. These are those choices, with the reasoning intact — including the ones that make the system less convenient on purpose.
01 — Money
Integer cents, the whole way through
There is no floating-point number anywhere in the money path. Not in the request schema, not in the policy comparison, not in the daily total, not in the audit row.
The wire format enforces it: amountCents is validated as a positive integer, and the error string says so in those words. A request carrying 50.00 is a 400 the builder can see, not a value that silently becomes fifty cents or 4999.999999.
This is unglamorous and it is the single easiest way to lose money in software. Currency in floats accumulates error the moment you divide, and a permission layer that compares a drifting number against a limit is a permission layer that eventually lets the wrong thing through — or stops the right one, which erodes trust faster.
The daily cap is per-currency and never converts. Adeia does not fetch exchange rates, because a limit that changes with the market is not a limit you set.
02 — Refusal
A denial is not an error
The request was well formed. The call succeeded. The answer was no. Those are three different facts and most APIs collapse them into one exception.
Adeia returns the refusal as a status with the figure that produced it — amount 500000 exceeds hard maximum 100000 — so an agent can read the reason, say it out loud to whoever asked, and stop. Thrown as an exception it becomes something a retry loop swallows, and the agent tries again, and again, against a rule that will never move.
The same reasoning runs through the SDK, which will not retry on your behalf at all. The idempotency key makes a retry safe; deciding to make one stays with the caller. A library that quietly re-sends a payment request is a library that quietly sends a payment twice.
The ceiling nobody can raise
Past the hard maximum, the action is refused outright. No approval email is sent, because there is no button anywhere that lets a tired human wave it through at 2am. Everything else in the system exists to ask a person; this is the one rule that does not.
03 — Loudness
Fail where somebody is looking
The worst failure this system could have is silent: over-limit actions pausing correctly, and then waiting forever because nothing ever told a human they were asked. From the outside that is indistinguishable from a hung agent.
The server refuses to boot
Without somewhere to send approval mail, it does not start. The credentials are verified with a live connection before the port is opened, so a wrong password fails at deploy time rather than the first time somebody's payment needs a person.
Half-configured never falls back
Set an SMTP user without a password and it refuses, naming the missing one. It does not quietly switch to another transport, because the channel a human is watching is not something to change by accident.
The banner on every boot
NO PAYMENT PROCESSOR ATTACHED, printed every start. A layer that has stopped executing anything looks identical to one that is working — the audit log fills up either way — so which of the two it is never goes unsaid.
Except the audit write
That one is deliberately quiet: it complains on stderr and returns rather than throwing. Losing a record is bad; unwinding an action that already completed because logging failed is worse, and reversing a real transaction over it is worse still.
04 — Limits
What this does not do
Named here so they cannot be mistaken for oversights. Every one is a real boundary of the current system.
-
Nothing settles
No payment processor is attached. The ledger adapter records the payment and stops where settlement would begin. The seam is deliberate and empty rather than filled with a convincing fake.
-
One approver per deployment
A single address, from an environment variable. Per-project approvers are not built, which breaks the moment two teams share a server.
-
One writer
SQLite. No concurrent writers, which means one machine. Running two instances against a shared nothing would let them diverge in silence.
-
Email only
Approvals arrive as mail and nothing else. No Slack, no SMS, no webhook — so the approval path is exactly as reliable as the inbox behind it.
-
No rate limiting
Nothing throttles requests to the action API. The policy bounds what an agent can spend; it does not bound how often it can ask.
-
No key rotation
An API key is issued once by the seed script and stored as a hash. There is no way to rotate one without reseeding.