uvd-x402-sdk 2.78.0 → 2.79.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/README.md CHANGED
@@ -1351,6 +1351,50 @@ const body = buildVerifyRequestV2(
1351
1351
  > error that names no field. If you see it, check the envelope shape first, not
1352
1352
  > the fields inside it.
1353
1353
 
1354
+ ### The top-level `x402Version` names the ENVELOPE
1355
+
1356
+ `VerifyRequest.x402Version` is typed `1`, not `1 | 2`: it says which of the two
1357
+ shapes above the body has, and `VerifyRequest` **is** the v1 shape. The payer's
1358
+ own version stays where the payer put it, in `paymentPayload.x402Version`, and
1359
+ the SDK never rewrites it.
1360
+
1361
+ ```typescript
1362
+ // A buyer that declares v2 while carrying plain network names — legal, and what
1363
+ // a 402 advertising CAIP-2 invites.
1364
+ const body = buildVerifyRequest({ x402Version: 2, scheme: 'exact', network: 'base', payload }, reqs);
1365
+
1366
+ body.x402Version; // 1 — the envelope is v1
1367
+ body.paymentPayload.x402Version; // 2 — the payer's marker, untouched
1368
+ ```
1369
+
1370
+ `resolveEnvelopeVersion` (and therefore the `'auto'` default) accepts a **v2
1371
+ payload** as well as a v1 header. A v2 payload has no top-level `network` at all
1372
+ — v2 moved the chain id into `accepted` — so `auto` reads it from there. Before
1373
+ **2.79.0** it read only the top level and threw
1374
+ `Cannot read properties of undefined` on exactly that shape, which is why
1375
+ integrators were pinning `x402Version` instead of using the default.
1376
+
1377
+ A network with **no CAIP-2 form** cannot travel in a v2 body at all, so pinning
1378
+ version 2 on one throws instead of building it — `xrpl-mainnet` is the case: its
1379
+ v1 string *is* its network id. `auto` leaves those on v1, where they work, so
1380
+ you only reach the throw by pinning. It names the network and the escape, which
1381
+ the facilitator's `400` (`data did not match any variant of untagged enum`) does
1382
+ not.
1383
+
1384
+ Until **2.79.0** the top level inherited `paymentHeader.x402Version`, so that
1385
+ call emitted a body declaring `2` around a `paymentRequirements` — a v1 shape.
1386
+ It was served correctly then and it is served correctly now: the facilitator's
1387
+ envelope enum is untagged and matches on shape. But the facilitator already
1388
+ reads that marker for one thing — choosing the hint in its `400`:
1389
+
1390
+ > `This body declares `x402Version: 2`. x402 v2 is a JSON object with
1391
+ > `paymentPayload`, `resource` and `accepted`…`
1392
+
1393
+ So the first time such a body failed for an unrelated reason, the diagnosis sent
1394
+ you to fix the wrong shape. If you were constructing a `VerifyRequest` by hand
1395
+ with a `1 | 2` variable, that no longer type-checks — write `1`, or use
1396
+ `buildVerifyRequestForVersion` and let it pick.
1397
+
1354
1398
  ## Metrics and history (`getStats` / `getTransactions`)
1355
1399
 
1356
1400
  ```typescript
@@ -234,18 +234,30 @@ interface PaymentRequirements {
234
234
  extra?: unknown;
235
235
  }
236
236
  /**
237
- * Verify request body for the facilitator /verify endpoint
237
+ * Verify request body for the facilitator /verify endpoint -- the **v1**
238
+ * envelope. {@link VerifyRequestV2} is the other one.
238
239
  */
239
240
  interface VerifyRequest {
240
- x402Version: X402Version;
241
+ /**
242
+ * Always `1`: this marker names the ENVELOPE, and this envelope is v1.
243
+ *
244
+ * Narrowed from `X402Version` on 2026-09-04. A `VerifyRequest` carrying `2`
245
+ * was always an uninhabitable value -- a body declaring v2 while shaped as
246
+ * v1 -- and typing it as `1 | 2` is what let the payer's marker be copied in
247
+ * here. The payer's version lives in `paymentPayload.x402Version`, which is
248
+ * still the full union.
249
+ */
250
+ x402Version: 1;
241
251
  paymentPayload: X402Header;
242
252
  paymentRequirements: PaymentRequirements;
243
253
  }
244
254
  /**
245
- * Settle request body for the facilitator /settle endpoint
255
+ * Settle request body for the facilitator /settle endpoint -- the **v1**
256
+ * envelope. {@link SettleRequestV2} is the other one.
246
257
  */
247
258
  interface SettleRequest {
248
- x402Version: X402Version;
259
+ /** Always `1` -- see {@link VerifyRequest.x402Version}. */
260
+ x402Version: 1;
249
261
  paymentPayload: X402Header;
250
262
  paymentRequirements: PaymentRequirements;
251
263
  }
@@ -551,6 +563,14 @@ declare function toResourceInfoV2(requirements: PaymentRequirements): ResourceIn
551
563
  * `extra` is carried through when present -- it is where the EIP-712 domain
552
564
  * `name`/`version` live for tokens the facilitator does not know by address, so
553
565
  * dropping it breaks EURC and the bridged USDCs.
566
+ *
567
+ * @throws If `requirements.network` has NO CAIP-2 form. `chainToCAIP2` answers
568
+ * with the name unchanged when it does not know a chain, and XRPL maps to
569
+ * itself on purpose -- its v1 string IS its network id. Passing that through
570
+ * would put a plain name inside a v2 body, which is a measured 400 (the same
571
+ * `no variant matched` that names no field). Only reachable by PINNING version
572
+ * 2 on such a network; `auto` leaves them on v1, where they work. Failing here
573
+ * names the network and the fix, which a 400 from the facilitator does not.
554
574
  */
555
575
  declare function toPaymentRequirementsV2(requirements: PaymentRequirements): PaymentRequirementsV2;
556
576
  /**
@@ -561,24 +581,48 @@ declare function toPaymentRequirementsV2(requirements: PaymentRequirements): Pay
561
581
  *
562
582
  * **Auto keys off CAIP-2, NOT off `paymentHeader.x402Version`,** and that is a
563
583
  * measured decision rather than a stylistic one. The facilitator's envelope enum
564
- * is untagged: it matches on SHAPE and ignores the version marker. Measured
565
- * against production on 2026-09-03:
584
+ * is untagged: it matches on SHAPE and ignores the version marker.
585
+ *
586
+ * Re-measured against `https://facilitator.ultravioletadao.xyz/verify` on
587
+ * **2026-09-04** with a fabricated signature. The signature never verifies, so
588
+ * every row is an HTTP 400 and the STATUS discriminates nothing -- what does is
589
+ * the error code. `invalid_request_body` means the facilitator could not
590
+ * deserialize the body; `contract_call_failed` means it read the body, resolved
591
+ * the chain and got as far as the on-chain call, i.e. the envelope was fine.
566
592
  *
567
593
  * | payload network | requirements network | v1 envelope today |
568
594
  * |-----------------|----------------------|-------------------|
569
- * | `base` | `base` | **200** |
570
- * | `base` (header says `x402Version: 2`) | `base` | **200** |
571
- * | `eip155:8453` | `base` | 400 |
572
- * | `base` | `eip155:8453` | 400 |
573
- * | `eip155:8453` | `eip155:8453` | 400 (`unknown variant \`eip155:8453\``) |
574
- *
575
- * So a header that merely *declares* version 2 while carrying plain names is
576
- * being served correctly today. Upgrading it on the strength of the marker would
577
- * change a call that works -- the one thing this must not do. Every CAIP-2
578
- * combination, by contrast, is already a hard 400, so switching those to v2
579
- * cannot regress anyone: it can only turn a failure into a payment.
580
- */
581
- declare function resolveEnvelopeVersion(paymentHeader: X402Header, requirements: PaymentRequirements, requested?: X402Version | 'auto'): X402Version;
595
+ * | `base` | `base` | understood |
596
+ * | `base` (header says `x402Version: 2`) | `base` | understood |
597
+ * | `eip155:8453` | `base` | understood |
598
+ * | `base` | `eip155:8453` | understood |
599
+ * | `eip155:8453` | `eip155:8453` | understood |
600
+ *
601
+ * **The last three rows used to be a hard 400** (`unknown variant
602
+ * \`eip155:8453\``) when this function was written on 2026-09-03. The
603
+ * facilitator has since taught the v1 envelope to read CAIP-2, so the original
604
+ * argument for this rule -- "every CAIP-2 combination is already a 400, so
605
+ * upgrading them cannot regress anyone" -- **is no longer true**. The rule is
606
+ * unchanged; three other reasons hold it up:
607
+ *
608
+ * 1. A CAIP-2 network on the wire means the 402 that produced it advertised v2.
609
+ * Answering in v2 is speaking the protocol the seller announced.
610
+ * 2. v2-with-CAIP-2 is the only shape BOTH generations of the facilitator
611
+ * accept. v1-with-CAIP-2 is a hard 400 on any build older than 2026-09-04,
612
+ * so choosing v1 there is what breaks against a self-hosted or pinned one.
613
+ * 3. The Python SDK resolves the identical rule, so the same wire produces the
614
+ * same body in both SDKs -- pinned by phase 6 of `npm run test:xlang`.
615
+ *
616
+ * And the marker still decides nothing: row 2 above is served correctly today,
617
+ * so upgrading on the strength of it would change a call that works.
618
+ *
619
+ * The negative half of that measurement, without which "understood" proves
620
+ * nothing -- the same run, bodies broken on purpose, all three
621
+ * `invalid_request_body`: a v2 body carrying a plain network name, one with
622
+ * `resource` as a bare string, and one with `accepted` removed. A well-formed
623
+ * v2 body with CAIP-2 reached `contract_call_failed` like the rows above.
624
+ */
625
+ declare function resolveEnvelopeVersion(paymentHeader: X402Header | PaymentPayloadV2, requirements: PaymentRequirements, requested?: X402Version | 'auto'): X402Version;
582
626
  /**
583
627
  * Build a `/verify` body in whichever envelope `version` names.
584
628
  *
@@ -234,18 +234,30 @@ interface PaymentRequirements {
234
234
  extra?: unknown;
235
235
  }
236
236
  /**
237
- * Verify request body for the facilitator /verify endpoint
237
+ * Verify request body for the facilitator /verify endpoint -- the **v1**
238
+ * envelope. {@link VerifyRequestV2} is the other one.
238
239
  */
239
240
  interface VerifyRequest {
240
- x402Version: X402Version;
241
+ /**
242
+ * Always `1`: this marker names the ENVELOPE, and this envelope is v1.
243
+ *
244
+ * Narrowed from `X402Version` on 2026-09-04. A `VerifyRequest` carrying `2`
245
+ * was always an uninhabitable value -- a body declaring v2 while shaped as
246
+ * v1 -- and typing it as `1 | 2` is what let the payer's marker be copied in
247
+ * here. The payer's version lives in `paymentPayload.x402Version`, which is
248
+ * still the full union.
249
+ */
250
+ x402Version: 1;
241
251
  paymentPayload: X402Header;
242
252
  paymentRequirements: PaymentRequirements;
243
253
  }
244
254
  /**
245
- * Settle request body for the facilitator /settle endpoint
255
+ * Settle request body for the facilitator /settle endpoint -- the **v1**
256
+ * envelope. {@link SettleRequestV2} is the other one.
246
257
  */
247
258
  interface SettleRequest {
248
- x402Version: X402Version;
259
+ /** Always `1` -- see {@link VerifyRequest.x402Version}. */
260
+ x402Version: 1;
249
261
  paymentPayload: X402Header;
250
262
  paymentRequirements: PaymentRequirements;
251
263
  }
@@ -551,6 +563,14 @@ declare function toResourceInfoV2(requirements: PaymentRequirements): ResourceIn
551
563
  * `extra` is carried through when present -- it is where the EIP-712 domain
552
564
  * `name`/`version` live for tokens the facilitator does not know by address, so
553
565
  * dropping it breaks EURC and the bridged USDCs.
566
+ *
567
+ * @throws If `requirements.network` has NO CAIP-2 form. `chainToCAIP2` answers
568
+ * with the name unchanged when it does not know a chain, and XRPL maps to
569
+ * itself on purpose -- its v1 string IS its network id. Passing that through
570
+ * would put a plain name inside a v2 body, which is a measured 400 (the same
571
+ * `no variant matched` that names no field). Only reachable by PINNING version
572
+ * 2 on such a network; `auto` leaves them on v1, where they work. Failing here
573
+ * names the network and the fix, which a 400 from the facilitator does not.
554
574
  */
555
575
  declare function toPaymentRequirementsV2(requirements: PaymentRequirements): PaymentRequirementsV2;
556
576
  /**
@@ -561,24 +581,48 @@ declare function toPaymentRequirementsV2(requirements: PaymentRequirements): Pay
561
581
  *
562
582
  * **Auto keys off CAIP-2, NOT off `paymentHeader.x402Version`,** and that is a
563
583
  * measured decision rather than a stylistic one. The facilitator's envelope enum
564
- * is untagged: it matches on SHAPE and ignores the version marker. Measured
565
- * against production on 2026-09-03:
584
+ * is untagged: it matches on SHAPE and ignores the version marker.
585
+ *
586
+ * Re-measured against `https://facilitator.ultravioletadao.xyz/verify` on
587
+ * **2026-09-04** with a fabricated signature. The signature never verifies, so
588
+ * every row is an HTTP 400 and the STATUS discriminates nothing -- what does is
589
+ * the error code. `invalid_request_body` means the facilitator could not
590
+ * deserialize the body; `contract_call_failed` means it read the body, resolved
591
+ * the chain and got as far as the on-chain call, i.e. the envelope was fine.
566
592
  *
567
593
  * | payload network | requirements network | v1 envelope today |
568
594
  * |-----------------|----------------------|-------------------|
569
- * | `base` | `base` | **200** |
570
- * | `base` (header says `x402Version: 2`) | `base` | **200** |
571
- * | `eip155:8453` | `base` | 400 |
572
- * | `base` | `eip155:8453` | 400 |
573
- * | `eip155:8453` | `eip155:8453` | 400 (`unknown variant \`eip155:8453\``) |
574
- *
575
- * So a header that merely *declares* version 2 while carrying plain names is
576
- * being served correctly today. Upgrading it on the strength of the marker would
577
- * change a call that works -- the one thing this must not do. Every CAIP-2
578
- * combination, by contrast, is already a hard 400, so switching those to v2
579
- * cannot regress anyone: it can only turn a failure into a payment.
580
- */
581
- declare function resolveEnvelopeVersion(paymentHeader: X402Header, requirements: PaymentRequirements, requested?: X402Version | 'auto'): X402Version;
595
+ * | `base` | `base` | understood |
596
+ * | `base` (header says `x402Version: 2`) | `base` | understood |
597
+ * | `eip155:8453` | `base` | understood |
598
+ * | `base` | `eip155:8453` | understood |
599
+ * | `eip155:8453` | `eip155:8453` | understood |
600
+ *
601
+ * **The last three rows used to be a hard 400** (`unknown variant
602
+ * \`eip155:8453\``) when this function was written on 2026-09-03. The
603
+ * facilitator has since taught the v1 envelope to read CAIP-2, so the original
604
+ * argument for this rule -- "every CAIP-2 combination is already a 400, so
605
+ * upgrading them cannot regress anyone" -- **is no longer true**. The rule is
606
+ * unchanged; three other reasons hold it up:
607
+ *
608
+ * 1. A CAIP-2 network on the wire means the 402 that produced it advertised v2.
609
+ * Answering in v2 is speaking the protocol the seller announced.
610
+ * 2. v2-with-CAIP-2 is the only shape BOTH generations of the facilitator
611
+ * accept. v1-with-CAIP-2 is a hard 400 on any build older than 2026-09-04,
612
+ * so choosing v1 there is what breaks against a self-hosted or pinned one.
613
+ * 3. The Python SDK resolves the identical rule, so the same wire produces the
614
+ * same body in both SDKs -- pinned by phase 6 of `npm run test:xlang`.
615
+ *
616
+ * And the marker still decides nothing: row 2 above is served correctly today,
617
+ * so upgrading on the strength of it would change a call that works.
618
+ *
619
+ * The negative half of that measurement, without which "understood" proves
620
+ * nothing -- the same run, bodies broken on purpose, all three
621
+ * `invalid_request_body`: a v2 body carrying a plain network name, one with
622
+ * `resource` as a bare string, and one with `accepted` removed. A well-formed
623
+ * v2 body with CAIP-2 reached `contract_call_failed` like the rows above.
624
+ */
625
+ declare function resolveEnvelopeVersion(paymentHeader: X402Header | PaymentPayloadV2, requirements: PaymentRequirements, requested?: X402Version | 'auto'): X402Version;
582
626
  /**
583
627
  * Build a `/verify` body in whichever envelope `version` names.
584
628
  *
@@ -1261,7 +1261,23 @@ function buildPaymentRequirements(options) {
1261
1261
  }
1262
1262
  function buildVerifyRequest(paymentHeader, requirements) {
1263
1263
  return {
1264
- x402Version: paymentHeader.x402Version,
1264
+ // The literal `1` names THIS ENVELOPE, not the payer's header. Echoing
1265
+ // `paymentHeader.x402Version` here -- what this did until 2026-09-04 --
1266
+ // let a buyer who declared `2` produce a body that says "2" while carrying
1267
+ // `paymentRequirements`, which is the v1 shape. The facilitator serves it
1268
+ // anyway because its envelope enum is untagged and matches on shape, so
1269
+ // nothing broke; but it ALREADY picks the hint in its 400 off this marker:
1270
+ //
1271
+ // "This body declares `x402Version: 2`. x402 v2 is a JSON object with
1272
+ // `paymentPayload`, `resource` and `accepted`..."
1273
+ //
1274
+ // So the day that body fails for any other reason, the diagnosis sends the
1275
+ // integrator to document the wrong shape. That inversion -- being told to
1276
+ // fix the fields when the wrapper is what is wrong -- is what cost two
1277
+ // teams a day. The payer's own marker survives untouched inside
1278
+ // `paymentPayload`, where it belongs: it describes the payment, not the
1279
+ // envelope carrying it.
1280
+ x402Version: 1,
1265
1281
  paymentPayload: paymentHeader,
1266
1282
  paymentRequirements: requirements
1267
1283
  };
@@ -1284,13 +1300,21 @@ function buildSettleRequestV2(payload, resource, accepted) {
1284
1300
  }
1285
1301
  function buildSettleRequest(paymentHeader, requirements) {
1286
1302
  return {
1287
- x402Version: paymentHeader.x402Version,
1303
+ // `1` for the same reason as {@link buildVerifyRequest}: it names the
1304
+ // envelope, and `/settle` takes the same body as `/verify`.
1305
+ x402Version: 1,
1288
1306
  paymentPayload: paymentHeader,
1289
1307
  paymentRequirements: requirements
1290
1308
  };
1291
1309
  }
1292
1310
  function isCaip2Network(network) {
1293
- return network.includes(":");
1311
+ return typeof network === "string" && network.includes(":");
1312
+ }
1313
+ function networkOfPayload(payload) {
1314
+ const top = payload.network;
1315
+ if (typeof top === "string") return top;
1316
+ const accepted = payload.accepted;
1317
+ return typeof accepted?.network === "string" ? accepted.network : void 0;
1294
1318
  }
1295
1319
  function toResourceInfoV2(requirements) {
1296
1320
  return {
@@ -1300,9 +1324,15 @@ function toResourceInfoV2(requirements) {
1300
1324
  };
1301
1325
  }
1302
1326
  function toPaymentRequirementsV2(requirements) {
1327
+ const network = isCaip2Network(requirements.network) ? requirements.network : chainToCAIP2(requirements.network);
1328
+ if (!isCaip2Network(network)) {
1329
+ throw new Error(
1330
+ `Network '${requirements.network}' has no CAIP-2 form, so it cannot travel in the x402 v2 envelope. Use x402Version: 1 for this network.`
1331
+ );
1332
+ }
1303
1333
  return {
1304
1334
  scheme: requirements.scheme,
1305
- network: isCaip2Network(requirements.network) ? requirements.network : chainToCAIP2(requirements.network),
1335
+ network,
1306
1336
  asset: requirements.asset,
1307
1337
  amount: requirements.maxAmountRequired,
1308
1338
  payTo: requirements.payTo,
@@ -1315,7 +1345,7 @@ function resolveEnvelopeVersion(paymentHeader, requirements, requested = "auto")
1315
1345
  if (requested !== "auto") {
1316
1346
  return requested;
1317
1347
  }
1318
- return isCaip2Network(paymentHeader.network) || isCaip2Network(requirements.network) ? 2 : 1;
1348
+ return isCaip2Network(networkOfPayload(paymentHeader)) || isCaip2Network(requirements.network) ? 2 : 1;
1319
1349
  }
1320
1350
  function buildVerifyRequestForVersion(paymentHeader, requirements, version) {
1321
1351
  if (version === 2) {