Skip to content

[Feat]: Post-checkout agent action record — verifiable audit trail after Checkout Receipt #290

Open

Description

@giskard09

Summary

AP2 defines the commerce flow through the Checkout Receipt. The Shopping Agent's role ends at "receiving Receipts and handling success and error" (implementation_considerations.md). After that receipt is issued, the Shopping Agent executes a task — books the flight, reserves the table, completes the order. AP2 does not currently define how to record that execution in a verifiable way.

The gap

The Payment Evidence Frame (#274) establishes a closed taxonomy for payment lifecycle events: payment_admission, payment_settlement, payment_cancellation, payment_refund, composite_verdict. This is the payment layer.

After the receipt with payment_settlement is issued, the Shopping Agent acts. That action is outside the PEF taxonomy by design — it is not a payment event. But it is the event that regulators and principals need to verify: what did the autonomous agent do with the access it received?

AP2 has no primitive for this.

Why this matters now

EU AI Act Article 12 requires logging capabilities for high-risk AI systems, and Article 26(6) requires deployers to keep those logs to the extent they are under their control. Both apply from 2 December 2027 for systems in the high-risk areas of Annex III. AP2 implementors whose Shopping Agents fall into one of those areas will need a technical answer to this requirement. The Checkout Receipt confirms that payment happened. It does not confirm what the agent did next.

Edited 2026-09-24: an earlier version said both obligations enter force in August 2026 and implied they apply to every Shopping Agent in the EU. Source: ai-act-service-desk.ec.europa.eu.

The gap is structural, not implementation-specific. Any Shopping Agent that receives a Checkout Receipt and executes autonomously has this audit gap, regardless of the payment processor or credential provider.

A proposed primitive: action_ref

An action_ref is a deterministic post-execution anchor:

action_ref = SHA-256(JCS({
  agent_id,       // stable identifier for the Shopping Agent
  action_type,    // what was executed (e.g. "checkout_complete")
  scope,          // what resource was accessed
  timestamp       // ISO 8601, ms precision
}))

Canonicalization follows RFC 8785 (JCS) — the same standard used throughout AP2. The action_ref is recomputable by any verifier from the same four fields, without trusting the operator that ran the agent.

This is distinct from the AlgoVoi Settlement Attestation (#271): the Settlement Attestation records the categorical payment outcome (SETTLED/PENDING/REVERSED). The action_ref records what the agent did after settlement. These are orthogonal — a verifier can have both without one implying the other.

Implementation path

A provider implementation exposes a /external/trail endpoint that accepts a POST with the action payload and returns a trail_id backed by an on-chain anchor. The action_ref is included in the trail and recomputable independently.

This is specified in draft-giskard-aeoess-action-ref (Internet-Draft, co-authored with AEOESS): https://github.com/giskard09/draft-giskard-aeoess-action-ref

CTEF v0.4 marks action_ref as REQUIRED for exactly-once execution guards (commit 773660e1). Four independent conformance sets have been validated byte-identical.

A reference implementation and self-certify endpoint are live at https://mycelium.rgiskard.xyz/external/trail

Suggested addition

Would an addition to implementation_considerations.md be the right place to document this — either as a new "Post-checkout audit" section or as an extension to the existing "Agent Identification" section? Happy to submit a PR if the direction is useful for AP2.

Activity

  1. vaaraio commented on Jun 21, 2026

    @vaaraio

    The gap is real, and anchoring it to Article 12 and Article 26 is the right pressure. A deployer has to show not just that payment happened, but what the agent did with the access, in a form a regulator can check without trusting the system that produced it.

    It helps to separate two things the post-checkout record has to do.

    First, identify the action. A content-addressed identifier over the action's semantic fields handles that, and PEF (#274) already sets the canonicalisation discipline for the payment side (JCS / RFC 8785, content-addressed frame_id).

    Second, prove the record is complete. Article 12 is a logging-integrity obligation, not only an identifier scheme. A per-action hash tells you a given action was recorded. It does not tell you whether an action inside the declared scope was dropped. That second question is the one a regulator actually asks, and an identifier alone stops short of it.

    There is an existing receipt layer that covers both, vaara.receipt/v1: https://github.com/vaaraio/vaara/blob/main/SPEC.md . The post-checkout action becomes a signed authorization receipt minted at the point where the action is authorized. It names the AP2 Checkout Receipt it followed (by PEF frame_id) and the action, carries the declared coverage boundary, and as of this week carries gap-evident completeness: a monotonic per-boundary sequence with a signed running count, so a dropped receipt inside the boundary is a provable gap from the held receipts alone, with no trusted witness. Public conformance vectors recompute the dropped-action case independently: https://github.com/vaaraio/vaara/tree/main/tests/vectors/contiguity_v0 .

    It uses the same canonicalisation as PEF (JCS, RFC 8785, content-addressed). Time anchoring is RFC 3161, so durability does not depend on a chain being reachable, which matters for anyone retaining regulated audit logs under Article 26. vaara.receipt/v1 is already the parent that downstream profiles pin to: an x402 settlement binding and an eIDAS qualified-timestamp profile pin to it today. AP2 can do the same from the point where the Checkout Receipt ends, rather than define a new primitive.

  2. giskard09 commented on Jun 21, 2026

    @giskard09
    Author

    The separation between identifying the action and proving the record is complete is exactly right — those are two distinct Article 12 obligations, and an identifier alone stops short of the second.

    One detail worth noting for AP2 specifically: vaara.receipt/v1 defines an actionRef field with its own preimage construction (JCS over agentId, actionType, scope, timestampMs). The draft-giskard-aeoess-action-ref preimage uses the same canonicalization (JCS / RFC 8785) but a different field set — the two are byte-incompatible. For an AP2 profile that wants to reference both, that's worth resolving at the field level before pinning to either.

    On the ledger anchor path in vaara.receipt/v1: that's the right trust model for regulated retention — block-time bounding with no TSA dependency. The conformance vectors we opened this issue with sit at the same layer, covering the post-checkout action record specifically. If there's overlap worth mapping, the open thread in A2A #1847 is the right place — we have a conformance vector exchange pending there.

  3. vaaraio commented on Jun 22, 2026

    @vaaraio

    Agreed the field-level resolution should happen before AP2 pins anything. One correction so the comparison is exact: the shipped vaara.receipt/v1 actionRef preimage is sha256(JCS({agentId, actionType, scope, timestampMs, seq, terminal})), not the four-field set. The seq and terminal fields are load-bearing, not incidental: terminal stops an in-progress receipt being presented where the terminal one is required, and seq is what lets a verifier sequence actions under a boundary and catch a dropped one. They are the difference between identifying an action and proving the record around it is whole.

    That second property is where I would anchor the AP2 profile. v1.5.0 already carries an AP2 checkout binding: the post-checkout receipt names the checkout by content address (evidenceRef.ref = ap2:checkout/<frame_id> over the PEF frame_id), the AP2 task scope is the coverage boundary, and the completeness block sequences the actions under it. A per-action hash says an action was recorded; the running count says none inside the task boundary was dropped. Conformance vectors for both are public (tests/vectors/ap2_v0/, tests/vectors/contiguity_v0/), each with a _check_independent.py a verifier runs with only the receipt and the settlement in hand.

    So rather than resolve two drafts into a new third format, the simpler path for AP2 is to pin from where the Checkout Receipt ends to the shipped preimage and map draft-giskard-aeoess-action-ref onto that field set. I can put the vectors side by side if that helps the comparison.

  4. giskard09 commented on Jun 22, 2026

    @giskard09
    Author

    Thanks for the correction — the six-field set is clearer. On the architecture question: I'd separate two properties you're combining in the preimage.

    The first is action identity: a hash that lets a verifier confirm a specific action was recorded as claimed. That's what action_ref does — sha256(JCS({agent_id, action_type, scope, timestamp})) binds those four fields and is recomputable with only the receipt and the settlement.

    The second is record completeness: the guarantee that no action inside a boundary was dropped. That's what seq and terminal give you — but it's a property of the record set, not of an individual action. Placing seq and terminal inside the preimage couples a single action's identity to its position in a sequence: a verifier who wants to check one action in isolation now needs the full sequence context.

    The cleaner separation: action_ref as the per-action identity handle, and a completeness layer (your seq + terminal + running count) built over a set of action_refs. Each layer is independently verifiable.

    For AP2 specifically: Checkout Receipt closes at payment_settlement, action_ref identifies each post-checkout action, contiguity covers the task boundary. Three composable layers rather than a merged preimage.

    I can put conformance vectors for the action_ref side alongside yours if that helps the comparison.

  5. vaaraio commented on Jun 22, 2026

    @vaaraio

    Good split, and I'll take the two-property framing. The disagreement is narrower than it looks: it is not identity versus completeness, it is what the signature commits to.

    One correction to my own phrasing first, so the mechanism is exact. The six fields are carried on the action record (the TAP request, schema tap.request/v0), not inline in the receipt. The request is content-addressed with JCS, and the receipt commits to that digest under its signature: decisionDerived.evidenceRef.digest = sha256(JCS(request)), with evidenceRef.ref = tap:request/. So a verifier checking one action in isolation holds the request and the receipt that names it, recomputes actionRef from the request, and confirms the receipt binds to it. No other receipt is in play. Sequence context is only needed for the separate completeness pass, so per-action identity and set-level completeness stay independently verifiable.

    Where the field set matters is what that digest commits to. A four-field action_ref over {agent_id, action_type, scope, timestamp} produces the same value for an in-progress action and its terminal version. They become substitutable: an in-progress record can be presented where the terminal one is required, and nothing in the identity handle catches it. Folding terminal into the content-addressed preimage is what makes those two distinct identities, and because the receipt signs the digest, terminal and seq are bound under the receipt signature transitively. If sequence position instead lives only in an outer layer over bare action_refs, it is not committed by any single record's signature, so it is re-orderable by whoever assembles the set, and the completeness guarantee then rests on trusting the assembler, which is the thing the receipt is meant to remove.

    For AP2 this is not abstract. Whether this was the final post-checkout action or an intermediate one is exactly the distinction a chargeback or an Article 12 review turns on, and a handle that hashes them identically cannot carry it.

    So I would put it as one content-addressed action record that commits to the action and its position, named under a signed receipt, plus a contiguity pass over the set. Two independently verifiable checks, one of them not dependent on trusting whoever holds the records. The in-progress and terminal case is in the public vectors as of this week (tests/vectors/tap_v0/, same pattern as ap2_v0). I can put them next to an action_ref vector so the substitution case is concrete.

  6. 0xbrainkid commented on Jun 22, 2026

    @0xbrainkid

    This seems like the right layer to separate payment completion from agent accountability. The Checkout Receipt proves the transaction path, but the shopping agent still needs a durable post-action record for what it did after receipt issuance: actor identity, delegated authority, tool/action summary, timestamp, outcome, and evidence hash/pointer.

    One design option is to keep that record protocol-neutral and attach it to an agent profile or reputation surface rather than the payment object itself. That lets AP2 stay focused on commerce settlement while downstream systems can evaluate agent behavior across merchants, sessions, and organizations.

    This is the same gap AgentFolio/SATP has been tracking: payment receipts answer whether the transaction was authorized; agent receipts answer whether the autonomous actor behaved as expected afterward.

  7. giskard09 commented on Jun 22, 2026

    @giskard09
    Author

    On the substitution case: action_ref producing the same value for in-progress and terminal is correct by design, not a gap. It binds action identity — agent, action type, scope, timestamp — not execution state. State is a property of the signed receipt envelope, which is where the check belongs. A receipt that signs the full action record commits to terminal and seq under that signature; the swap fails there, not at the identity handle. signing-trust-ref-v1 formalizes that layer for stacks that need it explicit. The conformance vector same_action_ref_different_state in near-miss-v1 documents this with byte-verified hashes (expected_result: KNOWN_DESIGN_PROPERTY).

    The reason to keep them separate matters for AP2 specifically: if state were in the preimage, a verifier holding a terminal receipt from merchant B could not match it against an in-progress anchor from merchant A — different identifiers for the same action. Cross-merchant correlation requires the identity handle to be stable across state transitions.

    The architecture brainKID describes — a protocol-neutral, durable post-action record that identifies the actor and action without coupling to the payment object — is what draft-giskard-aeoess-action-ref formalizes as the post-settlement layer. Five independent implementations verified byte-identical at examples/conformance; CTEF v0.4 marks it REQUIRED for cross-stack agent trust verification.

  8. Rul1an commented on Jun 23, 2026

    @Rul1an

    The post-checkout action record is the observed-effect complement to the checkout receipt, and the two compose cleanly if the spec names the record contract rather than a single format.

    The checkout receipt records the payment-side authorization and settlement path. The post-checkout action record answers a different question: what the agent then actually did, ideally seen independently rather than only self-reported, bounded by the coverage that record had. If both are content-addressed, declared against observed, with explicit coverage so an absent action is distinguishable from an unobserved one, then a checkout receipt and an independent action record sit as peer instances under one contract, neither standing in for the other. A verifier reads the pair rather than trusting either alone.

  9. arian-gogani commented on Jun 23, 2026

    @arian-gogani

    the record contract framing @Rul1an described is the right shape — the spec names the fields, implementations produce the record in their own format, composability comes from the shared field semantics.

    one field worth making structurally mandatory from day one: a content-derived action identifier computed before execution. if the identifier depends on the outcome, it can't be used as the dedup or correlation key for the in-flight state.

    action_ref = SHA-256(JCS({agent_id, action_type, scope, timestamp_ms})) derives from the intent fields alone — available at checkout receipt time, before the execution completes. the post-checkout action record then carries the same action_ref, making it joinable to the checkout receipt by any verifier without trusting the AP2 operator.

    for the substitution case @giskard09 named: action_ref staying stable across in-progress and terminal states is correct. the mutable part is terminal_state (COMMITTED / FAILED) in the signed envelope. a verifier checks: do the checkout receipt's authorized scope and the action record's action_ref produce the same hash from the four preimage fields? if yes, the action was the authorized one. if the agent deviated — different action_type or scope — the hashes diverge.

    nobulex (pip install nobulex) produces this record type. the field set is in agentrust-io/integrations#6 as a concrete reference.

  10. vaaraio commented on Jun 24, 2026

    @vaaraio

    @arian-gogani agreed that the action identifier should be content-derived and computed before execution, and that an outcome-dependent id can't serve as the correlation key for in-flight state. That is the argument for putting terminal and seq inside the preimage rather than alongside it.

    The construction specified in the draft, recomputed in the open, is:

    actionRef = sha256(JCS({agentId, actionType, scope, timestampMs, seq, terminal}))

    Both fields are known at receipt time, so the id stays pre-execution and intent-derived:

    • terminal is the in-flight bit. If it sits outside the preimage, the in-flight receipt and its terminal successor hash to the same actionRef, and a dedup or correlation key collapses them into one. With terminal inside, terminal: false and the final record are distinct keys, so a consumer can correlate the in-flight state and still refuse the non-final receipt where the terminal one is required.
    • seq is the per-boundary sequence number, so a held set is gap-evident: a verifier can tell that action 2 is missing between a present 1 and 3 instead of reading a chain with a silent hole as complete.

    A four-field key ({agent_id, action_type, scope, timestamp_ms}) drops both, so it can correlate but cannot express in-flight versus final or detect a missing step. If AP2 makes the identifier structurally mandatory from day one, the field set is what decides whether it can do those two jobs. The receipt instance and its recompute are specified in draft-sirkkavaara-vaara-receipt (vaara.receipt/v1); the six-field preimage recomputes green from public conformance vectors, so it is demonstrated rather than asserted.

  11. giskard09 commented on Jun 24, 2026

    @giskard09
    Author

    The record contract framing is the right abstraction for AP2 — names the fields, lets implementations produce the format.

    One field the spec already resolves: timestamp as RFC 3339 string, not timestamp_ms as epoch-integer. From action-ref.md: "Implementations holding epoch-ms integers MUST convert to RFC 3339 UTC with three-digit millisecond precision before hashing. Implementations that hash the epoch-ms integer directly are not conformant." The negative conformance vector for this case (neg-b01-epoch-integer) is in examples/conformance/recompute-drift-v1/.

    The join-key property only holds cross-stack if the field contract pins this — two implementations using different timestamp types produce different action_refs for the same event.

  12. arian-gogani commented on Jun 24, 2026

    @arian-gogani

    a correction on the timestamp field: the nobulex spec uses timestamp_ms as an epoch-millisecond integer, not RFC 3339 string. the field name itself (timestamp_ms) signals the type.

    this is the documented divergence point between the two specs — and it's the reason the two implementations produce different hashes for the same logical timestamp. JCS (RFC 8785) serializes integers and strings differently, so:

    • nobulex preimage: {"timestamp_ms": 1779618600000} (integer)
    • argentum-core preimage: {"timestamp": "2026-05-24T10:30:00.000Z"} (string)

    these produce different SHA-256 outputs from the same moment in time. they are not interoperable without a field mapping layer.

    for AP2: whichever encoding you standardize on, it needs to be explicit and locked — the field name and type together define the byte sequence. timestamp_ms: integer and timestamp: string are different fields producing different hashes, not different encodings of the same field.

  13. vaaraio commented on Jun 25, 2026

    @vaaraio

    The timestamp_ms-as-integer versus RFC 3339-string divergence arian-gogani named is the right problem to surface before this thread converges.

    Two implementations producing different hashes for the same logical timestamp is not a minor interop wrinkle: it breaks the portability of the action identifier. A verifier holding a record from one stack cannot reproduce the actionRef computed by the other, which is the exact failure mode a conformance corpus exists to prevent.

    The governance_decision_v0 conformance set published at vaaraio/vaara includes JCS vectors for this surface, and they pin RFC 3339 UTC string as the timestamp form in the preimage. The rationale: JCS (RFC 8785) serializes integers and strings by different rules, so the choice of type is the conformance point, not style. Epoch-integer and RFC 3339-string are not interchangeable under JCS.

    If the AP2 post-execution record is going to name actionRef as a normative field, it needs to name the timestamp type in the preimage as a normative requirement, not leave it to implementors. Pointing at an existing conformance target rather than re-specifying from scratch is the lower-friction path.

  14. giskard09 commented on Jun 25, 2026

    @giskard09
    Author

    The point is right: under JCS (RFC 8785), integer and string are serialized by different rules, so 1781166600123 and "2026-06-11T08:30:00.123Z" produce different byte sequences and different SHA-256 outputs for the same logical instant. This is not a portability wrinkle — it is a different identifier.

    The recompute-drift-v1 vectors in argentum-core already cover this surface. neg-b01-epoch-integer carries a real SHA-256 of the epoch-integer form alongside the canonical RFC 3339 digest for the same instant: d807275… ≠ d461626…. The grammar gate requires the verifier to reject the epoch-integer at timestamp validation before hashing — never convert and retry. neg-b02 and neg-b03 cover second-precision and microsecond forms for the same reason: each rendering is a structurally distinct byte sequence under JCS.

    For AP2 naming action_ref as a normative field, the timestamp type constraint follows directly from pinning the preimage: sha256(JCS({action_type, agent_id, scope, timestamp})) where timestamp is RFC 3339 UTC with exactly three fractional digits and a Z suffix. That constraint is already machine-checkable against the published vectors.

  15. giskard09 commented on Aug 6, 2026

    @giskard09
    Author

    Following up here with what has landed since June, specifically on-topic for what this thread converged on (the RFC 3339 vs epoch-ms timestamp-type constraint in the action_ref preimage):

    action_ref/decision_binding_ref are now specified as a standalone profile (IETF I-D: draft-etcheverry-action-ref) with frozen derivation and conformance vectors — the recompute-drift-v1 vectors cited above (neg-b01-epoch-integer, neg-b02, neg-b03) are part of that published set, unchanged since this thread pinned RFC 3339 as the conformant form.

    No new ask here — just leaving the pointer current in case this thread gets picked back up, either here or wherever the FIDO working groups pick up the post-checkout accountability question.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions