docs(ap2): add payment evidence frame specification#274
Conversation
Adds docs/ap2/payment_evidence_frame.md specifying the Payment Evidence Frame (PEF) -- a transport-agnostic envelope that wraps any AP2 payment lifecycle receipt under a named claim_type, a deterministic frame_id, and an optional RFC 9421 signature. Covers: claim_type taxonomy (5 receipt formats), frame field definitions, frame_id and receipt_hash derivation, frame_id stability guarantee, usage in AP2 agent flows, live production example, and reference implementations. Normative reference: draft-hopley-x402-payment-evidence-frame https://datatracker.ietf.org/doc/draft-hopley-x402-payment-evidence-frame/ Also adds pef, PEF, deserialising, multiparty, unrecognised to .cspell/custom-words.txt.
There was a problem hiding this comment.
Code Review
This pull request introduces a new specification document for the Payment Evidence Frame (PEF), which acts as a transport-agnostic envelope wrapping AP2 payment lifecycle receipts, and updates the custom dictionary. The review feedback highlights important security and clarity enhancements for the specification: explicitly requiring consumers to verify that the computed frame_id matches the requested ID, strengthening signature verification rules to mandate rejection of invalid signatures, and clarifying how RFC 9421 HTTP Message Signatures are formatted and applied to standalone JSON objects.
| - `frame_id` stability is predicated on deterministic JCS canonicalisation. | ||
| Consumers MUST use a conforming RFC 8785 implementation when re-deriving | ||
| `frame_id` for verification. |
There was a problem hiding this comment.
When a consumer requests or references a frame by its frame_id (as described in the AP2 flows), they must verify that the computed frame_id of the received frame matches the requested/expected frame_id. It would be beneficial to explicitly state this requirement in the Security Considerations to prevent content substitution attacks by a malicious or compromised provider.
| - The `frame_provider_did` field is informational within the unsigned frame. | ||
| If the `signature` field is present, consumers SHOULD verify it before | ||
| treating `frame_provider_did` as authoritative. |
There was a problem hiding this comment.
If a signature is present on the frame but is invalid, the frame should be rejected. Specifying that consumers MUST verify the signature if present, and MUST reject the frame if verification fails, provides stronger security guarantees than just saying they 'SHOULD verify it before treating frame_provider_did as authoritative'.
| | `frame_provider_did` | string | DID URI identifying the party that constructed the frame. | | ||
| | `frame_timestamp_ms` | integer | Unix epoch milliseconds at which the frame was constructed. | | ||
| | `canon_version` | string | In-band canonicalisation pin. Fixed `urn:x402:canonicalisation:jcs-rfc8785-v1` for this version. | | ||
| | `signature` | string | OPTIONAL. RFC 9421 HTTP Message Signature string over the frame. Adding `signature` to an existing frame does NOT change `frame_id`. | |
There was a problem hiding this comment.
Since RFC 9421 (HTTP Message Signatures) is designed for HTTP messages, applying it to a standalone JSON object requires defining how the Signature Base is constructed and how signature metadata (such as the signature parameters/input) is represented. It would be highly beneficial to clarify the expected format of the signature string (e.g., whether it contains the serialized Signature and Signature-Input parameters, or if it follows a specific profile) to ensure interoperability across different implementations.
Fix 22 pre-existing Biome TypeScript lint violations across 6 files: - noExplicitAny: replace (import.meta as any) with typed cast in App.tsx - noGlobalIsNan: isNaN → Number.isNaN in productPreviewUnavailable.ts - noNonNullAssertion: current! → current?.toFixed(2) in MandateApproval.tsx - noSvgWithoutTitle: add <title> elements to all 5 inline SVGs (MandateApproval.tsx ×4, InventoryOptionsCard.tsx ×2) - useButtonType: add type="button" to all 7 button elements missing it (App.tsx ×3, MandateApproval.tsx ×2, InventoryOptionsCard.tsx ×1, MonitoringCard.tsx ×1) - useExhaustiveDependencies: destructure messages from chatState so useEffect dep array references a stable value - useOptionalChain: proseText && proseText.trim() → proseText?.trim() in MessageRenderer.tsx (×2) - useSemanticElements: div[role=button] → <button> in InventoryOptionsCard.tsx (onKeyDown handler removed as buttons natively handle Enter/Space)
The word 'sublabel' (used as a JSX prop name in MessageRenderer.tsx) is not in any standard dictionary. Add both cases to suppress the cspell CI false positive.
Fix all remaining lint violations across the full web-client source
so that BIOME_LINT passes on all TypeScript files in the repo:
Biome fixes:
- useTemplate: string concat → template literals in useChat.ts (×2)
- useExhaustiveDependencies: add fetchMandate to sendToAgent deps;
add monitoringData?.qty to auto-poll useEffect deps; reference
messages.length in App.tsx scroll effect callback
- noNonNullAssertion: guard getElementById('root') with null check
- noUnusedImports: remove MonitoringStatus from mandateEntries.ts
- noUnusedFunctionParameters: msg → _msg, tc → _tc in toolCallEntries
- noUnusedVariables: remove unused args variable in toolCallEntries
- useButtonType: add type="button" to CopyButton and card-header
button in MandateCard.tsx
- noArrayIndexKey: replace key={i} with key={d.salt??d.key??String(i)}
- noSvgWithoutTitle: add <title> to SVGs in ReceiptCard.tsx and
UserActionCard.tsx
Prettier fixes:
- mandateEntries.ts: break long import (>80 chars) to multi-line,
remove triple-blank-line block
Both are used as meaningful shorthand terms in the web-client source: - dedup: abbreviation of deduplicate (used in useChat.ts, mandateEntries.ts) - sublabel: UI prop name in UserActionCard / MessageRenderer
…e files Run prettier --write on all 12 modified TypeScript/TSX files to bring them into full compliance with the project's .prettierrc config. No logic changes — formatting only. Biome lint errors were fixed in the previous commits; this commit solely addresses TYPESCRIPT_PRETTIER CI failures caused by pre-existing style inconsistencies that were surfaced when super-linter checked the modified files.
The project uses Biome for TypeScript/TSX quality (BIOME_LINT). Super-linter itself warns in CI output that running Biome and ESLint simultaneously on TSX files causes conflicts and recommends disabling one. Disable the ESLint-based checkers so Biome is the single source of truth for TypeScript quality. The ESLint checks were also producing false positives: - TSX: react/react-in-jsx-scope is not required for React 17+ JSX transform; super-linter's ESLint also lacks node_modules so every module import is flagged as unresolvable - TYPESCRIPT_ES: browser APIs (fetch, crypto.randomUUID) are flagged as unsupported Node.js built-ins because super-linter treats all TS as server-side Node.js rather than browser code
HTML spec forbids flow content (div) inside a button element. Replace the inner layout divs in ItemRow with span elements and add `display: block` to the SCSS rules for .item-name, .item-id, .item-price and .item-stock so they continue to render as block boxes. Also consistently use the destructured `messages` variable throughout App.tsx instead of mixing chatState.messages and messages. Addresses Gemini code-review feedback on PR google-agentic-commerce#249.
Stylelint requires: - rgb() instead of rgba() with alpha-value-notation: percentage (color-function-alias-notation + color-function-notation rules) - Unquoted font-family names for non-generic custom fonts (font-family-name-quotes rule) Converts rgba(52, 211, 153, 0.15/0.3) to rgb(52 211 153 / 15%/30%) and removes quotes from the "Geist" font-family declarations. These were pre-existing violations exposed by the previous commit touching the file.
Summary
docs/ap2/payment_evidence_frame.mdspecifying the Payment Evidence Frame (PEF) format.claim_type, a deterministic content-addressedframe_id, and an optional RFC 9421 signature.claim_typetaxonomy mapping each AP2 lifecycle event to its receipt format:payment_admission,payment_settlement,payment_cancellation,payment_refund,composite_verdict.frame_idderivation (SHA-256 of JCS canonical preimage withframe_idandsignatureexcluded) andreceipt_hashderivation (SHA-256 of JCS canonical inner receipt), giving agents a stable cross-system identifier and an integrity check without deserialising the receipt body.frame_idby reference across task boundaries.pef,PEF,deserialising,multiparty,unrecognisedto.cspell/custom-words.txt.Normative reference
draft-hopley-x402-payment-evidence-frameCanonicalisation follows RFC 8785 (JCS) per the pin URI
urn:x402:canonicalisation:jcs-rfc8785-v1. Optional signature follows RFC 9421.Test plan
mkdocs serve).frame_idcomputation against the live example: excludeframe_idandsignaturefrom the object, run JCS canonicalisation, compute SHA-256, compare tosha256:9badca88....receipt_hashcomputation against the live example: JCS-canonicalise the embeddedreceiptobject, compute SHA-256, compare tosha256:bc7a68b6....cspellor equivalent) with the new words added to.cspell/custom-words.txt.--as separator).AlgoVoi (chopmob-cloud) -- Acquisition enquiries: https://docs.algovoi.co.uk/acquisition