KeelsonDocs
    Judge Mode

    PayPal Agent Mandate Extension

    An open convention that ties each PayPal order, capture, refund and payout to the mandate a user signed. Version 1.0.0.

    Version 1.0.0, 2026-11-12. This is an open convention: anyone may implement it, and it needs no change on PayPal's side. The reference implementation is packages/paypal/src/ids.ts, and Alibi verification checks it in packages/alibi/src/verify/checks/paypal.ts.

    When an AI agent pays through PayPal, the payment carries no sign of who authorized it or under what limits, and a later dispute carries none either. This extension writes short, canonical identifiers into fields PayPal already has, so that each order, capture, refund and payout names the mandate the user signed and the public record of the purchase. The identifiers are the same ones the specification defines.

    The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC 2119] [RFC 8174] when, and only when, they appear in all capitals, as shown here.

    Fields used

    PayPal objectFieldPayPal limitWhat it carries
    Order (Orders v2), purchase_units[]custom_id255 charactersks-custom: the mandate or the purchase, and a digest binding it
    Order, purchase_units[]reference_idThe mandate id, or the purchase's request id
    Order, purchase_units[]descriptionA human-readable line naming the mandate, and for a single purchase the verifier link
    Capture (Payments v2)invoice_id127 characters, unique per merchantks-invoice: the purchase, its sequence on the hold and a digest
    Refund (Payments v2)custom_id127 charactersks-verdict: the verdict that ordered it
    Payout item (Payouts v1)sender_item_id63 charactersks-verdict
    Every create callPayPal-Request-Id headerA value derived from the signed object, so a retry is the same operation

    A capture has no custom_id, so each purchase is named by its capture's invoice_id. PayPal's Disputes API returns the order's custom_id and the capture's invoice_id with the disputed transaction (disputed_transactions[].custom and invoice_number), so a dispute leads back to the purchase.

    Grammar

    The grammar uses ABNF (RFC 5234) with case-sensitive strings (RFC 7405).

    ; Order: purchase_units[].custom_id
    ks-custom  = %s"ks1." env kind "." ulid "." digest32     ; 77 characters
    env        = %s"s" / %s"p"                               ; sandbox / production
    kind       = %s"m" / %s"a"                               ; m: a mandate's hold, a: one purchase's hold
    
    ; Capture: invoice_id
    ks-invoice = %s"KS1-" ulid "-" seq "-" digest16          ; 60 characters
    seq        = "0" %x31-39 / %x31-39 DIGIT                ; 01 to 99
    
    ; Refund: custom_id; payout item: sender_item_id
    ks-verdict = %s"ksv1." ulid "." digest12                 ; 48 characters
    
    ulid       = %x30-37 25CROCKFORD                         ; 128 bits
    digest16   = %x30-37 25CROCKFORD                         ; 16 bytes, big-endian
    digest32   = 43B64URL                                    ; 32 bytes, unpadded
    digest12   = 16B64URL                                    ; 12 bytes, unpadded
    
    CROCKFORD  = DIGIT / %x41-48 / %x4A-4B / %x4D-4E / %x50-54 / %x56-5A   ; 0-9 A-Z without I L O U
    B64URL     = ALPHA / DIGIT / "-" / "_"

    Encodings are canonical. Crockford base32 MUST be upper case. Base64url (RFC 4648, section 5) MUST NOT be padded, and the unused low bits of its last character MUST be zero. A parser MUST reject any other spelling, so each digest has exactly one. A value that does not match the grammar is not under this extension.

    Digests

    H(dst, v) is the domain-separated hash SHA-256(utf8(dst) ‖ 0x00 ‖ utf8(JCS(v))), with JCS from RFC 8785. Inside v, a hash is a 0x-prefixed lower-case hex string, amount is PayPal's decimal string for the amount ("1.00"), and currency is its ISO 4217 code.

    FormulidDigest
    ks1.<env>m.The mandate id (mid)The mandate hash M = SHA-256(utf8("keelson/mandate-challenge/v1") ‖ 0x00 ‖ H_sd ‖ C_m), the challenge the user's passkey signed. H_sd = H("keelson/mandate/v1", payload_sd) and C_m is the mandate commitment as 32 bytes big-endian.
    ks1.<env>a.The purchase's request idH("keelson/alibi/v1", {mandate_hash, cart_hash, merchant_id, amount, currency}), where merchant_id is the Keelson merchant id the mandate allows
    KS1-The Alibi idThe first 16 bytes of H("keelson/alibi/v1", {mandate_hash, cart_hash, policy_set_hash, countersign_digest, merchant_id, amount, currency, paypal_order_id}), where merchant_id is the PayPal merchant id of the payee and paypal_order_id the order the hold belongs to
    ksv1.The verdict idThe first 12 bytes of the verdict's EIP-712 digest

    The sequence in KS1- counts the captures taken from one hold. An issuer MUST take it once per purchase, starting at 01, and MUST NOT issue more than 99 captures from one hold. A parser MUST reject 00.

    PayPal-Request-Id is the first 36 hex characters of a digest: the countersign digest for a capture, the verdict digest for a refund or a payout, M for a mandate's hold, and SHA-256(utf8(custom_id)) for a single purchase's hold.

    Verifying a payment

    Given a PayPal order, capture or dispute, and the purchase's Alibi:

    1. The verifier MUST parse custom_id and invoice_id with the grammar, and MUST treat a value that does not parse as outside this extension.
    2. The env of custom_id MUST be s for an Alibi anchored on Arbitrum Sepolia (chain 421614) and p for one anchored on Arbitrum One (chain 42161).
    3. For kind m, the digest MUST equal the Alibi's /tee/countersign/mandateHash, and that hash MUST be the challenge of /mandate/payload_sd and /tee/countersign/mandateCommitment. For kind a, the ulid MUST be the Alibi id and the digest MUST equal the ks1.<env>a. digest of the countersigned fields.
    4. A capture MUST have an invoice_id. Its ulid MUST be the Alibi id, and its digest MUST equal the first 16 bytes of the KS1- digest of the countersign, /tee/digest, /paypal/merchant_id and /paypal/order_id.
    5. The verifier SHOULD fetch the public record at https://verify.keelson.finance/a/<ulid> and check its leaf in the log at log.keelson.finance. A match binds the PayPal object to the mandate the user signed and to the cart the TEE legs countersigned.

    Test vectors

    The vectors file is packages/paypal/test/vectors/ks1-v1.json. Its valid entries give each value with its parsed fields, and an implementation MUST parse each to those fields and encode the fields back to the same value. Its invalid entries cover lower-case Crockford, the letters I, L, O and U, = padding, non-zero trailing bits, a leading 8, wrong lengths, an unknown env or kind, and other versions, and an implementation MUST reject every one.

    The custom_id and invoice_id vectors were written by PayPal's sandbox on one hold: the order 8MY937060S1606416 and its captures 5VK089183A260134T and 8E547963J1687504T. The verdict reference was generated with encodeVerdictRef.

    FieldValueSource
    custom_idks1.sm.01M6ZWXSB31RY6D753ET0RGPZF.JeYgDHlgnVwNJEpAhS0JFmn8D_0Rzg3Zw2Va0K2LXK4The mandate hold of both Alibis
    invoice_idKS1-01M6ZWZJHWRRHQEYNJAT7NMSV8-01-1H6DNY2FEJ24A4TXAY209K6SXSAlibi 01M6ZWZJHWRRHQEYNJAT7NMSV8, first capture
    invoice_idKS1-01M6ZX07MFWDDDW4199FDJHWEG-02-1XENCQNVGYZTY0H0E8K2HHSY8WAlibi 01M6ZX07MFWDDDW4199FDJHWEG, second capture
    custom_id of a refundksv1.01M6ZX3QW8T2H5N7K9P1R4S6V8.ZxJoTl1yXFfDHgnlGenerated with encodeVerdictRef

    The digest of the custom_id above is the mandate hash 0x25e6200c79609d5c0d244a40852d091669fc0ffd11ce0dd9c3655ad0ad8b5cae, and both Alibis are in packages/alibi/test/fixtures, so the digests can be recomputed from their fields.

    Security considerations

    Replay and retries. Every create call carries a PayPal-Request-Id derived from the object it executes, so a retried capture, refund or payout is the same request to PayPal and is not executed twice. Because invoice_id is unique per merchant, PayPal also refuses a second capture under the same KS1- value.

    Binding. Each digest covers the signed object it names: a KS1- value cannot be moved to another order, merchant, amount or countersign without its digest failing to verify. A value that parses but whose digest does not match MUST be treated as unverified.

    No personal data. invoice_id is shown to the payer, and every field here is visible to PayPal and the merchant. Each form holds only ULIDs and hashes, never a name, an address, an email or the cart's contents.

    The unsigned capture id. PayPal assigns capture_id after the countersign, so no signature covers it. A verifier MUST show it as unverified metadata and MUST locate the capture by its invoice_id.

    Privacy considerations

    The values reveal that a payment was made under an agent mandate, and which mandate, to anyone who sees the PayPal object. A ULID encodes its creation time to the millisecond. Purchases captured from one mandate's hold share its custom_id, so they can be linked to each other; a per-purchase hold (kind a) gives each purchase its own value. The digests reveal nothing about their inputs: each covers a salted or signed value (the mandate's selective-disclosure digests, the countersign digest), so they cannot be reversed by guessing a cart or an amount.

    Proposal for PayPal's Agent Toolkit

    PayPal's Agent Toolkit create_order tool does not let an agent set intent or custom_id, so an agent using it can neither place a hold nor tag an order. Adding both, with the limits of the Orders v2 API, is enough for any agent to follow this extension.

    Changelog

    1.0.0 — 2026-11-12: first published version.