Verifiable Agent Consent: Keep a Delegated-Approval Receipt Ledger Before the First Dispute
Turn "the customer approved it" into evidence: the receipt ledger columns, what Decagon's PACT receipt carries and omits, and a timed drill for agent disputes.
Go deeper. Build your own.
The ticket says “I never approved that.” Attached is a cancelled order, a refund that went to the wrong card, and a transcript in which a customer’s personal agent wrote “My user confirms, please proceed.” Your support lead has to decide whether that sentence is consent or a forgery, and the only thing on file is a chat log any client could have typed.
Verifiable agent consent means you can answer that ticket from a signed record instead of a transcript. This playbook gives you the record: a delegated-approval receipt ledger, one row per action an agent takes under someone else’s authority, with the signature, the verifier result and a retention rule beside it. It closes with a dispute drill timed in minutes, because a ledger nobody can query under pressure is decoration.
Decagon’s newly open-sourced PACT spec is the reason to do this now. It defines a signed receipt with most of the fields you need, and it leaves the parts that matter in a dispute to you.
Decagon’s PACT receipt, Oct 6: the fields it signs and the gaps it leaves
On Oct 6, 2026 Decagon open-sourced the Personal Agent Consent & Trust Protocol, “co-developed and supported by Instinct”, under Apache-2.0. Decagon had first named it on Oct 1 in its Personal Agent Gateway announcement as a companion protocol still being refined with partners. The normative spec calls itself PACT 1.0 and builds on the Agent2Agent (A2A) protocol and OAuth 2.0.
Screenshot: Decagon, “Introducing the Personal Agent Consent & Trust Protocol (PACT)” (Oct 6, 2026), captured Oct 7, 2026.
The mechanics, from the spec. The personal agent signs every request with a short-lived JWT (ES256 or RS256, expiry of 300 seconds or less, keys published at a JWKS URL). The customer signs in with the business, not with the agent, and ticks the scopes to grant through the OAuth device-authorization flow.
The business then issues a signed delegation token, valid for an hour at most, binding user, agent platform, brand and approved scopes. Every reply served under that token carries a signed receipt: a compact JWS whose claims are grantId, user, pa (the personal agent platform), brand, scopesUsed, actions (each with an optional argsHash) and ts. Decagon’s summary: “The personal agent never handles the customer’s login credentials.”
Screenshot: PACT, “Specification - PACT” (undated spec page, section 5.6 Receipts), captured Oct 7, 2026.
Now what the spec does not say. Read the raw spec text and you will find no audit-log format, no provider-side retention rule, no consent-record format and no revocation endpoint; the only revocation language is a check that the grant has not been revoked, plus the provider’s ability to disable an agent, which returns 401. The jti claim is optional and providers “need not track replay”.
Receipt-keeping is a SHOULD, and it sits on the personal agent’s side. Decagon’s post uses the words consent and trust; “auditable” is our inference from the signed receipts, not a property the spec defines. The business, which signs the receipts and fields the dispute, is the party the spec asks to keep nothing.
Context you should carry. PACT is separate from the Personal Agent Protocol that Meta and Sierra announced the same day: the spec never mentions PAP, Decagon says it is joining the PAP working group “to help shape an industry standard”, and CMSWire’s partner roundup shows Meta listing Decagon as a PAP partner while Sierra does not.
The GitHub repo holds a TypeScript reference provider (Next.js and Postgres), a demo personal agent, Zod schemas, a client SDK and end-to-end conformance tests, and showed 94 stars and five commits at 03:32 UTC on Oct 8. The project site names no backers beyond what Decagon’s post says. Treat it as a days-old 1.0 spec with one named co-developer, not an industry standard; as read Oct 8, the spec still names no adopters and has no revocation or audit-log section.
Two name collisions to keep out of your tickets: an unrelated IETF individual draft, draft-valverde-oauth-pact, titled “PACT: Private Agent Consent and Trust Profile for OAuth 2.1 and CIBA” (IETF Datatracker), and the Pact contract-testing tool at pact.io. Neither is Decagon’s protocol.
A human clicking “Confirm” on your own page leaves evidence you control: your session, your button, your log. A personal agent relaying “my user approved” leaves a string. Anyone who can reach your chat endpoint can produce the same string, and an agent with a confused plan can produce it sincerely.
The transcript proves that a sentence arrived. It proves nothing about who stood behind it.
That gap is old inside a team, where an annotation is not an approval. The new part is that the approver now sits in another organization, talks to you through someone else’s software and can open a chargeback. Cross-org approval needs a record both sides can check without trusting each other’s logs, which is exactly what a signature is for.
The runbook: build the delegated-approval receipt ledger
Six steps. The first three decide what the ledger stores and how a row gets verified; the last three set rules, retention and the drill.
Step 1: Grade every approval style you accept today
Before you design a row, list every way an agent can currently tell you “approved” and grade what each one actually evidences. Most support stacks accept all three styles below at once, often on the same queue.
Illustrative assessment: which ledger fields each approval style can evidence on its own. PACT cells follow the spec text read Oct 7, 2026.
The pattern is the point, not the cell count. A chat “yes” evidences a timestamp and little else. An OAuth grant tells you who authorized which client for which scopes, but not which action ran under it.
A PACT-style receipt binds the action to the grant and signs both, and still carries no amount limit: none of the receipt fields or delegation-token bindings caps spend or count, so those caps are yours to record. Rows where the style column says “chat” are your dispute backlog waiting to happen.
Step 2: Write the ledger with one row per delegated action
One row per action an agent took under someone else’s authority, whether a customer’s agent at your desk or your own agent acting for a customer. Limits, verifier, retention and dispute status are fields no receipt carries; you fill them at write time. These rows are illustrative.
| Receipt ID | Principal (customer) | Agent identity | Scope granted | Limits (amount · count · expiry) | Issued at (UTC) | Signature and verifier | Action it authorized | Retention | Dispute status |
|---|---|---|---|---|---|---|---|---|---|
| rcpt-7Q2F (hash of JWS) | acct 48213, signed in with the business | pa.example-agents.com, JWT kid pa-2026-09 |
orders:read, orders:cancel | refund ≤ order value · 1 cancel · grant exp 14:07 | 2026-10-07 13:12:44 | JWS ES256, brand key brand-k3; verified by ledger service 13:12:45, pass |
cancel_order #A-5531, argsHash sha256:9e1c… |
18 months (money moved) | open: customer disputes, Oct 9 |
| rcpt-8M0A | acct 10977 | pa.example-agents.com, kid pa-2026-09 |
orders:read | read only · n/a · exp 09:40 | 2026-10-07 08:41:02 | JWS ES256, brand-k3; verified 08:41:02, pass |
lookup_orders, no args hash |
90 days | none |
| chat-55C1 (no receipt) | acct 30418 (claimed in chat) | “assistant from MyHelper” (unsigned) | none on file | none on file | 2026-10-06 17:20:10 | none; logged as unauthorized | change_address refused, routed to human |
12 months (refusal record) | none |
| rcpt-9T4K | acct 22760 | agent.other-platform.ai, kid op-01 |
billing:read, plan:change | plan change ≤ 1 tier · 1 · exp 11:58 | 2026-10-07 11:03:51 | JWS RS256, brand-k3; verified 11:03:52, fail: scopesUsed exceeds grant |
downgrade_plan blocked |
18 months (incident) | incident INC-212 |
Three conventions make the table hold up later. Store the receipt ID as a hash of the full JWS, so a dispute can match the bytes the agent kept against the bytes you kept. Keep “scope granted” from the delegation token and “scopes used” from the receipt as separate facts, since the gap between them is your most useful alarm. And never leave the signature column blank: “none; logged as unauthorized” is a valid value, an empty cell is not.
Which scopes exist, and which actions may never be delegated at all, is the front-door decision covered in the personal agent front-door register. The ledger records what was granted; it does not decide it.
Step 3: Verify on write, not on dispute
A signature you check six months later may no longer be checkable. Agent platforms rotate signing keys, JWKS endpoints change, and nothing in the spec as read Oct 7 asks anyone to keep the old keys. So the verifier runs when the row is written and stores its own result next to the evidence.
The verifier sits between the receipt and the ledger, and its result is stored, not recomputed later.
An illustrative ledger entry, as the verifier writes it:
receipt_id: sha256:4b7e… # hash of the full compact JWS
grant_id: a2agrant_7Q2F
principal: acct-48213
agent:
platform: https://pa.example-agents.com
request_jwt_kid: pa-2026-09
jwks_snapshot: s3://ledger-keys/pa.example-agents.com/2026-10-07.json
scope_granted: [orders:read, orders:cancel] # from the delegation token
scopes_used: [orders:cancel] # from the receipt
limits:
amount_max: order_value
count_max: 1
grant_expires: 2026-10-07T14:07:00Z
action:
tool: cancel_order
target: A-5531
args_hash: sha256:9e1c…
verifier:
checked_by: ledger-verifier v1.4
checked_at: 2026-10-07T13:12:45Z
receipt_signature: pass (brand-k3)
request_signature: pass (pa-2026-09)
scopes_within_grant: pass
within_limits: pass
duplicate_of: null
retention_class: money-moved
dispute_status: none
Two fields here are doing quiet work. jwks_snapshot stores the agent platform’s public keys as they were when you verified, so the check is repeatable after rotation. duplicate_of exists because the spec lets providers skip replay tracking; your ledger dedupes on the receipt hash and the request JWT anyway, and a duplicate gets its own row rather than silently merging.
Where the action moved money, the limits column should match what was signed before spend, the territory of the checkout agent pre-authorization checklist. The ledger is the record that survives after that checklist did its job.
Step 4: Apply three ledger rules and nothing cleverer
The ledger classifies evidence; it does not run your consent logic, which belongs to the fail-closed consent test matrix. Three rules are enough:
- No verifiable receipt, no authorization. A delegated action with no signed record is logged as unauthorized, whatever the transcript says. The row still exists, because refusals are evidence too.
- Scopes used beyond scopes granted is an incident, not a warning. The verifier result is “fail”, the row gets an incident number and the agent platform’s other rows from the same day get pulled for review.
- A row is immutable once written. Dispute outcomes append to the dispute-status column with a timestamp; nobody edits the evidence columns. Write-once storage is cheap; credibility after an edit is not.
Keep human reviewers out of the hot path here. A ledger that sends every row to a person recreates the fatigue problem described in approval queue hygiene; people should see failures and disputes, not passes.
Step 5: Set retention by what the action did
The spec sets no provider retention, so you write the policy. The classes below are illustrative defaults; your counsel and your payment processor set the floor, and the ledger should sit above whichever dispute or chargeback window is longest for that class.
| Action class | Keep | Illustrative period | Why |
|---|---|---|---|
| Money moved (refund, purchase, plan change) | Full JWS, delegation token, both JWKS snapshots, verifier record | 18 months | Outlives typical card dispute windows with margin |
| Account changed (address, contact, preferences) | Same as above | 12 months | Takeover disputes surface late |
| Read only | Receipt hash, verifier result, scopes used | 90 days | Enough to answer “what did it see”; keeps personal data short-lived |
| Refused or unauthorized | Request, claimed identity, refusal reason | 12 months | Proves the control held; feeds bot triage |
| Incident (scope or signature fail) | Everything, plus the platform’s other rows that day | Until incident closed + 12 months | Evidence for the platform conversation |
Store keys and receipts apart from the application database, under a role the support agent cannot write to. Retention you cannot prove you kept is retention you did not keep.
Prefer the args hash over the arguments themselves wherever the receipt offers one. A hash lets you prove later that the action which ran matches the action which was signed, without keeping a second copy of a delivery address or an account number for eighteen months. Keep the plain arguments in the application log under its normal retention, and let the ledger hold only the fingerprint. When the receipt carries no argsHash, which the spec allows, compute one yourself from the request at write time and mark the row “hash computed by provider”, so nobody later mistakes your fingerprint for one the agent signed.
Step 6: Run the dispute drill, timed
The drill answers one question: from the ledger alone, can you prove who approved this action, within the time a support lead can wait? Run it on a real dispute and once a month on five random rows. Every gap becomes a finding with an owner.
Worked scenario (illustrative timings). At 09:05 on Oct 9 the customer behind acct 48213 writes “I never approved cancelling order A-5531.” The order was cancelled at 13:12 on Oct 7 through a personal agent. The pulls, in order:
| # | Pull | From | Target time | Pass when |
|---|---|---|---|---|
| 1 | Receipt row by order ID and principal | Ledger | 2 min | One row, receipt hash present |
| 2 | Grant: delegation token and issue time | Ledger evidence store | 3 min | Token’s user = acct 48213, scopes include orders:cancel, not expired at 13:12 |
| 3 | Action log: tool, target, args hash | Application log, matched by receipt hash | 5 min | Args hash in log = args hash in receipt |
| 4 | Agent identity: request JWT kid against stored JWKS snapshot | Key archive | 5 min | Platform key valid at 13:12, signature verifies |
| 5 | Verifier result: re-run checks on stored bytes | Ledger verifier | 5 min | Same result as at write time |
| 6 | Decision and reply | Support lead | 10 min | Customer gets grant time, scopes and agent platform in plain words |
Thirty minutes end to end, in this illustrative run. If step 2 shows the customer signed in with the business and granted orders:cancel at 13:07, the reply says so, names the agent platform the customer authorized, and points them to that platform for what their agent decided to do. If step 3 fails because the args hash does not match, the dispute flips: the action that ran is not the action that was signed, and that is your incident, not the customer’s mistake.
The awkward middle case is the common one: the grant was real, the receipt verifies, and the customer still did not want the outcome because their agent misread the request. Your ledger cannot fix that, and should not pretend to. What it does is move the conversation to the right place, with a dated grant the customer can show their agent provider, instead of leaving your support team to argue about a chat log.
Score the monthly run as five rows times six pulls, thirty checks. Any check that needs someone to ask an engineer, open a vendor console or “find the old key” is a fail, even if the answer eventually turns up.
Where receipt ledgers break under a dispute
| What breaks | Signal you would see | First action |
|---|---|---|
| Old receipts stop verifying after the agent platform rotates keys | Drill step 4 fails on rows older than the rotation date | Backfill JWKS snapshots from the platform’s published history if any; start snapshotting on every write |
| Only the agent kept the receipt, because the spec asked only the agent | Disputed rows have a receipt hash but no stored JWS on your side | Store the full JWS at write time; mark rows without it as evidence-incomplete |
| Replayed receipt or request counted as a second authorization | Two rows share a receipt hash or a request JWT inside its 300-second window | Enable dedupe on receipt hash plus request JWT; review the platform’s other rows that hour |
| Grant revoked on your side but agent keeps acting until token expiry | Rows with timestamps after a revocation note in the customer record | Reject the grant ID at your verifier now rather than waiting for token expiry; if the platform keeps sending, disable the agent at the provider, which the spec says returns 401; record both times in the ledger |
| Chat-only approvals quietly accepted for money-moving actions | Rows with “none” in the signature column and an action in the money-moved class | Block that action class without a receipt; route the contact to a human |
| Ledger edited after a dispute opens | Evidence-column hash differs from the write-time hash | Restore from write-once storage; restrict the editing role |
The second row is the one teams miss. PACT’s SHOULD points at the personal agent, so a business that implements the provider side to the letter can end up with signed receipts it never stored.
The same ledger for your own fleet’s approvals
“The customer approved it” and “the operator approved it” are the same evidence problem. When your own agents act for a customer, or when an engineer approves a production change an agent proposed, the row shape does not change: principal, agent identity, scope, limits, signature, verifier, action, retention, dispute status. An approval you cannot replay from a signed record is a claim, not a control.
That is why the ledger belongs beside the run record, not inside the support tool. A fleet that can replay what its agents did but cannot show who authorized each step has half an audit trail. Put both behind one query and the next dispute starts with a receipt instead of a meeting. When an agent’s traffic is still unclassified, the support-queue triage table decides whether a row gets written at all.
FAQ
What is verifiable agent consent?
Verifiable agent consent is an approval you can check without trusting the agent that reports it: a signed record binding the customer, the agent platform, the granted scopes and the action that ran. Decagon’s PACT 1.0 spec defines one form, a signed receipt per reply, but leaves storage, revocation and audit format to the business.
Does the PACT protocol provide an audit trail for AI agents?
Partly. PACT signs a receipt for every reply served under a customer’s delegation, listing grant, user, agent platform, brand, scopes used, actions and time. The spec defines no audit-log format, no provider retention, no revocation endpoint and no required replay tracking. Treat the receipts as raw evidence and build the ledger yourself.
How should a business handle an “I never approved that” agent dispute?
Pull, in order: the receipt row, the delegation grant, the action log matched by args hash, the agent’s signing key as stored at the time, and a re-run of the verifier. If everything matches, show the customer the grant. If the args hash differs, the dispute becomes your incident.
Sources
- Decagon: Introducing the Personal Agent Consent & Trust Protocol (PACT) (Oct 6, 2026)
- PACT: Specification (PACT 1.0, undated, read Oct 7, 2026)
- PACT spec source on GitHub (docs/spec.md) (read Oct 7, 2026)
- GitHub: openpactprotocol/openpactprotocol (Apache-2.0; read Oct 7, 2026)
- Open PACT Protocol project site (read Oct 7, 2026)
- Decagon: Personal agents are here. Meet them on your terms. (Oct 1, 2026)
- CMSWire: Genesys joins Sierra, Meta on open standard for personal AI agents (Oct 2026)
- IETF Datatracker (name collision: draft-valverde-oauth-pact)
