Mandate freshness gate: a valid payment signature proves authorization at issue time, not that the authority is still live when the agent spends. mandate_freshness_gate.py replays a recorded mandate against a recorded execution, offline, and blocks when authority was revoked, expired, over a lowered limit, or out of scope at execution. The signature stays valid throughout.
AI disclosure: I wrote
mandate_freshness_gate.pywith an AI assistant and ran it myself, offline, on Python 3.13.5, standard library only, no network. Every number in the output blocks below is pasted from a real local run. I checked the exit codes (0 / 1 / 2), ran each scenario twice to confirm STDOUT is byte-for-byte identical, and had the tool print a sha256 of its own report so you can reproduce the exact bytes. The two developers I cite and the paper one of them references are other people's work, attributed inline, and I keep their numbers out of my fixtures.
In short:
- A valid signature is a fact about issue time. Whether the mandate is still live is a fact about execution time. A conformance test that replays the mandate and verifies the signature, but never checks revocation, expiry, or the current limit at execution, will wave through a payment the user already cancelled.
- The gate replays a recorded mandate plus a recorded execution as data and reconciles authority liveness at the execution timestamp. When the mandate was revoked before execution, it blocks with
revoked-before-execution. - The demo that matters: two mandate files identical in every byte except one field. Set
revoked_atfromnullto a timestamp three seconds before execution, and the verdict flips from LIVE exit 0 to BLOCK exit 1. - It is revocation-dominant and fail-closed. It does not verify crypto, it does not phone a revocation server, and unusable input exits 2 rather than passing silently.
- This is a post-hoc replay of what you recorded. It is the last check before an agent spends on stale authority, not a live network interceptor.
How does a valid signature authorize a revoked mandate?
The signature and the authority live on different clocks. When the mandate is signed, the authority says yes: this agent may pay this recipient, up to this limit, until this expiry. That yes gets frozen into a signature. From that instant the signature is a photograph. It records that permission existed at issue time, and it keeps recording it forever, because a signature cannot un-sign itself.
Authority does not sit still like that. A user revokes consent. A budget owner lowers the daily limit. The mandate reaches its expiry. A recipient gets pulled from the allowlist. None of those actions travel backward into the signature. The photograph still shows a smiling yes, taken at 09:00, while the authority behind it walked out at 14:29.
Now the agent executes at 14:30. If your check verifies the signature and stops there, it verifies the photograph. The photograph is genuine. So the check passes, and the payment leaves, three seconds after the user pulled the plug. The failure is not a forged signature or a broken cipher. Everything cryptographic held. What was missing is the second question: is the authority in the photograph still standing at execution time? That question has no signature to lean on. Someone has to ask it against the state of the world at the moment of the spend, and that someone is this gate.
Tracking is not control
A valid signature tracks that the mandate was authorized. It does not control whether that authorization is still in force when the money moves. This is the whole thesis of the pre-execution and reconciliation gates I keep building: tracking a fact is not the same as controlling the thing the fact describes. You tracked consent. Consent was granted. Consent was then withdrawn, and nobody re-read the withdrawal at execution.
Here is the claim, stated so you can break it. Take two mandate files that are byte-identical except for the revoked_at field. In the first it is null. In the second it holds a timestamp three seconds before the execution timestamp, and everything else, signature, amount, recipient, limit, scope, currency, stays the same and stays inside every bound. The gate must exit 0 on the first and exit non-zero with reason revoked-before-execution on the second. One field has to flip the verdict. If you can show me a revoked mandate that this gate lets pass, or a genuinely live mandate it blocks, the tool is broken and this post goes down with it.
I lean on revocation for the headline case on purpose. Borrowing a phrase from a developer publishing as mspro3210, the gate is revocation-dominant: a revoked mandate blocks regardless of how fresh and well-formed the payment looks. More on his framing at the end. The point for now is that revocation wins ties. A signature can be immaculate and the recipient can be perfect and the amount can be a penny, and if the authority was withdrawn before execution, the answer is no.
What does the mandate freshness gate reconcile?
Two things, per file. A recorded mandate: when it was issued, when it expires, whether and when it was revoked, its scope of allowed recipients and currency, its limit, and any recorded changes to that limit over time. And a recorded execution: the timestamp, the amount, the recipient, the currency. The gate lines them up on one axis, time, and asks whether the authority was alive at the execution timestamp and whether the spend stayed inside the bounds that were in force at that instant.
The limit needs a word, because a limit is not a constant. A mandate can carry a limit_adjustments timeline: the daily cap was 500 at issue, lowered to 100 at noon. The gate computes the effective limit at execution, which is the latest adjustment dated at or before the execution timestamp. So a 120 USD payment that was fine against a 500 limit at issue is over the 100 limit that was actually in force when it ran. Same amount, same signature, different answer, because the authority moved and the amount did not.
One design choice worth calling out: the gate collects every reason that fired, it does not bail on the first. A mandate that is both expired and out of scope reports both. The headline reason drives the exit code, the full list drives the forensics.
Here are the states it can reach:
| Verdict | Condition at execution time | reason-code | exit |
|---|---|---|---|
| LIVE | signature asserted, issued <= exec < expiry, not revoked, amount <= effective_limit, recipient in scope, currency matches |
mandate-live |
0 |
| BLOCK | revoked_at <= exec |
revoked-before-execution |
1 |
| BLOCK | expires_at <= exec |
expired-before-execution |
1 |
| BLOCK |
amount > effective_limit(exec), including a limit lowered after issue |
limit-exceeded-at-execution |
1 |
| BLOCK | recipient not in scope, or currency mismatch |
recipient-out-of-scope / currency-mismatch
|
1 |
| BLOCK |
exec < issued (replay or clock anomaly) |
execution-precedes-issue |
1 |
| ERROR | missing field, unparseable date, malformed input | bad-input |
2 |
The revocation check is a few lines, simplified here for the post (the real evaluate() builds a display record and carries the currency and ordering guards shown in the table):
def evaluate(mandate, execution): # simplified for the post
issued = parse_ts(mandate["issued_at"])
expires = parse_ts(mandate["expires_at"])
revoked = parse_ts(mandate["revoked_at"]) if mandate.get("revoked_at") else None
exec_at = parse_ts(execution["exec_at"])
limit, lowered_at = effective_limit(mandate, exec_at) # honors post-issue limit_adjustments
reasons = []
# signature_valid is TRUSTED as asserted input; the gate never verifies crypto.
# Signature validity and authority liveness are two different facts.
if revoked is not None and revoked <= exec_at:
gap = int((exec_at - revoked).total_seconds())
reasons.append(("revoked-before-execution",
f"signature valid, authority not: revoked {fmt(revoked)}, "
f"{gap}s before execution {fmt(exec_at)}"))
if expires <= exec_at:
reasons.append(("expired-before-execution", ...))
if amount > limit:
reasons.append(("limit-exceeded-at-execution", ...))
if recipient not in scope:
reasons.append(("recipient-out-of-scope", ...))
return ("BLOCK", reasons) if reasons else ("LIVE", [])
Notice what is absent. There is no call to verify_signature. The gate trusts the signature_valid boolean as an asserted input and moves on, because recomputing a signature is a separate, orthogonal job. The gate exists to make the point that a true signature_valid and a live authority are not the same claim.
How is this different from checking the transaction itself?
Fair question, because I have built neighbors that look adjacent. Two of them deserve the borders drawn explicitly.
The first is the canary that sanity-checks an on-chain transaction before it is sent. That tool asks whether this transaction is normal: does the target address exist, is the price sane against a reference, will the calldata revert. It reaches the network to answer. This gate asks a question the canary cannot: is the mandate behind the transaction still alive at all? A transaction can be flawless in every on-chain sense, a real address, a fair price, a clean simulation, and still ride a consent the user cancelled three seconds ago. The canary would wave it through. This gate catches exactly that, and it does it offline, without touching a chain.
The second is the trace that compares an agent's allowed tools against what it actually did. That one works on a static scope: here is the allowlist, here is the telemetry, flag the mismatch. This gate is about time, not scope. In the revocation case the recipient stays inside scope the entire time. The action was permitted at issue and the signature proves it. What changed is the calendar: the authority behind the permission was withdrawn before the clock reached execution. Scope did not move. The mandate's lifecycle did. That axis, freshness of authority at execution, is what neither of those tools measures, and it is the one this gate is built around.
None of this is a new idea about authorization lifecycles. OAuth has token revocation, payment standards have mandate expiry and strong-customer-authentication windows, and lifecycle checks are old. What is worth a small tool is the specific shape: a portable, offline gate for agent payments that reconciles a recorded mandate against a recorded execution and blocks on stale authority, with one field isolated so the failure is reproducible. I am not reinventing revocation. I am making it fail a test.
Quick start
Feed it a JSON manifest with a mandate object and an execution object. Then run it:
python3 mandate_freshness_gate.py fixtures/fixture_live.json
No install, no key, no network. It reads the file, prints a verdict per mandate, and returns an exit code you can wire into CI over the mandate and execution records you already keep. In production you would feed it a live snapshot of the revocation status. Here it reconciles the timeline you recorded, so the honesty of the input is your job; the gate reconciles what you wrote down, it does not go fetch ground truth.
The one field that flips the verdict
Here are two files. fixture_live.json is a signed mandate and an execution that sits inside every bound. fixture_revoked.json is the same file with one change: the user revoked consent three seconds before execution. The signature, amount, recipient, limit, and scope are byte-identical between them. The only difference is one line:
$ diff fixtures/fixture_live.json fixtures/fixture_revoked.json
6c6
< "revoked_at": null,
---
> "revoked_at": "2026-07-11T14:29:57Z",
Run the live one:
mandate-freshness-gate: authority live at execution?
files: 1
[1] fixtures/fixture_live.json
mandate : mnd-7f3a91 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked never
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 500.00 USD at exec; scope ok; signature asserted
verdict : LIVE (mandate-live) exit 0
detail : authority live at execution: issued <= exec < expiry, not revoked, 120.00 <= 500.00 limit
summary: 1 LIVE, 0 BLOCK, 0 ERROR -> overall exit 0
report-sha256: 615b7a753f9b5c1b0071578445e0d543f89d0136c35badcb7c14d55ff29b89a4
Now run the revoked one:
mandate-freshness-gate: authority live at execution?
files: 1
[1] fixtures/fixture_revoked.json
mandate : mnd-7f3a91 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked 2026-07-11T14:29:57Z
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 500.00 USD at exec; scope ok; signature asserted
verdict : BLOCK (revoked-before-execution) exit 1
reason : revoked-before-execution: signature valid, authority not: revoked 2026-07-11T14:29:57Z, 3s before execution 2026-07-11T14:30:00Z
summary: 0 LIVE, 1 BLOCK, 0 ERROR -> overall exit 1
report-sha256: b0923070ee0cbcf8863d42a3d4221c8790b37e000ccf0c16d401c06b977890b1
Same signature, same 120.00 USD, same recipient, same 500.00 limit. In one file the authority is live and the payment ships. In the other the authority was withdrawn at 14:29:57, three seconds before the 14:30:00 execution, and the same payment is blocked. The exit code goes from 0 to 1 on the strength of one edited field. The gate spells the gap out in its own words: signature valid, authority not.
In production nobody edits that field by hand. The user taps cancel in an app, a budget owner flips a switch, a fraud rule trips, and the revocation lands at 14:29:57 while the agent's payment is already in flight for 14:30:00. Your signature check sees a valid signature and says go. This gate sees a revoked authority and says stop. The two sha256 lines are the tool hashing its own report body, so you can confirm you got the same bytes I did.
Six mandates, one sweep
Revocation is the headline, but it is one of several ways an authority goes stale between issue and execution. Hand the gate a batch and it reconciles each file. This run (mandate_freshness_gate.py fixtures/*.json, so the files arrive in alphabetical order) covers the full set:
mandate-freshness-gate: authority live at execution?
files: 6
[1] fixtures/fixture_bad_input.json
verdict : ERROR (bad-input) exit 2
detail : Invalid isoformat string: 'not-a-real-timestamp'
[2] fixtures/fixture_expired.json
mandate : mnd-2c4b08 issued 2026-07-01T09:00:00Z, expires 2026-07-10T09:00:00Z, revoked never
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 500.00 USD at exec; scope ok; signature asserted
verdict : BLOCK (expired-before-execution) exit 1
reason : expired-before-execution: mandate expired 2026-07-10T09:00:00Z at or before execution 2026-07-11T14:30:00Z
[3] fixtures/fixture_live.json
mandate : mnd-7f3a91 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked never
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 500.00 USD at exec; scope ok; signature asserted
verdict : LIVE (mandate-live) exit 0
detail : authority live at execution: issued <= exec < expiry, not revoked, 120.00 <= 500.00 limit
[4] fixtures/fixture_lowered_limit.json
mandate : mnd-9d51e7 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked never
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 100.00 USD at exec; scope ok; signature asserted
verdict : BLOCK (limit-exceeded-at-execution) exit 1
reason : limit-exceeded-at-execution: amount 120.00 > effective limit 100.00 at execution (limit lowered to 100.00 at 2026-07-11T12:00:00Z after issue)
[5] fixtures/fixture_out_of_scope.json
mandate : mnd-4a80f2 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked never
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:unknown-wallet-9931
authority : effective_limit 500.00 USD at exec; scope MISS; signature asserted
verdict : BLOCK (recipient-out-of-scope) exit 1
reason : recipient-out-of-scope: recipient 'merchant:unknown-wallet-9931' not in mandate scope ['merchant:acme-cloud-eu']
[6] fixtures/fixture_revoked.json
mandate : mnd-7f3a91 issued 2026-07-11T09:00:00Z, expires 2026-07-18T09:00:00Z, revoked 2026-07-11T14:29:57Z
execution : 2026-07-11T14:30:00Z, 120.00 USD -> merchant:acme-cloud-eu
authority : effective_limit 500.00 USD at exec; scope ok; signature asserted
verdict : BLOCK (revoked-before-execution) exit 1
reason : revoked-before-execution: signature valid, authority not: revoked 2026-07-11T14:29:57Z, 3s before execution 2026-07-11T14:30:00Z
summary: 1 LIVE, 4 BLOCK, 1 ERROR -> overall exit 1
report-sha256: 56cebaac439bbc0dbeaf027a6cf9ddf97800a599b3dccdfef715c2bbbb19157c
Read the four blocks. The expired mandate ran a day after its 2026-07-10 expiry. The lowered-limit mandate is the quiet one: at issue its cap was 500 and the 120.00 payment was fine; the cap was lowered to 100.00 at noon, so at 14:30 the same payment is over the limit that was actually in force. The out-of-scope mandate points at a wallet that was never on the allowlist. And the revoked mandate is the killer from the previous section, sitting in the same batch. The summary is 1 LIVE, 4 BLOCK, 1 ERROR, and the process exits 1, because one confirmed BLOCK outranks the bad-input file. That ordering is deliberate: a real block should not be masked by a parse error somewhere else in the batch.
The report ends with a sha256 of its own body, and because that body depends on the order you pass the files, fixtures/*.json reproduces exactly the digest above.
That bad-input file is not an accident either. The gate fails closed. No arguments prints a usage line and exits 2. A missing file reports file not found and exits 2. A mandate with a timestamp like 'not-a-real-timestamp' exits 2 with the parser's own message rather than guessing. An input the gate cannot trust never leaves as a green pass, so a malformed mandate cannot slip a payment through on the back of an exception the gate swallowed.
What this is NOT
I would rather draw the borders myself than have you find them the hard way.
It does not verify the cryptographic signature. The signature_valid field is an asserted input the gate trusts. It is an honest stub. Recomputing a signature is a real job, it is just a different one, and folding it in would blur the single point of this tool: signature validity and authority liveness are separate facts. If your pipeline needs the crypto verified, verify it upstream and hand this gate the result.
It does not intercept live traffic. This is an offline replay of records you already have: a recorded mandate and a recorded execution. It does not call a chain, a payment gateway, or a revocation server. In production the honest design is to feed it a live snapshot of revocation status at execution. An offline replay proves the logic and fails a test in CI. It is not a runtime firewall on the wire.
Freshness here means revocation, expiry, limit, and scope, checked against your recorded data. It is not the authority's full policy engine. If your authority can deny a payment for reasons outside those fields, velocity rules, risk scores, jurisdiction, this gate does not model them. It checks the lifecycle facts you recorded, at the execution timestamp, and nothing more.
It is not an AP2 or ERC-8183 conformance suite. It checks one property, freshness of authority at execution, not an entire standard. Treat it as one assertion you can drop into a larger suite, not the suite.
It does not stop prompt injection in the judgment layer. By the time a mandate reaches this gate, the intent has already been formed, possibly by an agent that was manipulated into forming it. The gate is a deterministic check on the lifecycle of the authority behind that intent. It is the last line on stale authority, sitting after the layer where judgment happens, not a defense of that layer. If the agent was talked into signing a bad mandate in the first place, a live and in-scope mandate will pass here exactly as designed.
Garbage in, garbage out. The gate reconciles the timeline you recorded. Feed it a wrong revoked_at or a stale limit and it will trust your record, because it has no ground truth to consult. It reconciles what you wrote down. It does not go find out whether what you wrote down is true.
The recorded mandate is untrusted data. A fixture can carry any string, including a field crafted to look like an instruction to a tool reading the log. The gate reads every value as data and executes none of it. An injected instruction sitting in a recipient name or a note is counted as a field value and nothing else. Treating records as data and never as commands is not optional when the records can come off the open internet.
Why this, why now
Two developers put this problem in front of me the same week, both on Dev.to, both on July 9. I read both posts, checked the tools were live, and built my own fixtures rather than reuse anyone's numbers.
The first, publishing as mspro3210, wrote a piece called "The receipt cannot be written by the pen it is checking: separation of duties for agent payments". His formulation is the one I borrowed above. He argues the real binding is not "the mandate authorizes execution" but "the mandate authorizes execution, and the mandate is still live at execution time," and he calls the property revocation-dominant: a revoked mandate blocks regardless of how fresh the intent looks. That is his framing and his phrase. My contribution is to turn it into a runnable gate with the revocation case isolated to a single field.
The second, publishing as barissozen, was writing about escrow and judgment layers for agent trades, and he pointed at a paper I have not read myself: arXiv 2601.22569, "Whispers of Wealth." Per barissozen, and I am relaying his summary rather than an independent read, the paper describes an agent operating under Google's Agent Payments Protocol (AP2) that got subverted, and the part that failed was not the cryptography. The signatures held. What folded was the layer that exercises judgment. He contrasts that with schemes like ERC-8183, escrow with an evaluator, where a judge sits between intent and settlement. I have not verified the paper's claims or numbers, so none of them are in my fixtures, and you should read barissozen and the paper before quoting either.
Both of those point at the same seam from opposite sides. Cryptography can be flawless and the money can still move wrongly, because the fact a signature carries (authorized at issue) is not the fact you need at execution (authority still live). Agent payments are shipping into the real world right now, which is why the seam matters this week and not in the abstract. This gate does not fix the judgment layer either developer worries about. It handles the smaller, harder-edged case downstream of it: once the intent exists, is the authority behind it still standing when the agent spends? That question deserves a check that fails a test, and now it has one.
I publish one of these small offline gates most weeks, each a runnable tool with the raw output and the exit codes, no keys and no network. Follow along if you want the next one. And here is the question I genuinely cannot answer cleanly, so I am putting it to you: in production, execution happens at 14:30:00 and the revocation landed at 14:29:57, but your revocation snapshot has propagation lag of its own. How do you get a live view of authority status that is fresh enough to catch a cancellation that arrived three seconds ago, without blocking every legitimate payment behind a slow consistency check? I read every comment.
Top comments (0)