@piprail/sdk 2.6.0 → 2.7.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,39 @@ 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.7.0] — 2026-06-19 — symmetric payment notifications (`onFailed`: both sides notified on success AND failure)
8
+
9
+ Additive and backward-compatible — defaults and the zero-config 402 stay byte-identical; omit the new
10
+ options and behaviour is unchanged.
11
+
12
+ - **New merchant hook `onFailed(failure)`** on `createPaymentGate` / `requirePayment` — the mirror of
13
+ `onPaid`. It fires whenever a SUBMITTED proof is rejected (a `kind:'invalid'` verdict: wrong amount,
14
+ expired, replayed, unknown asset, …), so the merchant is notified of a failure exactly as the buyer's
15
+ client already is. The new `FailedPayment` it receives carries the SAME machine `code` (a
16
+ `VerifyErrorCode`) the buyer gets — one consistent reason on both sides.
17
+ - **`onFailedError(error, failure)`** and **`awaitOnFailed`** mirror `onPaidError` / `awaitOnPaid`: the
18
+ hook is fully isolated (a sync throw or async rejection routes to `onFailedError` — it can never break
19
+ the request or escape as an unhandledRejection), and `awaitOnFailed` runs it before the 402 returns.
20
+ - **Exported `FailedPayment` type** — `{ code, detail, transient }`. The `transient` flag is `true` for
21
+ `tx_not_found` / `insufficient_confirmations` (the proof may still be settling and the buyer's client
22
+ auto-retries — alert on `!transient` to avoid false alarms on RPC lag), `false` for a definitive
23
+ rejection (wrong amount, expired, replayed, bad signature, …). Nothing is hidden — every rejected
24
+ attempt still fires `onFailed`.
25
+ - **Buyer side:** the `payment-failed` client event now also carries structured `code` / `detail` (the
26
+ server's parsed reason) alongside the existing human `reason`. It now ALSO fires on a **pre-send
27
+ decline** (policy / `onBeforePay` / no settleable rail) — previously those only threw — so a consumer
28
+ watching `onEvent` learns of EVERY failure, not just server rejections (the typed throw is unchanged).
29
+ - **Fires only on a real rejection** — not on a normal first-request `challenge` (no proof yet), and not
30
+ on a transient/settlement error that throws (an RPC blip, or a 5xx `SettlementError`): those aren't
31
+ payment verdicts. A failure the merchant never receives a request for (insufficient funds, policy
32
+ decline, abandonment) reaches only the buyer — a backendless gate is passive by design.
33
+ - **Examples + tests:** a complete `examples/payment-system/` reference (merchant + buyer — both sides,
34
+ success + failure → a SQLite ledger with a free `/ledger` dashboard); `onFailed` added to the Express +
35
+ Next.js examples. Coverage spans every `VerifyErrorCode`, both rails, the `requirePayment` middleware,
36
+ the `transient` flag, concurrency, replay-after-success, that a thrown `SettlementError`/transient error
37
+ does NOT fire `onFailed`, buyer declines, and a full client↔gate HTTP loop proving both sides receive
38
+ the same `code`.
39
+
7
40
  ## [2.6.0] — 2026-06-18 — keyless-gasless `exact` on 7 more EVM mainnets (6 → 13 chains)
8
41
 
9
42
  Additive and backward-compatible — defaults and the zero-config 402 stay byte-identical; this only
package/README.md CHANGED
@@ -24,7 +24,7 @@ app.get('/report',
24
24
  )
25
25
  ```
26
26
 
27
- That route now costs **0.05 USDC on Base**, paid straight to your wallet. One parameter picks the chain. → [Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)
27
+ That route now costs **0.05 USDC on Base**, paid straight to your wallet. One parameter picks the chain. Add `onPaid` / `onFailed` to be notified the moment a payment settles or is rejected — both carry the same reason, and the buyer's client is notified too. → [Accepting payments](https://docs.piprail.com/accepting-payments/require-payment-and-gate/)
28
28
 
29
29
  ## Let an agent pay for it
30
30
 
package/dist/index.cjs CHANGED
@@ -3212,7 +3212,9 @@ var PipRailClient = (_class2 = class {
3212
3212
  if (autoRoute) {
3213
3213
  const plan = await this.planFromChallenge(net, wallet, challenge, url, schemes);
3214
3214
  if (!plan.best) {
3215
- throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(_nullishCoalesce(plan.fundingHint, () => ( "No rail is settleable for this payment.")));
3215
+ const reason = _nullishCoalesce(plan.fundingHint, () => ( "No rail is settleable for this payment."));
3216
+ this.safeEmit({ kind: "payment-failed", reason });
3217
+ throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(reason);
3216
3218
  }
3217
3219
  accept = plan.best.accept;
3218
3220
  quote = plan.best.quote;
@@ -3502,10 +3504,10 @@ var PipRailClient = (_class2 = class {
3502
3504
  * TERMINAL expiry/approval decline it must not retry) without parsing prose. */
3503
3505
  async authorize(quote) {
3504
3506
  if (!quote.withinPolicy) {
3505
- throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(
3506
- `Payment refused by policy: ${_nullishCoalesce(quote.policyReason, () => ( "not allowed"))}`,
3507
- { reasonCode: reasonCodeForPolicy(quote.policyCode) }
3508
- );
3507
+ const reason = `Payment refused by policy: ${_nullishCoalesce(quote.policyReason, () => ( "not allowed"))}`;
3508
+ const reasonCode = reasonCodeForPolicy(quote.policyCode);
3509
+ this.safeEmit({ kind: "payment-failed", reason, code: reasonCode });
3510
+ throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(reason, { reasonCode });
3509
3511
  }
3510
3512
  const hook = this.opts.onBeforePay;
3511
3513
  if (!hook) return;
@@ -3513,16 +3515,14 @@ var PipRailClient = (_class2 = class {
3513
3515
  try {
3514
3516
  approved = await hook(quote);
3515
3517
  } catch (err) {
3516
- throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)("onBeforePay threw \u2014 refusing to pay.", {
3517
- cause: err,
3518
- reasonCode: "APPROVAL"
3519
- });
3518
+ const reason = "onBeforePay threw \u2014 refusing to pay.";
3519
+ this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3520
+ throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(reason, { cause: err, reasonCode: "APPROVAL" });
3520
3521
  }
3521
3522
  if (!approved) {
3522
- throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(
3523
- `onBeforePay declined ${quote.amountFormatted} ${_nullishCoalesce(quote.symbol, () => ( ""))}`.trimEnd() + ` on ${quote.network}.`,
3524
- { reasonCode: "APPROVAL" }
3525
- );
3523
+ const reason = `onBeforePay declined ${quote.amountFormatted} ${_nullishCoalesce(quote.symbol, () => ( ""))}`.trimEnd() + ` on ${quote.network}.`;
3524
+ this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3525
+ throw new (0, _chunkJG6KRAW6cjs.PaymentDeclinedError)(reason, { reasonCode: "APPROVAL" });
3526
3526
  }
3527
3527
  }
3528
3528
  /** Record a settled payment in the ledger (true decimals for the running total). */
@@ -3617,7 +3617,8 @@ var PipRailClient = (_class2 = class {
3617
3617
  const unconfirmedNote = confirmed ? "" : " (broadcast but NOT locally confirmed \u2014 it may still have settled on-chain)";
3618
3618
  this.safeEmit({
3619
3619
  kind: "payment-failed",
3620
- reason: `server returned 402 after broadcasting payment ${ref}${unconfirmedNote} (${why})`
3620
+ reason: `server returned 402 after broadcasting payment ${ref}${unconfirmedNote} (${why})`,
3621
+ ...lastReason ? { code: lastReason.error, detail: lastReason.detail } : {}
3621
3622
  });
3622
3623
  throw new (0, _chunkJG6KRAW6cjs.MaxRetriesExceededError)(
3623
3624
  `Server still returned 402 after ${attempts} attempt(s) with on-chain proof ref=${ref}${unconfirmedNote}. Last server rejection: ${why}. Re-verify or re-submit ref=${ref} before retrying \u2014 never re-pay (it would double-spend).`,
@@ -3652,7 +3653,7 @@ var PipRailClient = (_class2 = class {
3652
3653
  const headers = new Headers(_optionalChain([init, 'optionalAccess', _49 => _49.headers]));
3653
3654
  headers.set(HEADER_SIGNATURE, buildExactSignatureHeader({ accepted, payload }));
3654
3655
  const rejectDefinitive = (why2) => {
3655
- this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})` });
3656
+ this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})`, code: why2 });
3656
3657
  throw new (0, _chunkJG6KRAW6cjs.MaxRetriesExceededError)(
3657
3658
  `exact: the facilitator rejected the payment (${why2}). Fix the cause, then re-present the SAME signed authorization (nonce=${nonce}) \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
3658
3659
  { ref: nonce }
@@ -3707,7 +3708,8 @@ var PipRailClient = (_class2 = class {
3707
3708
  const why = lastReason ? `${lastReason.error}${lastReason.detail ? ` \u2014 ${lastReason.detail}` : ""}` : "server gave no reason";
3708
3709
  this.safeEmit({
3709
3710
  kind: "payment-failed",
3710
- reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`
3711
+ reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`,
3712
+ ...lastReason ? { code: lastReason.error, detail: lastReason.detail } : {}
3711
3713
  });
3712
3714
  throw new (0, _chunkJG6KRAW6cjs.MaxRetriesExceededError)(
3713
3715
  `exact: server still returned 402 after submitting the signed authorization (nonce=${nonce}). Last rejection: ${why}. Re-present the SAME authorization \u2014 do NOT re-sign a fresh nonce; verify authorizationState(${payerFrom}, ${nonce}) first. ref=${nonce}.`,
@@ -5260,6 +5262,7 @@ function normaliseExactOption(exact) {
5260
5262
  if (exact === true) return { settle: "keyless" };
5261
5263
  return exact;
5262
5264
  }
5265
+ var TRANSIENT_VERIFY_CODES = /* @__PURE__ */ new Set(["tx_not_found", "insufficient_confirmations"]);
5263
5266
  function createPaymentGate(options) {
5264
5267
  const minConfirmations = _nullishCoalesce(options.minConfirmations, () => ( 1));
5265
5268
  const maxTimeoutSeconds = _nullishCoalesce(options.maxTimeoutSeconds, () => ( 600));
@@ -5544,6 +5547,32 @@ function createPaymentGate(options) {
5544
5547
  if (options.awaitOnPaid) await fireOnPaid(paid);
5545
5548
  else void fireOnPaid(paid);
5546
5549
  }
5550
+ function reportOnFailedError(error, failure) {
5551
+ if (!options.onFailedError) return;
5552
+ try {
5553
+ options.onFailedError(error, failure);
5554
+ } catch (e38) {
5555
+ }
5556
+ }
5557
+ function fireOnFailed(failure) {
5558
+ if (!options.onFailed) return;
5559
+ let outcome;
5560
+ try {
5561
+ outcome = options.onFailed(failure);
5562
+ } catch (err) {
5563
+ reportOnFailedError(err, failure);
5564
+ return;
5565
+ }
5566
+ if (outcome != null && typeof outcome.then === "function") {
5567
+ return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
5568
+ }
5569
+ }
5570
+ async function deliverOnFailed(result) {
5571
+ const code = result.error;
5572
+ const failure = { code, detail: result.detail, transient: TRANSIENT_VERIFY_CODES.has(code) };
5573
+ if (options.awaitOnFailed) await fireOnFailed(failure);
5574
+ else void fireOnFailed(failure);
5575
+ }
5547
5576
  async function describe(resourceUrl = "") {
5548
5577
  const specs = await ready();
5549
5578
  const accepts = [];
@@ -5622,28 +5651,28 @@ function createPaymentGate(options) {
5622
5651
  nonce = [exact.payload.transaction, exact.payload.senderAuth].map((t) => {
5623
5652
  try {
5624
5653
  return Buffer.from(t, "base64").toString("base64");
5625
- } catch (e38) {
5654
+ } catch (e39) {
5626
5655
  return t;
5627
5656
  }
5628
5657
  }).join("|");
5629
5658
  } else if ("transaction" in exact.payload) {
5630
5659
  try {
5631
5660
  nonce = Buffer.from(exact.payload.transaction, "base64").toString("base64");
5632
- } catch (e39) {
5661
+ } catch (e40) {
5633
5662
  nonce = exact.payload.transaction;
5634
5663
  }
5635
5664
  } else if ("paymentGroup" in exact.payload) {
5636
5665
  nonce = exact.payload.paymentGroup.map((t) => {
5637
5666
  try {
5638
5667
  return Buffer.from(t, "base64").toString("base64");
5639
- } catch (e40) {
5668
+ } catch (e41) {
5640
5669
  return t;
5641
5670
  }
5642
5671
  }).join("|");
5643
5672
  } else if ("signedDelegateAction" in exact.payload) {
5644
5673
  try {
5645
5674
  nonce = Buffer.from(exact.payload.signedDelegateAction, "base64").toString("base64");
5646
- } catch (e41) {
5675
+ } catch (e42) {
5647
5676
  nonce = exact.payload.signedDelegateAction;
5648
5677
  }
5649
5678
  } else if ("permit2Authorization" in exact.payload) {
@@ -5707,6 +5736,11 @@ function createPaymentGate(options) {
5707
5736
  return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
5708
5737
  }
5709
5738
  async function verify(paymentSignature) {
5739
+ const result = await resolveVerdict(paymentSignature);
5740
+ if (result.kind === "invalid") await deliverOnFailed(result);
5741
+ return result;
5742
+ }
5743
+ async function resolveVerdict(paymentSignature) {
5710
5744
  const raw = normaliseHeader(paymentSignature);
5711
5745
  if (!raw) return asChallenge();
5712
5746
  const sig = parseSignatureHeader(raw);
@@ -5784,7 +5818,7 @@ async function signBody(secret, body) {
5784
5818
  const sig = await subtle.sign("HMAC", key, enc.encode(body));
5785
5819
  const hex = Array.from(new Uint8Array(sig)).map((b) => b.toString(16).padStart(2, "0")).join("");
5786
5820
  return `sha256=${hex}`;
5787
- } catch (e42) {
5821
+ } catch (e43) {
5788
5822
  return null;
5789
5823
  }
5790
5824
  }
@@ -5845,7 +5879,7 @@ async function deliverReceipt(receipt, options) {
5845
5879
  const willRetry = !ok && retryable && attempt < maxAttempts;
5846
5880
  try {
5847
5881
  _optionalChain([onAttempt, 'optionalCall', _91 => _91({ attempt, ok, ...status !== void 0 ? { status } : {}, ...error ? { error } : {}, willRetry })]);
5848
- } catch (e43) {
5882
+ } catch (e44) {
5849
5883
  }
5850
5884
  if (ok) return { delivered: true, attempts: attempt, status };
5851
5885
  if (!willRetry) {
package/dist/index.d.cts CHANGED
@@ -5044,6 +5044,14 @@ type PipRailEvent = {
5044
5044
  } | {
5045
5045
  kind: 'payment-failed';
5046
5046
  reason: string;
5047
+ /** A machine-readable failure code when one is known. For a SERVER rejection it's the SAME
5048
+ * code the merchant's `onFailed` hook receives (a canonical {@link VerifyErrorCode} from a
5049
+ * PipRail gate, or a foreign facilitator's reason string); for a pre-send client DECLINE
5050
+ * (policy / budget / approval) it's that decline reason (e.g. `'BUDGET'`, `'APPROVAL'`).
5051
+ * Absent when no structured code was given. */
5052
+ code?: string;
5053
+ /** Human-readable detail, when present (e.g. `"Paid 40000, required 500000."`). */
5054
+ detail?: string;
5047
5055
  };
5048
5056
  /**
5049
5057
  * Wallet for the chosen chain family. **One field, every chain: `{ key }`** — the
@@ -6463,6 +6471,28 @@ interface ExactRailOption {
6463
6471
  * "just works". Force `'eip3009'` or `'permit2'` to pin one. Ignored on Solana (always SVM). */
6464
6472
  method?: 'eip3009' | 'permit2' | 'auto';
6465
6473
  }
6474
+ /**
6475
+ * The merchant-side mirror of {@link PaidReceipt}: what an {@link RequirePaymentOptions.onFailed}
6476
+ * hook receives when a SUBMITTED payment proof is REJECTED. It carries the SAME machine-readable
6477
+ * `code` the buyer's client is given for that rejection, so both sides are notified of one
6478
+ * consistent reason. (A rejection has no settlement, so — unlike a receipt — there is no tx hash
6479
+ * or settled amount to report.)
6480
+ */
6481
+ interface FailedPayment {
6482
+ /** The canonical rejection reason — the same {@link VerifyErrorCode} surfaced to the buyer
6483
+ * (e.g. `amount_too_low`, `payment_expired`, `tx_already_used`, `transfer_not_found`). */
6484
+ code: VerifyErrorCode;
6485
+ /** Human-readable detail, e.g. `"Paid 40000, required 500000."`. */
6486
+ detail: string;
6487
+ /**
6488
+ * `true` for a **transient** rejection (`tx_not_found` / `insufficient_confirmations`): the proof
6489
+ * may still be settling and the buyer's client **retries automatically** — you'll get `onPaid` if
6490
+ * it then succeeds. `false` for a **definitive** rejection the buyer must fix (wrong amount,
6491
+ * expired, replayed, bad signature, wrong recipient). Alert on `!transient` to avoid false alarms
6492
+ * on normal RPC lag; nothing is hidden — every rejected attempt still fires `onFailed`.
6493
+ */
6494
+ transient: boolean;
6495
+ }
6466
6496
  interface RequirePaymentOptions {
6467
6497
  /**
6468
6498
  * Single-chain form: which chain to accept payment on. EVM ('bnb'|'base'|…),
@@ -6535,6 +6565,37 @@ interface RequirePaymentOptions {
6535
6565
  * via `onPaidError`; it never turns a settled payment into a 402.
6536
6566
  */
6537
6567
  awaitOnPaid?: boolean;
6568
+ /**
6569
+ * The merchant-side mirror of `onPaid`: fired when a SUBMITTED payment proof is REJECTED — a
6570
+ * `kind:'invalid'` verdict (wrong amount, expired, replayed, unknown asset, …). Receives a
6571
+ * {@link FailedPayment} carrying the SAME machine `code` the buyer's client is given, so the
6572
+ * merchant and the buyer are notified of the same failure with the same reason.
6573
+ *
6574
+ * Fires ONLY on a rejected attempt — NOT on a normal first-request 402 `challenge` (no proof
6575
+ * yet), and NOT on a transient/settlement error that throws (an RPC blip, or a 5xx
6576
+ * `SettlementError`): those aren't payment verdicts. Like `onPaid`, it may be **sync or async**
6577
+ * and is fully isolated — a throw OR a rejected promise is caught and routed to `onFailedError`,
6578
+ * so it can never break the request or crash the process. Fire-and-forget by default; set
6579
+ * `awaitOnFailed` to run it before the 402 is returned.
6580
+ *
6581
+ * NOTE: a failure the merchant never receives a request for — the buyer can't afford it, an
6582
+ * `onBeforePay`/`policy` declines it, or the buyer abandons before paying — cannot reach a
6583
+ * backendless gate (only the buyer's client sees it). `onFailed` covers every rejection that
6584
+ * DOES reach the gate.
6585
+ */
6586
+ onFailed?: (failure: FailedPayment) => void | Promise<void>;
6587
+ /**
6588
+ * Observe a failure inside `onFailed` (sync throw or async rejection) — the mirror of
6589
+ * `onPaidError`. Without it, a throwing `onFailed` is swallowed silently. Its own throws are
6590
+ * also swallowed (it can never break a request).
6591
+ */
6592
+ onFailedError?: (error: unknown, failure: FailedPayment) => void;
6593
+ /**
6594
+ * Await `onFailed` before the 402 rejection is returned (mirror of `awaitOnPaid`), so
6595
+ * "failure recorded" is guaranteed before the caller is told. Default `false` (fire-and-forget).
6596
+ * A rejection inside the hook is still isolated via `onFailedError`.
6597
+ */
6598
+ awaitOnFailed?: boolean;
6538
6599
  /**
6539
6600
  * ALSO advertise a standard x402 `exact` rail so any standard x402 client can pay this
6540
6601
  * gate — opt-in, EVM (EIP-3009/Permit2) + Solana (SVM). See {@link ExactRailOption}.
@@ -6639,15 +6700,6 @@ interface PaymentGate {
6639
6700
  */
6640
6701
  landingPage(challenge: X402Challenge): string;
6641
6702
  }
6642
- /**
6643
- * Framework-agnostic core. Build one gate per gated resource and reuse it
6644
- * — its in-memory used-tx set is what stops the same proof being redeemed
6645
- * twice. Wrap it for Express with `requirePayment`, or call it directly
6646
- * from Hono / Fastify / Adonis / Workers / etc.
6647
- *
6648
- * The chain's driver is resolved lazily on first `challenge()`/`verify()`,
6649
- * which is what lets Solana (and future families) auto-mount with no setup.
6650
- */
6651
6703
  declare function createPaymentGate(options: RequirePaymentOptions): PaymentGate;
6652
6704
  interface ExpressLikeRequest {
6653
6705
  headers: Record<string, string | string[] | undefined>;
@@ -7527,4 +7579,4 @@ declare const PERMIT2_WITNESS_TYPES: {
7527
7579
  */
7528
7580
  declare function renderLandingPage(sd: SelfDescription): string;
7529
7581
 
7530
- export { type AcceptOption, type AddressId, type AgentTool, type AlgorandToken, type AptosToken, type AssetId, BRAND, type BazaarExtension, type BuildExactParams, CHAINS, type Caip2, type ChainFamily, type ChainInput, type ChainName, type ChainPreset, type ChainSelector, type ChallengeTriage, type ChallengeVerdict, type ConfirmInfo, ConfirmationTimeoutError, type CostEstimate, DIRECTORY_INFO, type DeclineReasonCode, type DeliverAttempt, type DeliverReceiptOptions, type DeliverResult, type DirectoryInfo, type DiscoverOptions, type DiscoveredRail, type DiscoveredResource, type DiscoveryDescriptor, type DiscoverySigner, type DiscoverySort, type DiscoverySource, type DomainClaim, type DomainVerification, EIP3009_TYPES, EXACT_NETWORK_SLUGS, type EvmToken, type ExactAccept, type ExactAuthorization, type ExactAuthorizationWire, type ExactPaymentPayload, type ExactPaymentPayloadAny, type ExactRailOption, type ExpressLikeMiddleware, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type FacilitatorConfig, type FacilitatorPaymentRequirements, type FacilitatorSupportedKind, GENERATOR, HEADER_REQUIRED, HEADER_RESPONSE, HEADER_RESPONSE_V1, HEADER_SIGNATURE, HEADER_SIGNATURE_V1, InsufficientFundsError, InvalidEnvelopeError, KNOWN_FACILITATORS, type KnownFacilitator, type ListingVisibility, type ManifestInput, MaxRetriesExceededError, MissingDriverError, MultiChainPayer, type MultiChainPayerOptions, type NearToken, NoCompatibleAcceptError, NonReplayableBodyError, type OpenApiDocument, type OpenApiOperation, PERMIT2_ADDRESS, PERMIT2_PROXY_CHAIN_IDS, PERMIT2_WITNESS_TYPES, PIPRAIL_AGENT_GUIDE, POWERED_BY, type PaidReceipt, type ParsedExactPayment, type PayBlocker, type PayOption, type PayWarning, type PayingClient, PaymentDeclinedError, type PaymentDriver, type PaymentGate, type PaymentIntent, type PaymentPlan, type PaymentPolicy, type PaymentRail, type PaymentScheme, PaymentTimeoutError, type Permit2Authorization, type Permit2PaymentPayload, PipRailClient, type PipRailClientOptions, type PipRailCostQuote, PipRailError, type PipRailEvent, type PipRailQuote, type PolicyDecision, type PolicyDenyCode, REGISTER_ATTRIBUTION, RecipientNotReadyError, type RecipientReason, type RegisterInput, type RegisterOptions, type RegisterOutcome, type RequirePaymentOptions, type ResolveOptions, type ResolvedChain, type ResolvedNetwork, type ResolvedToken, type ResourceDescription, type SearchOpenIndexesOptions, type SelfDescribeEndpoint, type SelfDescribeRail, type SelfDescription, type SessionBudget, type SettleOutcome, type SettleViaFacilitatorInput, SettlementError, type SolanaToken, type SpendAssetTotal, type SpendRecord, type SpendRemaining, type SpendSummary, type StellarToken, type SuiToken, type TokenInfo, type TokenInput, type TonToken, type ToolAnnotations, type TronToken, UnknownTokenError, UnsupportedNetworkError, UnsupportedSchemeError, type VerifyErrorCode, type VerifyPaymentResult, type VerifyResult, type WalletBalance, type WalletHandle, type WalletInput, WalletRequiredError, type WellKnownX402, WrongChainError, WrongFamilyError, type X402AcceptEntry, type X402AnyAccept, type X402Challenge, type X402DnsRecord, type X402ExactAcceptEntry, type X402InvalidBody, type X402PaymentSignature, type X402Receipt, type X402ResourceObject, X402_EXACT_PERMIT2_PROXY, type XrplToken, agentGuide, appendAttribution, appendKeywords, buildBazaarExtension, buildChallengeHeader, buildEndpointInfo, buildExactAuthorization, buildExactSignatureHeader, buildOpenApi, buildReceiptHeader, buildSelfDescription, buildSignatureHeader, buildWellKnownX402, buildX402DnsTxt, chainIdForExactNetwork, claim402IndexDomain, classifyChallenge, createPaymentGate, decorateOutcome, deliverReceipt, describeChallenge, discoveryHeaders, eip3009Abi, encodeXPaymentHeader, evaluatePolicy, explainDecline, facilitatorCoverage, fetchAcross, firstKeylessFacilitator, formatSpendReport, getDirectoryInfo, isPermit2ProxyChain, knownFacilitatorsFor, normalizeNetwork, parseChallenge, parseExactPaymentHeader, parseExactRequirements, parseFacilitatorSupported, parseReceipt, parseSettleResponse, parseSignatureHeader, paymentTools, pickAccept, planAcross, rankResources, readExactDomain, register402Index, registerDriver, registerX402Scan, renderLandingPage, requirePayment, resolveChain, scoreResource, searchOpenIndexes, settleViaFacilitator, summarizePlan, toInsufficientFundsError, toInvalidBody, verify402IndexDomain };
7582
+ export { type AcceptOption, type AddressId, type AgentTool, type AlgorandToken, type AptosToken, type AssetId, BRAND, type BazaarExtension, type BuildExactParams, CHAINS, type Caip2, type ChainFamily, type ChainInput, type ChainName, type ChainPreset, type ChainSelector, type ChallengeTriage, type ChallengeVerdict, type ConfirmInfo, ConfirmationTimeoutError, type CostEstimate, DIRECTORY_INFO, type DeclineReasonCode, type DeliverAttempt, type DeliverReceiptOptions, type DeliverResult, type DirectoryInfo, type DiscoverOptions, type DiscoveredRail, type DiscoveredResource, type DiscoveryDescriptor, type DiscoverySigner, type DiscoverySort, type DiscoverySource, type DomainClaim, type DomainVerification, EIP3009_TYPES, EXACT_NETWORK_SLUGS, type EvmToken, type ExactAccept, type ExactAuthorization, type ExactAuthorizationWire, type ExactPaymentPayload, type ExactPaymentPayloadAny, type ExactRailOption, type ExpressLikeMiddleware, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type FacilitatorConfig, type FacilitatorPaymentRequirements, type FacilitatorSupportedKind, type FailedPayment, GENERATOR, HEADER_REQUIRED, HEADER_RESPONSE, HEADER_RESPONSE_V1, HEADER_SIGNATURE, HEADER_SIGNATURE_V1, InsufficientFundsError, InvalidEnvelopeError, KNOWN_FACILITATORS, type KnownFacilitator, type ListingVisibility, type ManifestInput, MaxRetriesExceededError, MissingDriverError, MultiChainPayer, type MultiChainPayerOptions, type NearToken, NoCompatibleAcceptError, NonReplayableBodyError, type OpenApiDocument, type OpenApiOperation, PERMIT2_ADDRESS, PERMIT2_PROXY_CHAIN_IDS, PERMIT2_WITNESS_TYPES, PIPRAIL_AGENT_GUIDE, POWERED_BY, type PaidReceipt, type ParsedExactPayment, type PayBlocker, type PayOption, type PayWarning, type PayingClient, PaymentDeclinedError, type PaymentDriver, type PaymentGate, type PaymentIntent, type PaymentPlan, type PaymentPolicy, type PaymentRail, type PaymentScheme, PaymentTimeoutError, type Permit2Authorization, type Permit2PaymentPayload, PipRailClient, type PipRailClientOptions, type PipRailCostQuote, PipRailError, type PipRailEvent, type PipRailQuote, type PolicyDecision, type PolicyDenyCode, REGISTER_ATTRIBUTION, RecipientNotReadyError, type RecipientReason, type RegisterInput, type RegisterOptions, type RegisterOutcome, type RequirePaymentOptions, type ResolveOptions, type ResolvedChain, type ResolvedNetwork, type ResolvedToken, type ResourceDescription, type SearchOpenIndexesOptions, type SelfDescribeEndpoint, type SelfDescribeRail, type SelfDescription, type SessionBudget, type SettleOutcome, type SettleViaFacilitatorInput, SettlementError, type SolanaToken, type SpendAssetTotal, type SpendRecord, type SpendRemaining, type SpendSummary, type StellarToken, type SuiToken, type TokenInfo, type TokenInput, type TonToken, type ToolAnnotations, type TronToken, UnknownTokenError, UnsupportedNetworkError, UnsupportedSchemeError, type VerifyErrorCode, type VerifyPaymentResult, type VerifyResult, type WalletBalance, type WalletHandle, type WalletInput, WalletRequiredError, type WellKnownX402, WrongChainError, WrongFamilyError, type X402AcceptEntry, type X402AnyAccept, type X402Challenge, type X402DnsRecord, type X402ExactAcceptEntry, type X402InvalidBody, type X402PaymentSignature, type X402Receipt, type X402ResourceObject, X402_EXACT_PERMIT2_PROXY, type XrplToken, agentGuide, appendAttribution, appendKeywords, buildBazaarExtension, buildChallengeHeader, buildEndpointInfo, buildExactAuthorization, buildExactSignatureHeader, buildOpenApi, buildReceiptHeader, buildSelfDescription, buildSignatureHeader, buildWellKnownX402, buildX402DnsTxt, chainIdForExactNetwork, claim402IndexDomain, classifyChallenge, createPaymentGate, decorateOutcome, deliverReceipt, describeChallenge, discoveryHeaders, eip3009Abi, encodeXPaymentHeader, evaluatePolicy, explainDecline, facilitatorCoverage, fetchAcross, firstKeylessFacilitator, formatSpendReport, getDirectoryInfo, isPermit2ProxyChain, knownFacilitatorsFor, normalizeNetwork, parseChallenge, parseExactPaymentHeader, parseExactRequirements, parseFacilitatorSupported, parseReceipt, parseSettleResponse, parseSignatureHeader, paymentTools, pickAccept, planAcross, rankResources, readExactDomain, register402Index, registerDriver, registerX402Scan, renderLandingPage, requirePayment, resolveChain, scoreResource, searchOpenIndexes, settleViaFacilitator, summarizePlan, toInsufficientFundsError, toInvalidBody, verify402IndexDomain };
package/dist/index.d.ts CHANGED
@@ -5044,6 +5044,14 @@ type PipRailEvent = {
5044
5044
  } | {
5045
5045
  kind: 'payment-failed';
5046
5046
  reason: string;
5047
+ /** A machine-readable failure code when one is known. For a SERVER rejection it's the SAME
5048
+ * code the merchant's `onFailed` hook receives (a canonical {@link VerifyErrorCode} from a
5049
+ * PipRail gate, or a foreign facilitator's reason string); for a pre-send client DECLINE
5050
+ * (policy / budget / approval) it's that decline reason (e.g. `'BUDGET'`, `'APPROVAL'`).
5051
+ * Absent when no structured code was given. */
5052
+ code?: string;
5053
+ /** Human-readable detail, when present (e.g. `"Paid 40000, required 500000."`). */
5054
+ detail?: string;
5047
5055
  };
5048
5056
  /**
5049
5057
  * Wallet for the chosen chain family. **One field, every chain: `{ key }`** — the
@@ -6463,6 +6471,28 @@ interface ExactRailOption {
6463
6471
  * "just works". Force `'eip3009'` or `'permit2'` to pin one. Ignored on Solana (always SVM). */
6464
6472
  method?: 'eip3009' | 'permit2' | 'auto';
6465
6473
  }
6474
+ /**
6475
+ * The merchant-side mirror of {@link PaidReceipt}: what an {@link RequirePaymentOptions.onFailed}
6476
+ * hook receives when a SUBMITTED payment proof is REJECTED. It carries the SAME machine-readable
6477
+ * `code` the buyer's client is given for that rejection, so both sides are notified of one
6478
+ * consistent reason. (A rejection has no settlement, so — unlike a receipt — there is no tx hash
6479
+ * or settled amount to report.)
6480
+ */
6481
+ interface FailedPayment {
6482
+ /** The canonical rejection reason — the same {@link VerifyErrorCode} surfaced to the buyer
6483
+ * (e.g. `amount_too_low`, `payment_expired`, `tx_already_used`, `transfer_not_found`). */
6484
+ code: VerifyErrorCode;
6485
+ /** Human-readable detail, e.g. `"Paid 40000, required 500000."`. */
6486
+ detail: string;
6487
+ /**
6488
+ * `true` for a **transient** rejection (`tx_not_found` / `insufficient_confirmations`): the proof
6489
+ * may still be settling and the buyer's client **retries automatically** — you'll get `onPaid` if
6490
+ * it then succeeds. `false` for a **definitive** rejection the buyer must fix (wrong amount,
6491
+ * expired, replayed, bad signature, wrong recipient). Alert on `!transient` to avoid false alarms
6492
+ * on normal RPC lag; nothing is hidden — every rejected attempt still fires `onFailed`.
6493
+ */
6494
+ transient: boolean;
6495
+ }
6466
6496
  interface RequirePaymentOptions {
6467
6497
  /**
6468
6498
  * Single-chain form: which chain to accept payment on. EVM ('bnb'|'base'|…),
@@ -6535,6 +6565,37 @@ interface RequirePaymentOptions {
6535
6565
  * via `onPaidError`; it never turns a settled payment into a 402.
6536
6566
  */
6537
6567
  awaitOnPaid?: boolean;
6568
+ /**
6569
+ * The merchant-side mirror of `onPaid`: fired when a SUBMITTED payment proof is REJECTED — a
6570
+ * `kind:'invalid'` verdict (wrong amount, expired, replayed, unknown asset, …). Receives a
6571
+ * {@link FailedPayment} carrying the SAME machine `code` the buyer's client is given, so the
6572
+ * merchant and the buyer are notified of the same failure with the same reason.
6573
+ *
6574
+ * Fires ONLY on a rejected attempt — NOT on a normal first-request 402 `challenge` (no proof
6575
+ * yet), and NOT on a transient/settlement error that throws (an RPC blip, or a 5xx
6576
+ * `SettlementError`): those aren't payment verdicts. Like `onPaid`, it may be **sync or async**
6577
+ * and is fully isolated — a throw OR a rejected promise is caught and routed to `onFailedError`,
6578
+ * so it can never break the request or crash the process. Fire-and-forget by default; set
6579
+ * `awaitOnFailed` to run it before the 402 is returned.
6580
+ *
6581
+ * NOTE: a failure the merchant never receives a request for — the buyer can't afford it, an
6582
+ * `onBeforePay`/`policy` declines it, or the buyer abandons before paying — cannot reach a
6583
+ * backendless gate (only the buyer's client sees it). `onFailed` covers every rejection that
6584
+ * DOES reach the gate.
6585
+ */
6586
+ onFailed?: (failure: FailedPayment) => void | Promise<void>;
6587
+ /**
6588
+ * Observe a failure inside `onFailed` (sync throw or async rejection) — the mirror of
6589
+ * `onPaidError`. Without it, a throwing `onFailed` is swallowed silently. Its own throws are
6590
+ * also swallowed (it can never break a request).
6591
+ */
6592
+ onFailedError?: (error: unknown, failure: FailedPayment) => void;
6593
+ /**
6594
+ * Await `onFailed` before the 402 rejection is returned (mirror of `awaitOnPaid`), so
6595
+ * "failure recorded" is guaranteed before the caller is told. Default `false` (fire-and-forget).
6596
+ * A rejection inside the hook is still isolated via `onFailedError`.
6597
+ */
6598
+ awaitOnFailed?: boolean;
6538
6599
  /**
6539
6600
  * ALSO advertise a standard x402 `exact` rail so any standard x402 client can pay this
6540
6601
  * gate — opt-in, EVM (EIP-3009/Permit2) + Solana (SVM). See {@link ExactRailOption}.
@@ -6639,15 +6700,6 @@ interface PaymentGate {
6639
6700
  */
6640
6701
  landingPage(challenge: X402Challenge): string;
6641
6702
  }
6642
- /**
6643
- * Framework-agnostic core. Build one gate per gated resource and reuse it
6644
- * — its in-memory used-tx set is what stops the same proof being redeemed
6645
- * twice. Wrap it for Express with `requirePayment`, or call it directly
6646
- * from Hono / Fastify / Adonis / Workers / etc.
6647
- *
6648
- * The chain's driver is resolved lazily on first `challenge()`/`verify()`,
6649
- * which is what lets Solana (and future families) auto-mount with no setup.
6650
- */
6651
6703
  declare function createPaymentGate(options: RequirePaymentOptions): PaymentGate;
6652
6704
  interface ExpressLikeRequest {
6653
6705
  headers: Record<string, string | string[] | undefined>;
@@ -7527,4 +7579,4 @@ declare const PERMIT2_WITNESS_TYPES: {
7527
7579
  */
7528
7580
  declare function renderLandingPage(sd: SelfDescription): string;
7529
7581
 
7530
- export { type AcceptOption, type AddressId, type AgentTool, type AlgorandToken, type AptosToken, type AssetId, BRAND, type BazaarExtension, type BuildExactParams, CHAINS, type Caip2, type ChainFamily, type ChainInput, type ChainName, type ChainPreset, type ChainSelector, type ChallengeTriage, type ChallengeVerdict, type ConfirmInfo, ConfirmationTimeoutError, type CostEstimate, DIRECTORY_INFO, type DeclineReasonCode, type DeliverAttempt, type DeliverReceiptOptions, type DeliverResult, type DirectoryInfo, type DiscoverOptions, type DiscoveredRail, type DiscoveredResource, type DiscoveryDescriptor, type DiscoverySigner, type DiscoverySort, type DiscoverySource, type DomainClaim, type DomainVerification, EIP3009_TYPES, EXACT_NETWORK_SLUGS, type EvmToken, type ExactAccept, type ExactAuthorization, type ExactAuthorizationWire, type ExactPaymentPayload, type ExactPaymentPayloadAny, type ExactRailOption, type ExpressLikeMiddleware, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type FacilitatorConfig, type FacilitatorPaymentRequirements, type FacilitatorSupportedKind, GENERATOR, HEADER_REQUIRED, HEADER_RESPONSE, HEADER_RESPONSE_V1, HEADER_SIGNATURE, HEADER_SIGNATURE_V1, InsufficientFundsError, InvalidEnvelopeError, KNOWN_FACILITATORS, type KnownFacilitator, type ListingVisibility, type ManifestInput, MaxRetriesExceededError, MissingDriverError, MultiChainPayer, type MultiChainPayerOptions, type NearToken, NoCompatibleAcceptError, NonReplayableBodyError, type OpenApiDocument, type OpenApiOperation, PERMIT2_ADDRESS, PERMIT2_PROXY_CHAIN_IDS, PERMIT2_WITNESS_TYPES, PIPRAIL_AGENT_GUIDE, POWERED_BY, type PaidReceipt, type ParsedExactPayment, type PayBlocker, type PayOption, type PayWarning, type PayingClient, PaymentDeclinedError, type PaymentDriver, type PaymentGate, type PaymentIntent, type PaymentPlan, type PaymentPolicy, type PaymentRail, type PaymentScheme, PaymentTimeoutError, type Permit2Authorization, type Permit2PaymentPayload, PipRailClient, type PipRailClientOptions, type PipRailCostQuote, PipRailError, type PipRailEvent, type PipRailQuote, type PolicyDecision, type PolicyDenyCode, REGISTER_ATTRIBUTION, RecipientNotReadyError, type RecipientReason, type RegisterInput, type RegisterOptions, type RegisterOutcome, type RequirePaymentOptions, type ResolveOptions, type ResolvedChain, type ResolvedNetwork, type ResolvedToken, type ResourceDescription, type SearchOpenIndexesOptions, type SelfDescribeEndpoint, type SelfDescribeRail, type SelfDescription, type SessionBudget, type SettleOutcome, type SettleViaFacilitatorInput, SettlementError, type SolanaToken, type SpendAssetTotal, type SpendRecord, type SpendRemaining, type SpendSummary, type StellarToken, type SuiToken, type TokenInfo, type TokenInput, type TonToken, type ToolAnnotations, type TronToken, UnknownTokenError, UnsupportedNetworkError, UnsupportedSchemeError, type VerifyErrorCode, type VerifyPaymentResult, type VerifyResult, type WalletBalance, type WalletHandle, type WalletInput, WalletRequiredError, type WellKnownX402, WrongChainError, WrongFamilyError, type X402AcceptEntry, type X402AnyAccept, type X402Challenge, type X402DnsRecord, type X402ExactAcceptEntry, type X402InvalidBody, type X402PaymentSignature, type X402Receipt, type X402ResourceObject, X402_EXACT_PERMIT2_PROXY, type XrplToken, agentGuide, appendAttribution, appendKeywords, buildBazaarExtension, buildChallengeHeader, buildEndpointInfo, buildExactAuthorization, buildExactSignatureHeader, buildOpenApi, buildReceiptHeader, buildSelfDescription, buildSignatureHeader, buildWellKnownX402, buildX402DnsTxt, chainIdForExactNetwork, claim402IndexDomain, classifyChallenge, createPaymentGate, decorateOutcome, deliverReceipt, describeChallenge, discoveryHeaders, eip3009Abi, encodeXPaymentHeader, evaluatePolicy, explainDecline, facilitatorCoverage, fetchAcross, firstKeylessFacilitator, formatSpendReport, getDirectoryInfo, isPermit2ProxyChain, knownFacilitatorsFor, normalizeNetwork, parseChallenge, parseExactPaymentHeader, parseExactRequirements, parseFacilitatorSupported, parseReceipt, parseSettleResponse, parseSignatureHeader, paymentTools, pickAccept, planAcross, rankResources, readExactDomain, register402Index, registerDriver, registerX402Scan, renderLandingPage, requirePayment, resolveChain, scoreResource, searchOpenIndexes, settleViaFacilitator, summarizePlan, toInsufficientFundsError, toInvalidBody, verify402IndexDomain };
7582
+ export { type AcceptOption, type AddressId, type AgentTool, type AlgorandToken, type AptosToken, type AssetId, BRAND, type BazaarExtension, type BuildExactParams, CHAINS, type Caip2, type ChainFamily, type ChainInput, type ChainName, type ChainPreset, type ChainSelector, type ChallengeTriage, type ChallengeVerdict, type ConfirmInfo, ConfirmationTimeoutError, type CostEstimate, DIRECTORY_INFO, type DeclineReasonCode, type DeliverAttempt, type DeliverReceiptOptions, type DeliverResult, type DirectoryInfo, type DiscoverOptions, type DiscoveredRail, type DiscoveredResource, type DiscoveryDescriptor, type DiscoverySigner, type DiscoverySort, type DiscoverySource, type DomainClaim, type DomainVerification, EIP3009_TYPES, EXACT_NETWORK_SLUGS, type EvmToken, type ExactAccept, type ExactAuthorization, type ExactAuthorizationWire, type ExactPaymentPayload, type ExactPaymentPayloadAny, type ExactRailOption, type ExpressLikeMiddleware, type ExpressLikeNext, type ExpressLikeRequest, type ExpressLikeResponse, type FacilitatorConfig, type FacilitatorPaymentRequirements, type FacilitatorSupportedKind, type FailedPayment, GENERATOR, HEADER_REQUIRED, HEADER_RESPONSE, HEADER_RESPONSE_V1, HEADER_SIGNATURE, HEADER_SIGNATURE_V1, InsufficientFundsError, InvalidEnvelopeError, KNOWN_FACILITATORS, type KnownFacilitator, type ListingVisibility, type ManifestInput, MaxRetriesExceededError, MissingDriverError, MultiChainPayer, type MultiChainPayerOptions, type NearToken, NoCompatibleAcceptError, NonReplayableBodyError, type OpenApiDocument, type OpenApiOperation, PERMIT2_ADDRESS, PERMIT2_PROXY_CHAIN_IDS, PERMIT2_WITNESS_TYPES, PIPRAIL_AGENT_GUIDE, POWERED_BY, type PaidReceipt, type ParsedExactPayment, type PayBlocker, type PayOption, type PayWarning, type PayingClient, PaymentDeclinedError, type PaymentDriver, type PaymentGate, type PaymentIntent, type PaymentPlan, type PaymentPolicy, type PaymentRail, type PaymentScheme, PaymentTimeoutError, type Permit2Authorization, type Permit2PaymentPayload, PipRailClient, type PipRailClientOptions, type PipRailCostQuote, PipRailError, type PipRailEvent, type PipRailQuote, type PolicyDecision, type PolicyDenyCode, REGISTER_ATTRIBUTION, RecipientNotReadyError, type RecipientReason, type RegisterInput, type RegisterOptions, type RegisterOutcome, type RequirePaymentOptions, type ResolveOptions, type ResolvedChain, type ResolvedNetwork, type ResolvedToken, type ResourceDescription, type SearchOpenIndexesOptions, type SelfDescribeEndpoint, type SelfDescribeRail, type SelfDescription, type SessionBudget, type SettleOutcome, type SettleViaFacilitatorInput, SettlementError, type SolanaToken, type SpendAssetTotal, type SpendRecord, type SpendRemaining, type SpendSummary, type StellarToken, type SuiToken, type TokenInfo, type TokenInput, type TonToken, type ToolAnnotations, type TronToken, UnknownTokenError, UnsupportedNetworkError, UnsupportedSchemeError, type VerifyErrorCode, type VerifyPaymentResult, type VerifyResult, type WalletBalance, type WalletHandle, type WalletInput, WalletRequiredError, type WellKnownX402, WrongChainError, WrongFamilyError, type X402AcceptEntry, type X402AnyAccept, type X402Challenge, type X402DnsRecord, type X402ExactAcceptEntry, type X402InvalidBody, type X402PaymentSignature, type X402Receipt, type X402ResourceObject, X402_EXACT_PERMIT2_PROXY, type XrplToken, agentGuide, appendAttribution, appendKeywords, buildBazaarExtension, buildChallengeHeader, buildEndpointInfo, buildExactAuthorization, buildExactSignatureHeader, buildOpenApi, buildReceiptHeader, buildSelfDescription, buildSignatureHeader, buildWellKnownX402, buildX402DnsTxt, chainIdForExactNetwork, claim402IndexDomain, classifyChallenge, createPaymentGate, decorateOutcome, deliverReceipt, describeChallenge, discoveryHeaders, eip3009Abi, encodeXPaymentHeader, evaluatePolicy, explainDecline, facilitatorCoverage, fetchAcross, firstKeylessFacilitator, formatSpendReport, getDirectoryInfo, isPermit2ProxyChain, knownFacilitatorsFor, normalizeNetwork, parseChallenge, parseExactPaymentHeader, parseExactRequirements, parseFacilitatorSupported, parseReceipt, parseSettleResponse, parseSignatureHeader, paymentTools, pickAccept, planAcross, rankResources, readExactDomain, register402Index, registerDriver, registerX402Scan, renderLandingPage, requirePayment, resolveChain, scoreResource, searchOpenIndexes, settleViaFacilitator, summarizePlan, toInsufficientFundsError, toInvalidBody, verify402IndexDomain };
package/dist/index.js CHANGED
@@ -3212,7 +3212,9 @@ var PipRailClient = class {
3212
3212
  if (autoRoute) {
3213
3213
  const plan = await this.planFromChallenge(net, wallet, challenge, url, schemes);
3214
3214
  if (!plan.best) {
3215
- throw new PaymentDeclinedError(plan.fundingHint ?? "No rail is settleable for this payment.");
3215
+ const reason = plan.fundingHint ?? "No rail is settleable for this payment.";
3216
+ this.safeEmit({ kind: "payment-failed", reason });
3217
+ throw new PaymentDeclinedError(reason);
3216
3218
  }
3217
3219
  accept = plan.best.accept;
3218
3220
  quote = plan.best.quote;
@@ -3502,10 +3504,10 @@ var PipRailClient = class {
3502
3504
  * TERMINAL expiry/approval decline it must not retry) without parsing prose. */
3503
3505
  async authorize(quote) {
3504
3506
  if (!quote.withinPolicy) {
3505
- throw new PaymentDeclinedError(
3506
- `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`,
3507
- { reasonCode: reasonCodeForPolicy(quote.policyCode) }
3508
- );
3507
+ const reason = `Payment refused by policy: ${quote.policyReason ?? "not allowed"}`;
3508
+ const reasonCode = reasonCodeForPolicy(quote.policyCode);
3509
+ this.safeEmit({ kind: "payment-failed", reason, code: reasonCode });
3510
+ throw new PaymentDeclinedError(reason, { reasonCode });
3509
3511
  }
3510
3512
  const hook = this.opts.onBeforePay;
3511
3513
  if (!hook) return;
@@ -3513,16 +3515,14 @@ var PipRailClient = class {
3513
3515
  try {
3514
3516
  approved = await hook(quote);
3515
3517
  } catch (err) {
3516
- throw new PaymentDeclinedError("onBeforePay threw \u2014 refusing to pay.", {
3517
- cause: err,
3518
- reasonCode: "APPROVAL"
3519
- });
3518
+ const reason = "onBeforePay threw \u2014 refusing to pay.";
3519
+ this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3520
+ throw new PaymentDeclinedError(reason, { cause: err, reasonCode: "APPROVAL" });
3520
3521
  }
3521
3522
  if (!approved) {
3522
- throw new PaymentDeclinedError(
3523
- `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`,
3524
- { reasonCode: "APPROVAL" }
3525
- );
3523
+ const reason = `onBeforePay declined ${quote.amountFormatted} ${quote.symbol ?? ""}`.trimEnd() + ` on ${quote.network}.`;
3524
+ this.safeEmit({ kind: "payment-failed", reason, code: "APPROVAL" });
3525
+ throw new PaymentDeclinedError(reason, { reasonCode: "APPROVAL" });
3526
3526
  }
3527
3527
  }
3528
3528
  /** Record a settled payment in the ledger (true decimals for the running total). */
@@ -3617,7 +3617,8 @@ var PipRailClient = class {
3617
3617
  const unconfirmedNote = confirmed ? "" : " (broadcast but NOT locally confirmed \u2014 it may still have settled on-chain)";
3618
3618
  this.safeEmit({
3619
3619
  kind: "payment-failed",
3620
- reason: `server returned 402 after broadcasting payment ${ref}${unconfirmedNote} (${why})`
3620
+ reason: `server returned 402 after broadcasting payment ${ref}${unconfirmedNote} (${why})`,
3621
+ ...lastReason ? { code: lastReason.error, detail: lastReason.detail } : {}
3621
3622
  });
3622
3623
  throw new MaxRetriesExceededError(
3623
3624
  `Server still returned 402 after ${attempts} attempt(s) with on-chain proof ref=${ref}${unconfirmedNote}. Last server rejection: ${why}. Re-verify or re-submit ref=${ref} before retrying \u2014 never re-pay (it would double-spend).`,
@@ -3652,7 +3653,7 @@ var PipRailClient = class {
3652
3653
  const headers = new Headers(init?.headers);
3653
3654
  headers.set(HEADER_SIGNATURE, buildExactSignatureHeader({ accepted, payload }));
3654
3655
  const rejectDefinitive = (why2) => {
3655
- this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})` });
3656
+ this.safeEmit({ kind: "payment-failed", reason: `exact: facilitator rejected nonce=${nonce} (${why2})`, code: why2 });
3656
3657
  throw new MaxRetriesExceededError(
3657
3658
  `exact: the facilitator rejected the payment (${why2}). Fix the cause, then re-present the SAME signed authorization (nonce=${nonce}) \u2014 do NOT re-sign a fresh nonce. ref=${nonce}.`,
3658
3659
  { ref: nonce }
@@ -3707,7 +3708,8 @@ var PipRailClient = class {
3707
3708
  const why = lastReason ? `${lastReason.error}${lastReason.detail ? ` \u2014 ${lastReason.detail}` : ""}` : "server gave no reason";
3708
3709
  this.safeEmit({
3709
3710
  kind: "payment-failed",
3710
- reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`
3711
+ reason: `exact: 402 after submitting authorization nonce=${nonce} (${why})`,
3712
+ ...lastReason ? { code: lastReason.error, detail: lastReason.detail } : {}
3711
3713
  });
3712
3714
  throw new MaxRetriesExceededError(
3713
3715
  `exact: server still returned 402 after submitting the signed authorization (nonce=${nonce}). Last rejection: ${why}. Re-present the SAME authorization \u2014 do NOT re-sign a fresh nonce; verify authorizationState(${payerFrom}, ${nonce}) first. ref=${nonce}.`,
@@ -5260,6 +5262,7 @@ function normaliseExactOption(exact) {
5260
5262
  if (exact === true) return { settle: "keyless" };
5261
5263
  return exact;
5262
5264
  }
5265
+ var TRANSIENT_VERIFY_CODES = /* @__PURE__ */ new Set(["tx_not_found", "insufficient_confirmations"]);
5263
5266
  function createPaymentGate(options) {
5264
5267
  const minConfirmations = options.minConfirmations ?? 1;
5265
5268
  const maxTimeoutSeconds = options.maxTimeoutSeconds ?? 600;
@@ -5544,6 +5547,32 @@ function createPaymentGate(options) {
5544
5547
  if (options.awaitOnPaid) await fireOnPaid(paid);
5545
5548
  else void fireOnPaid(paid);
5546
5549
  }
5550
+ function reportOnFailedError(error, failure) {
5551
+ if (!options.onFailedError) return;
5552
+ try {
5553
+ options.onFailedError(error, failure);
5554
+ } catch {
5555
+ }
5556
+ }
5557
+ function fireOnFailed(failure) {
5558
+ if (!options.onFailed) return;
5559
+ let outcome;
5560
+ try {
5561
+ outcome = options.onFailed(failure);
5562
+ } catch (err) {
5563
+ reportOnFailedError(err, failure);
5564
+ return;
5565
+ }
5566
+ if (outcome != null && typeof outcome.then === "function") {
5567
+ return Promise.resolve(outcome).catch((err) => reportOnFailedError(err, failure));
5568
+ }
5569
+ }
5570
+ async function deliverOnFailed(result) {
5571
+ const code = result.error;
5572
+ const failure = { code, detail: result.detail, transient: TRANSIENT_VERIFY_CODES.has(code) };
5573
+ if (options.awaitOnFailed) await fireOnFailed(failure);
5574
+ else void fireOnFailed(failure);
5575
+ }
5547
5576
  async function describe(resourceUrl = "") {
5548
5577
  const specs = await ready();
5549
5578
  const accepts = [];
@@ -5707,6 +5736,11 @@ function createPaymentGate(options) {
5707
5736
  return { kind: "paid", receipt: result.receipt, receiptHeader: buildReceiptHeader(result.receipt) };
5708
5737
  }
5709
5738
  async function verify(paymentSignature) {
5739
+ const result = await resolveVerdict(paymentSignature);
5740
+ if (result.kind === "invalid") await deliverOnFailed(result);
5741
+ return result;
5742
+ }
5743
+ async function resolveVerdict(paymentSignature) {
5710
5744
  const raw = normaliseHeader(paymentSignature);
5711
5745
  if (!raw) return asChallenge();
5712
5746
  const sig = parseSignatureHeader(raw);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@piprail/sdk",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Accept x402 crypto payments across 29 chains — every major EVM chain plus Solana, TON, Tron, NEAR, Sui, Aptos, Algorand, Stellar & XRPL — in a couple of lines. No backend, no database, no fee; payments settle straight to your wallet.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",