@piprail/sdk 2.9.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,89 @@ All notable changes to `@piprail/sdk` are documented here. The format
4
4
  follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
5
5
  versions follow [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## [2.11.0] — 2026-06-21 — A2A interop hardening: spec-correct merchant status + verified x402-V2 conformance
8
+
9
+ A2A transport correctness, after a **live cross-check against Google's official `x402` / `x402_a2a`
10
+ libraries** (reproducible harness + verdict in `examples/a2a-interop/`). One behavior fix on the A2A rejection path;
11
+ HTTP and every other rail are byte-identical to 2.10.0.
12
+
13
+ - **fix(a2a): a rejected proof now re-challenges as `payment-required`, not the client-only
14
+ `payment-rejected`.** Per the A2A x402 spec §5.1 and Google's reference merchant executor, a MERCHANT
15
+ emits only `payment-required` / `payment-completed` / `payment-failed`; `payment-rejected` and
16
+ `payment-submitted` are CLIENT→merchant statuses. A rejection is now signalled by the appended failure
17
+ receipt + the `x402.payment.error` code — the retry loop and the `input-required` state are unchanged.
18
+ - **interop: PipRail's A2A envelopes are byte-conformant with the current x402 V2 standard.** The
19
+ canonical `x402` lib parses our `PaymentRequired` + `PaymentPayload` (CAIP-2 networks, `amount`,
20
+ `x402Version: 2`, nested `accepted`); the five `x402.payment.*` keys + the extension URI match
21
+ `x402_a2a` verbatim. The deferred emit-version question is resolved — **v2** (the live standard; the
22
+ legacy v0.1 `x402-a2a` package is bitrotted and not targeted). New always-on guard:
23
+ `test/transports/a2a-wire-conformance.test.ts`; reproducible cross-check harness: `examples/a2a-interop/`.
24
+ - **docs(a2a):** the A2A guide is now fully runnable — a real `@a2a-js/sdk` `AgentExecutor` mount, a
25
+ complete AgentCard, where the buyer's payload comes from, and the co-resident one-gate pattern.
26
+ - No public API change. A2A remains seller-side (the `A2APayer` buyer + AP2 mandate carriage stay deferred).
27
+
28
+ ## [2.10.0] — 2026-06-20 — x402 parity: verifiable receipts · the `upto` metered rail · A2A transport
29
+
30
+ Four ratified-x402 capabilities, all **additive and opt-in** — omit the new options and the 402, the
31
+ 200, and the pay path are byte-identical to 2.9.0.
32
+
33
+ - **feat(receipts): verifiable receipts — chain-grounded (no key) + optional EIP-712 attestation.**
34
+ `receipts: true` on a gate emits a self-contained `PipRailReceipt` on every settled payment, in a
35
+ byte-compatible `extensions['offer-receipt'].info` block. **Anyone** re-verifies it against the chain
36
+ with only an RPC: `PipRailClient.verifyReceipt(receipt)` re-reads the settlement tx and re-derives
37
+ `payTo`/`asset`/`payer`, **ignoring the receipt's claims** (never throws; `amount` is a verified lower
38
+ bound; `payer` genuinely re-derived). `client.lastReceipt()` captures it after a paid `fetch`. The new
39
+ additive wire field is `X402Receipt.nonce?` (required to re-verify the five memo-bound families:
40
+ Stellar/XRPL/NEAR/Algorand/TON). **Tier 2 (EVM-only):** `receipts: { attest: { wallet } }` also signs
41
+ the official x402 offer-receipt EIP-712 `RECEIPT_TYPES` with the merchant's existing `payTo` wallet —
42
+ attesting the one thing the chain can't (that the resource was *served*); verify with
43
+ `PipRailClient.verifyAttestation`. `includeTxHash` defaults **true** (PipRail-default verifiability — a
44
+ deliberate, documented divergence from the reference privacy-default; flip to `false` for the §5.3
45
+ empty-string privacy path). New exports: `buildReceiptExtension`, `parseReceiptExtension`,
46
+ `PipRailReceipt`, `SignedReceipt`, `ReceiptInput`, `ReceiptOption`, `ReceiptVerification`.
47
+ - **feat(upto): the ratified `upto` (metered / variable-amount) rail — EVM-Permit2, self-settle.** The
48
+ buyer signs a Permit2 authorization for a **maximum**; the merchant serves, meters, then self-settles
49
+ the **actual** (`≤ max`) from its own relayer through the on-chain `x402UptoPermit2Proxy` — backendless,
50
+ no fee. Opt in with `upto: { relayer, settleAmount }` and meter inside `settleAmount` on a direct
51
+ `gate.verify()` call (`requirePayment` throws for `upto` — it settles before the handler serves). A
52
+ zero charge settles nothing on-chain. New `X402UptoAcceptEntry` / `Permit2UptoAuthorization` types +
53
+ `parseUptoPaymentHeader` / `buildUptoSignatureHeader` codecs.
54
+ - **feat(transport): A2A — `gate.verifyObject()` + the A2A seller handler.** `gate.verifyObject(payload)`
55
+ verifies a raw-JSON payment object (not just a base64 HTTP header), and `createA2APaymentHandler(gate)`
56
+ maps a gate onto Google's A2A JSON-RPC Task/Message metadata — **sharing one replay set with HTTP**.
57
+ Backendless, zero `@a2a` dependency. The parser cores `parseSignatureObject`/`parseExactObject` are now
58
+ exported. *(The A2A buyer, AP2 carriage, and the live-Google-interop emit-version finalization trail.)*
59
+ - **feat(agent/mcp): an 8th tool — `piprail_verify_receipt`** (read-only, key-less) re-verifies a receipt
60
+ against the chain; `piprail_pay_request` now surfaces the `verifiableReceipt`.
61
+ - **fix:** a batch of correctness hardening from an adversarial multi-agent audit — require the replay
62
+ store's `isUsed`/`markUsed` as a pair (a lone one silently disabled double-spend protection); the read
63
+ methods (`planPayment`/`canAfford`/`estimateCost`) degrade instead of throwing on a malformed accept;
64
+ Sui coin pagination; Stellar custom-asset decimals; Tron native safe-integer; and more.
65
+ - **fix:** a second adversarial multi-agent conformance pass (vs the cloned x402 spec) hardened the new
66
+ surfaces — **upto cumulative caps are now merchant-proof:** the budget debits the authorized **MAX**
67
+ (a merchant that under-reports its settled `amount` can no longer loosen `maxTotal`/`maxTotalPerDenom`/
68
+ `windowTotal`); the metered actual is surfaced on `SpendRecord.settledBase`. The `upto` driver now
69
+ enforces **strict** `permitted.amount === advertised max` at verify time (an over-permit is rejected —
70
+ x402 `scheme_upto_evm` §Phase 3). `describe()` now projects the upto rail's mandatory
71
+ `extra.facilitatorAddress` (+ the exact rail's EIP-712 domain) so a discovered rail is reconstructable.
72
+ A2A conformance: a rejected proof re-challenges as `payment-rejected` (the spec status that pairs with
73
+ the retryable `input-required`, not the terminal `payment-failed`) carrying a failure receipt with
74
+ `network` + `transaction:''`. The agent guide + `paymentTools` JSDoc now document the upto rail and the
75
+ 8th tool.
76
+ - **fix:** a third verification pass (regression-focused) caught two **doc copy-paste bugs** that would
77
+ brick a user's code — the `upto` seller + Tier-2 receipt examples used the pre-v2 `{ privateKey }`
78
+ wallet field (which throws `WrongFamilyError`) instead of the unified `{ key }`. Also: the A2A
79
+ terminal-`failed` receipt now carries the attempted `network`; the A2A `fulfill` doc/example returns a
80
+ structurally valid artifact (`{ name, parts }`); the MCP config docs + registry `server.json` now list
81
+ `upto`; and a **regression test** locks the merchant-proof cumulative leash (a sequence of
82
+ under-reporting `upto` payments is refused by `maxTotal`/`maxTotalPerDenom`/`windowTotal`).
83
+ - **fix:** a fourth, end-to-end pass (every flow traced hop-by-hop + a clean-room standards sign-off,
84
+ which came back **CONFORMANT on all four wire formats**, and a live MCP-pays-the-`upto`-rail proof)
85
+ found only doc-accuracy gaps + one low edge case: A2A failure receipts now attribute `network` from
86
+ the buyer's submitted payload (covering multi-network gates + v1-flat exact payloads), and the whole
87
+ docs-site is updated to the **8-tool** surface (the new `piprail_verify_receipt`) with the 2.10.0
88
+ receipts/upto/A2A APIs documented in the reference. No SDK behavior change beyond the A2A edge fix.
89
+
7
90
  ## [2.9.0] — 2026-06-19 — Cross-token grand total · payment-count caps · durable budget · richer spend observability
8
91
 
9
92
  Spend controls grow up — a single budget across every token and chain, caps on the *number* of
package/README.md CHANGED
@@ -74,8 +74,9 @@ The same app can **take** payments and **make** them. → [Making payments](http
74
74
  | | |
75
75
  |---|---|
76
76
  | **[Getting started](https://docs.piprail.com/getting-started/introduction/)** | Install · quickstart · how it works |
77
- | **[Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)** | `requirePayment` · `createPaymentGate` · the `exact` rail |
77
+ | **[Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)** | `requirePayment` · `createPaymentGate` · the `exact` rail · the `upto` metered rail |
78
78
  | **[Making payments](https://docs.piprail.com/making-payments/piprail-client/)** | `PipRailClient` · `quote` · `estimateCost` · `planPayment` · auto-route · `MultiChainPayer` |
79
+ | **[Verifiable receipts](https://docs.piprail.com/accepting-payments/verifiable-receipts/)** | Chain-grounded, anyone-verifiable receipts (no key) · optional EIP-712 attestation |
79
80
  | **[Spend controls](https://docs.piprail.com/spend-controls/payment-policy/)** | Per-token + cross-token grand total · payment-count caps · time envelope · durable budget · the spend ledger |
80
81
  | **[Agent toolkit](https://docs.piprail.com/agent-toolkit/payment-tools/)** | `paymentTools` · the agent guide · NL renderers |
81
82
  | **[Discovery](https://docs.piprail.com/discovery/discover-and-register/)** | Find & be found on the open x402 indexes ($0, no backend) |
@@ -0,0 +1,92 @@
1
+ // src/drivers/evm/receipt.ts
2
+ import {
3
+ recoverTypedDataAddress,
4
+ getAddress
5
+ } from "viem";
6
+ var RECEIPT_DOMAIN = Object.freeze({
7
+ name: "x402 receipt",
8
+ version: "1",
9
+ chainId: 1
10
+ });
11
+ var RECEIPT_TYPES = Object.freeze({
12
+ Receipt: [
13
+ { name: "version", type: "uint256" },
14
+ { name: "network", type: "string" },
15
+ { name: "resourceUrl", type: "string" },
16
+ { name: "payer", type: "string" },
17
+ { name: "issuedAt", type: "uint256" },
18
+ { name: "transaction", type: "string" }
19
+ ]
20
+ });
21
+ var RECEIPT_PRIMARY_TYPE = "Receipt";
22
+ function receiptMessage(input) {
23
+ return {
24
+ version: 1n,
25
+ network: input.network,
26
+ resourceUrl: input.resourceUrl,
27
+ payer: input.payer,
28
+ issuedAt: BigInt(input.issuedAt),
29
+ // §5.3: unused optional fields are the empty string in the SIGNED message, never omitted.
30
+ transaction: input.transaction ?? ""
31
+ };
32
+ }
33
+ async function signReceiptEvm(wallet, input) {
34
+ const adapter = wallet._native;
35
+ const account = adapter.account;
36
+ const walletClient = adapter.walletClient;
37
+ const signature = await walletClient.signTypedData({
38
+ account,
39
+ domain: RECEIPT_DOMAIN,
40
+ types: RECEIPT_TYPES,
41
+ primaryType: RECEIPT_PRIMARY_TYPE,
42
+ message: receiptMessage(input)
43
+ });
44
+ return {
45
+ format: "eip712",
46
+ signature,
47
+ // The address that actually produced the signature (the bound wallet). A
48
+ // verifier re-recovers + checks it equals the trusted `payTo`.
49
+ signer: account.address,
50
+ payload: {
51
+ version: 1,
52
+ network: input.network,
53
+ resourceUrl: input.resourceUrl,
54
+ payer: input.payer,
55
+ issuedAt: input.issuedAt,
56
+ transaction: input.transaction ?? ""
57
+ }
58
+ };
59
+ }
60
+ async function verifyReceiptAttestationEvm(input) {
61
+ try {
62
+ const recovered = await recoverTypedDataAddress({
63
+ domain: RECEIPT_DOMAIN,
64
+ types: RECEIPT_TYPES,
65
+ primaryType: RECEIPT_PRIMARY_TYPE,
66
+ message: receiptMessage(input),
67
+ signature: input.signature
68
+ });
69
+ let expected;
70
+ let actual;
71
+ try {
72
+ expected = getAddress(input.payTo);
73
+ actual = getAddress(recovered);
74
+ } catch {
75
+ return { ok: false, signer: recovered, reason: "bad-address" };
76
+ }
77
+ if (actual !== expected) {
78
+ return { ok: false, signer: recovered, reason: "signer-mismatch" };
79
+ }
80
+ return { ok: true, signer: recovered };
81
+ } catch {
82
+ return { ok: false, reason: "invalid-signature" };
83
+ }
84
+ }
85
+
86
+ export {
87
+ RECEIPT_DOMAIN,
88
+ RECEIPT_TYPES,
89
+ RECEIPT_PRIMARY_TYPE,
90
+ signReceiptEvm,
91
+ verifyReceiptAttestationEvm
92
+ };
@@ -0,0 +1,92 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } }// src/drivers/evm/receipt.ts
2
+
3
+
4
+
5
+ var _viem = require('viem');
6
+ var RECEIPT_DOMAIN = Object.freeze({
7
+ name: "x402 receipt",
8
+ version: "1",
9
+ chainId: 1
10
+ });
11
+ var RECEIPT_TYPES = Object.freeze({
12
+ Receipt: [
13
+ { name: "version", type: "uint256" },
14
+ { name: "network", type: "string" },
15
+ { name: "resourceUrl", type: "string" },
16
+ { name: "payer", type: "string" },
17
+ { name: "issuedAt", type: "uint256" },
18
+ { name: "transaction", type: "string" }
19
+ ]
20
+ });
21
+ var RECEIPT_PRIMARY_TYPE = "Receipt";
22
+ function receiptMessage(input) {
23
+ return {
24
+ version: 1n,
25
+ network: input.network,
26
+ resourceUrl: input.resourceUrl,
27
+ payer: input.payer,
28
+ issuedAt: BigInt(input.issuedAt),
29
+ // §5.3: unused optional fields are the empty string in the SIGNED message, never omitted.
30
+ transaction: _nullishCoalesce(input.transaction, () => ( ""))
31
+ };
32
+ }
33
+ async function signReceiptEvm(wallet, input) {
34
+ const adapter = wallet._native;
35
+ const account = adapter.account;
36
+ const walletClient = adapter.walletClient;
37
+ const signature = await walletClient.signTypedData({
38
+ account,
39
+ domain: RECEIPT_DOMAIN,
40
+ types: RECEIPT_TYPES,
41
+ primaryType: RECEIPT_PRIMARY_TYPE,
42
+ message: receiptMessage(input)
43
+ });
44
+ return {
45
+ format: "eip712",
46
+ signature,
47
+ // The address that actually produced the signature (the bound wallet). A
48
+ // verifier re-recovers + checks it equals the trusted `payTo`.
49
+ signer: account.address,
50
+ payload: {
51
+ version: 1,
52
+ network: input.network,
53
+ resourceUrl: input.resourceUrl,
54
+ payer: input.payer,
55
+ issuedAt: input.issuedAt,
56
+ transaction: _nullishCoalesce(input.transaction, () => ( ""))
57
+ }
58
+ };
59
+ }
60
+ async function verifyReceiptAttestationEvm(input) {
61
+ try {
62
+ const recovered = await _viem.recoverTypedDataAddress.call(void 0, {
63
+ domain: RECEIPT_DOMAIN,
64
+ types: RECEIPT_TYPES,
65
+ primaryType: RECEIPT_PRIMARY_TYPE,
66
+ message: receiptMessage(input),
67
+ signature: input.signature
68
+ });
69
+ let expected;
70
+ let actual;
71
+ try {
72
+ expected = _viem.getAddress.call(void 0, input.payTo);
73
+ actual = _viem.getAddress.call(void 0, recovered);
74
+ } catch (e) {
75
+ return { ok: false, signer: recovered, reason: "bad-address" };
76
+ }
77
+ if (actual !== expected) {
78
+ return { ok: false, signer: recovered, reason: "signer-mismatch" };
79
+ }
80
+ return { ok: true, signer: recovered };
81
+ } catch (e2) {
82
+ return { ok: false, reason: "invalid-signature" };
83
+ }
84
+ }
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+ exports.RECEIPT_DOMAIN = RECEIPT_DOMAIN; exports.RECEIPT_TYPES = RECEIPT_TYPES; exports.RECEIPT_PRIMARY_TYPE = RECEIPT_PRIMARY_TYPE; exports.signReceiptEvm = signReceiptEvm; exports.verifyReceiptAttestationEvm = verifyReceiptAttestationEvm;