@piprail/sdk 2.9.0 → 2.10.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,68 @@ 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.10.0] — 2026-06-20 — x402 parity: verifiable receipts · the `upto` metered rail · A2A transport
8
+
9
+ Four ratified-x402 capabilities, all **additive and opt-in** — omit the new options and the 402, the
10
+ 200, and the pay path are byte-identical to 2.9.0.
11
+
12
+ - **feat(receipts): verifiable receipts — chain-grounded (no key) + optional EIP-712 attestation.**
13
+ `receipts: true` on a gate emits a self-contained `PipRailReceipt` on every settled payment, in a
14
+ byte-compatible `extensions['offer-receipt'].info` block. **Anyone** re-verifies it against the chain
15
+ with only an RPC: `PipRailClient.verifyReceipt(receipt)` re-reads the settlement tx and re-derives
16
+ `payTo`/`asset`/`payer`, **ignoring the receipt's claims** (never throws; `amount` is a verified lower
17
+ bound; `payer` genuinely re-derived). `client.lastReceipt()` captures it after a paid `fetch`. The new
18
+ additive wire field is `X402Receipt.nonce?` (required to re-verify the five memo-bound families:
19
+ Stellar/XRPL/NEAR/Algorand/TON). **Tier 2 (EVM-only):** `receipts: { attest: { wallet } }` also signs
20
+ the official x402 offer-receipt EIP-712 `RECEIPT_TYPES` with the merchant's existing `payTo` wallet —
21
+ attesting the one thing the chain can't (that the resource was *served*); verify with
22
+ `PipRailClient.verifyAttestation`. `includeTxHash` defaults **true** (PipRail-default verifiability — a
23
+ deliberate, documented divergence from the reference privacy-default; flip to `false` for the §5.3
24
+ empty-string privacy path). New exports: `buildReceiptExtension`, `parseReceiptExtension`,
25
+ `PipRailReceipt`, `SignedReceipt`, `ReceiptInput`, `ReceiptOption`, `ReceiptVerification`.
26
+ - **feat(upto): the ratified `upto` (metered / variable-amount) rail — EVM-Permit2, self-settle.** The
27
+ buyer signs a Permit2 authorization for a **maximum**; the merchant serves, meters, then self-settles
28
+ the **actual** (`≤ max`) from its own relayer through the on-chain `x402UptoPermit2Proxy` — backendless,
29
+ no fee. Opt in with `upto: { relayer, settleAmount }` and meter inside `settleAmount` on a direct
30
+ `gate.verify()` call (`requirePayment` throws for `upto` — it settles before the handler serves). A
31
+ zero charge settles nothing on-chain. New `X402UptoAcceptEntry` / `Permit2UptoAuthorization` types +
32
+ `parseUptoPaymentHeader` / `buildUptoSignatureHeader` codecs.
33
+ - **feat(transport): A2A — `gate.verifyObject()` + the A2A seller handler.** `gate.verifyObject(payload)`
34
+ verifies a raw-JSON payment object (not just a base64 HTTP header), and `createA2APaymentHandler(gate)`
35
+ maps a gate onto Google's A2A JSON-RPC Task/Message metadata — **sharing one replay set with HTTP**.
36
+ Backendless, zero `@a2a` dependency. The parser cores `parseSignatureObject`/`parseExactObject` are now
37
+ exported. *(The A2A buyer, AP2 carriage, and the live-Google-interop emit-version finalization trail.)*
38
+ - **feat(agent/mcp): an 8th tool — `piprail_verify_receipt`** (read-only, key-less) re-verifies a receipt
39
+ against the chain; `piprail_pay_request` now surfaces the `verifiableReceipt`.
40
+ - **fix:** a batch of correctness hardening from an adversarial multi-agent audit — require the replay
41
+ store's `isUsed`/`markUsed` as a pair (a lone one silently disabled double-spend protection); the read
42
+ methods (`planPayment`/`canAfford`/`estimateCost`) degrade instead of throwing on a malformed accept;
43
+ Sui coin pagination; Stellar custom-asset decimals; Tron native safe-integer; and more.
44
+ - **fix:** a second adversarial multi-agent conformance pass (vs the cloned x402 spec) hardened the new
45
+ surfaces — **upto cumulative caps are now merchant-proof:** the budget debits the authorized **MAX**
46
+ (a merchant that under-reports its settled `amount` can no longer loosen `maxTotal`/`maxTotalPerDenom`/
47
+ `windowTotal`); the metered actual is surfaced on `SpendRecord.settledBase`. The `upto` driver now
48
+ enforces **strict** `permitted.amount === advertised max` at verify time (an over-permit is rejected —
49
+ x402 `scheme_upto_evm` §Phase 3). `describe()` now projects the upto rail's mandatory
50
+ `extra.facilitatorAddress` (+ the exact rail's EIP-712 domain) so a discovered rail is reconstructable.
51
+ A2A conformance: a rejected proof re-challenges as `payment-rejected` (the spec status that pairs with
52
+ the retryable `input-required`, not the terminal `payment-failed`) carrying a failure receipt with
53
+ `network` + `transaction:''`. The agent guide + `paymentTools` JSDoc now document the upto rail and the
54
+ 8th tool.
55
+ - **fix:** a third verification pass (regression-focused) caught two **doc copy-paste bugs** that would
56
+ brick a user's code — the `upto` seller + Tier-2 receipt examples used the pre-v2 `{ privateKey }`
57
+ wallet field (which throws `WrongFamilyError`) instead of the unified `{ key }`. Also: the A2A
58
+ terminal-`failed` receipt now carries the attempted `network`; the A2A `fulfill` doc/example returns a
59
+ structurally valid artifact (`{ name, parts }`); the MCP config docs + registry `server.json` now list
60
+ `upto`; and a **regression test** locks the merchant-proof cumulative leash (a sequence of
61
+ under-reporting `upto` payments is refused by `maxTotal`/`maxTotalPerDenom`/`windowTotal`).
62
+ - **fix:** a fourth, end-to-end pass (every flow traced hop-by-hop + a clean-room standards sign-off,
63
+ which came back **CONFORMANT on all four wire formats**, and a live MCP-pays-the-`upto`-rail proof)
64
+ found only doc-accuracy gaps + one low edge case: A2A failure receipts now attribute `network` from
65
+ the buyer's submitted payload (covering multi-network gates + v1-flat exact payloads), and the whole
66
+ docs-site is updated to the **8-tool** surface (the new `piprail_verify_receipt`) with the 2.10.0
67
+ receipts/upto/A2A APIs documented in the reference. No SDK behavior change beyond the A2A edge fix.
68
+
7
69
  ## [2.9.0] — 2026-06-19 — Cross-token grand total · payment-count caps · durable budget · richer spend observability
8
70
 
9
71
  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;