@playmos/sdk 0.3.1 → 0.3.2

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/dist/index.cjs CHANGED
@@ -275,15 +275,36 @@ function createHttpClient(baseUrl, apiKey, retry) {
275
275
  );
276
276
  }
277
277
  }
278
+ function hasIdempotencyKey(init) {
279
+ const h = init.headers;
280
+ if (!h) return false;
281
+ if (h instanceof Headers) {
282
+ return Boolean(h.get("idempotency-key") || h.get("Idempotency-Key"));
283
+ }
284
+ if (Array.isArray(h)) {
285
+ return h.some(([k]) => String(k).toLowerCase() === "idempotency-key");
286
+ }
287
+ const rec = h;
288
+ return Object.keys(rec).some((k) => k.toLowerCase() === "idempotency-key" && !!rec[k]);
289
+ }
290
+ function isRetryable(res, init) {
291
+ if (res.status === 429) return true;
292
+ if (res.status === 502 || res.status === 503 || res.status === 504) {
293
+ const method = (init.method ?? "GET").toUpperCase();
294
+ if (method === "GET" || method === "HEAD") return true;
295
+ return hasIdempotencyKey(init);
296
+ }
297
+ return false;
298
+ }
278
299
  async function sendWithRetry(url, init) {
279
300
  let res = await send(url, init);
280
- for (let attempt = 0; res.status === 429 && attempt < cfg.maxRetries; attempt++) {
301
+ for (let attempt = 0; attempt < cfg.maxRetries && isRetryable(res, init); attempt++) {
281
302
  await sleep(backoffMs(res, attempt, cfg));
282
303
  res = await send(url, init);
283
304
  }
284
305
  return res;
285
306
  }
286
- async function handle(res) {
307
+ async function handle(res, acceptStatuses) {
287
308
  let text;
288
309
  try {
289
310
  text = await res.text();
@@ -298,9 +319,13 @@ function createHttpClient(baseUrl, apiKey, retry) {
298
319
  try {
299
320
  json = text ? JSON.parse(text) : {};
300
321
  } catch {
301
- throw new ApiError(`Non-JSON response (${res.status}) from ${res.url}`, { status: res.status, body: text });
322
+ const gateway = res.status === 502 || res.status === 503 || res.status === 504;
323
+ const msg = gateway ? `Sandbox gateway timeout (${res.status}) from ${res.url} \u2014 the request likely exceeded the platform limit while settling on-chain. Retry with the same idempotency key (safe); prefer a dedicated Base Sepolia RPC on the service (BB-GATEB-002).` : `Non-JSON response (${res.status}) from ${res.url}`;
324
+ throw new ApiError(msg, { status: res.status, body: text.slice(0, 500), gateway });
325
+ }
326
+ if (res.ok || acceptStatuses !== void 0 && acceptStatuses.includes(res.status)) {
327
+ return json;
302
328
  }
303
- if (res.ok) return json;
304
329
  const errBody = json;
305
330
  const message = errBody?.error?.message ?? `Request failed with ${res.status}`;
306
331
  if (res.status === 401 || res.status === 403 || errBody?.error?.code === "auth") {
@@ -316,7 +341,7 @@ function createHttpClient(baseUrl, apiKey, retry) {
316
341
  };
317
342
  if (opts?.idempotencyKey) headers["idempotency-key"] = opts.idempotencyKey;
318
343
  const res = await sendWithRetry(`${base}${path}`, { method: "POST", headers, body: JSON.stringify(body) });
319
- return handle(res);
344
+ return handle(res, opts?.acceptStatuses);
320
345
  },
321
346
  async get(path) {
322
347
  const res = await sendWithRetry(`${base}${path}`, {
@@ -532,7 +557,23 @@ async function waitForCalls(provider, id, timeoutMs = 6e4) {
532
557
  function encodeApprove(spender, amountUnits) {
533
558
  return viem.encodeFunctionData({ abi: erc20Abi, functionName: "approve", args: [spender, amountUnits] });
534
559
  }
535
- async function assertEnoughGas(provider, from, minWei = 200000000000000n) {
560
+ var GAS_FLOOR_WEI_DEFAULT = 200000000000000n;
561
+ var GAS_FLOOR_WEI_BASE_SEPOLIA = 50000000000000n;
562
+ var CHAIN_ID_BASE_SEPOLIA = 84532;
563
+ function gasFloorWei(chainId) {
564
+ return chainId === CHAIN_ID_BASE_SEPOLIA ? GAS_FLOOR_WEI_BASE_SEPOLIA : GAS_FLOOR_WEI_DEFAULT;
565
+ }
566
+ async function assertEnoughGas(provider, from, minWei) {
567
+ let floor = minWei;
568
+ if (floor === void 0) {
569
+ let chainId = 0;
570
+ try {
571
+ const hex = await provider.request({ method: "eth_chainId", params: [] });
572
+ chainId = Number(BigInt(hex));
573
+ } catch {
574
+ }
575
+ floor = gasFloorWei(chainId);
576
+ }
536
577
  let balance;
537
578
  try {
538
579
  const hex = await provider.request({ method: "eth_getBalance", params: [from, "latest"] });
@@ -540,8 +581,11 @@ async function assertEnoughGas(provider, from, minWei = 200000000000000n) {
540
581
  } catch {
541
582
  return;
542
583
  }
543
- if (balance < minWei) {
544
- throw new InsufficientGasError({ balanceWei: balance.toString(), minWei: minWei.toString() });
584
+ if (balance < floor) {
585
+ throw new InsufficientGasError({
586
+ balanceWei: balance.toString(),
587
+ minWei: floor.toString()
588
+ });
545
589
  }
546
590
  }
547
591
  var toBytes32 = (s) => viem.keccak256(viem.toBytes(s));
@@ -603,6 +647,7 @@ function mockEntryPayment(args) {
603
647
  args.seedBps,
604
648
  args.rakeBps
605
649
  );
650
+ const roundKey = args.roundKey && args.roundKey.trim() !== "" ? args.roundKey : args.roundId;
606
651
  return {
607
652
  id: prefixedId("entry"),
608
653
  status: "confirmed",
@@ -619,7 +664,8 @@ function mockEntryPayment(args) {
619
664
  },
620
665
  gameId: args.gameId,
621
666
  roundId: args.roundId,
622
- roundKey: args.roundId,
667
+ roundKey,
668
+ identity: args.identity,
623
669
  // Deterministic mock pool address so claim-flow unit tests can assert shape (#41).
624
670
  prizePoolAddress: "0x0000000000000000000000000000000000000001",
625
671
  playerId: args.playerId,
@@ -630,14 +676,160 @@ function mockEntryPayment(args) {
630
676
  mock: true
631
677
  };
632
678
  }
679
+ function mockVerifyResult(payment) {
680
+ return { ...payment, mock: true };
681
+ }
633
682
 
634
- // src/client.ts
683
+ // src/x402.ts
635
684
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
636
- function requireAddressField(value, field) {
685
+ function requireAddress(value, field) {
637
686
  if (typeof value !== "string" || value.trim() === "") {
638
687
  throw new MissingFieldError(field);
639
688
  }
640
689
  if (!ADDRESS_RE.test(value)) {
690
+ throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
691
+ field,
692
+ value
693
+ });
694
+ }
695
+ return value.toLowerCase();
696
+ }
697
+ function networkToCaip2(network) {
698
+ if (network === "base") return "eip155:8453";
699
+ if (network === "base-sepolia") return "eip155:84532";
700
+ throw new ConfigError(`Unknown network for x402: ${JSON.stringify(network)}`, { network });
701
+ }
702
+ function toX402PaymentRequired(req, extras) {
703
+ const amountMicro = parseUsdToMicro(req.amount);
704
+ const out = {
705
+ scheme: "exact",
706
+ network: networkToCaip2(req.network),
707
+ maxAmountRequired: amountMicro.toString(),
708
+ // Service V1 emits the asset symbol; address is implied by network + config.
709
+ asset: "USDC",
710
+ payTo: req.payTo,
711
+ playmosRequirementId: req.id,
712
+ amount: req.amount,
713
+ playmosNetwork: req.network,
714
+ expiresAt: req.expiresAt,
715
+ terms: req.terms ?? null,
716
+ feeBps: extras?.feeBps ?? null,
717
+ feeSink: extras?.feeSink ?? null
718
+ };
719
+ return out;
720
+ }
721
+ function encodePaymentHeader(obj) {
722
+ const json = JSON.stringify(obj);
723
+ if (typeof Buffer !== "undefined") {
724
+ return Buffer.from(json, "utf8").toString("base64");
725
+ }
726
+ const bytes = new TextEncoder().encode(json);
727
+ let bin = "";
728
+ for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
729
+ return btoa(bin);
730
+ }
731
+ function decodePaymentHeader(header) {
732
+ if (typeof header !== "string" || header.trim() === "") {
733
+ throw new ConfigError("payment header must be a non-empty base64 JSON string");
734
+ }
735
+ try {
736
+ let json;
737
+ if (typeof Buffer !== "undefined") {
738
+ json = Buffer.from(header, "base64").toString("utf8");
739
+ } else {
740
+ const bin = atob(header);
741
+ const bytes = new Uint8Array(bin.length);
742
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
743
+ json = new TextDecoder().decode(bytes);
744
+ }
745
+ return JSON.parse(json);
746
+ } catch (err) {
747
+ throw new ConfigError("invalid base64 JSON payment header", {
748
+ cause: err instanceof Error ? err.message : String(err)
749
+ });
750
+ }
751
+ }
752
+ function createX402Challenge(requirement, extras) {
753
+ if (!requirement || typeof requirement !== "object") {
754
+ throw new ConfigError("createX402Challenge requires a PaymentRequirement");
755
+ }
756
+ if (typeof requirement.id !== "string" || !requirement.id.startsWith("preq_")) {
757
+ throw new ConfigError('PaymentRequirement.id must be a "preq_\u2026" string', { id: requirement.id });
758
+ }
759
+ requireAddress(requirement.payTo, "payTo");
760
+ parseUsdToMicro(requirement.amount);
761
+ const feeBps = extras?.feeBps ?? 0;
762
+ if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
763
+ throw new ConfigError(
764
+ `feeBps must be an integer in [0, 10000], got: ${JSON.stringify(extras?.feeBps)}`,
765
+ { feeBps: extras?.feeBps }
766
+ );
767
+ }
768
+ const feeSink = extras?.feeSink ?? null;
769
+ if (feeBps > 0 && feeSink) requireAddress(feeSink, "feeSink");
770
+ const paymentRequired = toX402PaymentRequired(requirement, { feeBps, feeSink });
771
+ return {
772
+ status: 402,
773
+ headers: {
774
+ "PAYMENT-REQUIRED": encodePaymentHeader(paymentRequired),
775
+ "Content-Type": "application/json"
776
+ },
777
+ body: {
778
+ error: { code: "payment_required", message: "Payment Required" },
779
+ requirement,
780
+ paymentRequired,
781
+ feeBps,
782
+ feeSink
783
+ },
784
+ paymentRequired
785
+ };
786
+ }
787
+ function validateX402ChallengeInput(input) {
788
+ if (!input || typeof input !== "object") {
789
+ throw new ConfigError("x402 challenge input is required");
790
+ }
791
+ if ("url" in input && input.url !== void 0) {
792
+ throw new ConfigError(
793
+ "playmos.x402 does not accept { url } in V1 \u2014 use pay({ payTo, amount }) wrapping challenges+settle (ADR: resource URLs deferred V1.1+). See docs/design/PHASE-4-X402-ADR.md.",
794
+ { field: "url" }
795
+ );
796
+ }
797
+ const payTo = requireAddress(input.payTo, "payTo");
798
+ parseUsdToMicro(input.amount);
799
+ const intent = input.intent ?? "transfer";
800
+ if (intent !== "transfer" && intent !== "marketplace.buy") {
801
+ throw new ConfigError(
802
+ `x402 intent must be "transfer" or "marketplace.buy", got: ${JSON.stringify(input.intent)}`,
803
+ { intent: input.intent }
804
+ );
805
+ }
806
+ const feeBps = input.feeBps ?? 0;
807
+ if (!Number.isInteger(feeBps) || feeBps < 0 || feeBps > 1e4) {
808
+ throw new ConfigError(
809
+ `feeBps must be an integer in [0, 10000], got: ${JSON.stringify(input.feeBps)}`,
810
+ { feeBps: input.feeBps }
811
+ );
812
+ }
813
+ const feeSink = input.feeSink === void 0 ? void 0 : requireAddress(input.feeSink, "feeSink");
814
+ return {
815
+ payTo,
816
+ amount: input.amount,
817
+ intent,
818
+ feeBps,
819
+ ...feeSink !== void 0 ? { feeSink } : {},
820
+ ...input.terms !== void 0 ? { terms: input.terms } : {},
821
+ ...input.expiresInMs !== void 0 ? { expiresInMs: input.expiresInMs } : {},
822
+ ...input.id !== void 0 ? { id: input.id } : {}
823
+ };
824
+ }
825
+
826
+ // src/client.ts
827
+ var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
828
+ function requireAddressField(value, field) {
829
+ if (typeof value !== "string" || value.trim() === "") {
830
+ throw new MissingFieldError(field);
831
+ }
832
+ if (!ADDRESS_RE2.test(value)) {
641
833
  throw new ConfigError(
642
834
  `${field} must be a 0x-prefixed 20-byte wallet address (Phase 1a transfers settle server-held wallets), got: ${JSON.stringify(value)}`,
643
835
  { field, value }
@@ -682,6 +874,122 @@ var Playmos = class {
682
874
  /** Create a Bridge KYC onboarding link (fiat payout). */
683
875
  createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
684
876
  };
877
+ /**
878
+ * `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
879
+ *
880
+ * V1 surface (ADR):
881
+ * - `challenge` → `POST /v1/x402/challenges` (returns 402; treated as success)
882
+ * - `fulfill` → `POST /v1/x402/settle`
883
+ * - `pay` → challenge then fulfill (senior DX ≤15 lines)
884
+ *
885
+ * **Not** resource URLs (`pay({ url })` is V1.1+). **Not** stock third-party
886
+ * x402 client interop (X3b deferred until #161). Requires `sk_test_` +
887
+ * Base Sepolia; flag `PLAYMOS_X402_ENABLED` on the service.
888
+ *
889
+ * ```ts
890
+ * const playmos = new Playmos({ apiKey: process.env.PLAYMOS_SK!, network: "base-sepolia" });
891
+ * const result = await playmos.x402.pay({
892
+ * payTo: "0x…",
893
+ * amount: "0.50",
894
+ * feeBps: 0,
895
+ * });
896
+ * // result.txHash → BaseScan
897
+ * ```
898
+ */
899
+ this.x402 = {
900
+ /**
901
+ * Mint a server-authoritative PaymentRequirement (HTTP 402).
902
+ * Fee terms are stored server-side — settle rejects payload disagreement.
903
+ */
904
+ challenge: async (input) => {
905
+ this.assertX402Allowed();
906
+ const body = validateX402ChallengeInput(input);
907
+ if (body.intent === "marketplace.buy") {
908
+ throw new ConfigError(
909
+ 'x402 challenge intent "marketplace.buy" is not supported yet (V1 service: "transfer" only; marketplace.buy follows).',
910
+ { intent: body.intent }
911
+ );
912
+ }
913
+ const res = await this.http.post("/x402/challenges", body, { acceptStatuses: [402] });
914
+ return {
915
+ requirement: res.requirement,
916
+ paymentRequired: res.paymentRequired,
917
+ feeBps: res.feeBps ?? body.feeBps,
918
+ feeSink: res.feeSink ?? body.feeSink ?? null
919
+ };
920
+ },
921
+ /**
922
+ * Settle a previously minted challenge with an `x402-payload` authorization.
923
+ * Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
924
+ * materialize still requires mode; the route also defaults for this path).
925
+ */
926
+ fulfill: async (requirement, authorization) => {
927
+ this.assertX402Allowed();
928
+ if (!requirement || typeof requirement !== "object") {
929
+ throw new ConfigError("fulfill requires a PaymentRequirement from challenge()");
930
+ }
931
+ if (typeof requirement.id !== "string" || requirement.id.trim() === "") {
932
+ throw new MissingFieldError("requirement.id");
933
+ }
934
+ let payload;
935
+ if (authorization && typeof authorization === "object" && "kind" in authorization) {
936
+ const a = authorization;
937
+ if (a.kind !== "x402-payload") {
938
+ throw new ConfigError('authorization.kind must be "x402-payload"', { kind: a.kind });
939
+ }
940
+ payload = { ...a.payload ?? {} };
941
+ } else {
942
+ payload = { ...authorization ?? {} };
943
+ }
944
+ delete payload.feeBps;
945
+ delete payload.feeSink;
946
+ if (payload.mode === void 0 || typeof payload.mode === "string" && payload.mode.trim() === "") {
947
+ const hasNestedAuth = payload.authorization !== void 0 || payload.signature !== void 0 || payload.paymentPayload !== void 0;
948
+ if (hasNestedAuth) {
949
+ throw new ConfigError(
950
+ "fulfill: payload.mode is required when authorization/signature is present \u2014 stock-shaped payloads are not auto-mapped to server-signer (V1: no interop; set mode explicitly or see #161)",
951
+ { field: "mode" }
952
+ );
953
+ }
954
+ payload.mode = "server-signer";
955
+ }
956
+ if (payload.scheme === void 0) payload.scheme = "exact";
957
+ if (payload.requirementId === void 0) payload.requirementId = requirement.id;
958
+ const res = await this.http.post("/x402/settle", {
959
+ requirement,
960
+ authorization: { kind: "x402-payload", payload }
961
+ });
962
+ const s = res.settlement;
963
+ return {
964
+ requirementId: s.requirementId,
965
+ status: s.status,
966
+ txHash: s.txHash ?? null,
967
+ idempotentReplay: Boolean(s.idempotentReplay),
968
+ verifiedVia: s.verifiedVia,
969
+ feeBps: s.feeBps,
970
+ fee: s.fee,
971
+ net: s.net,
972
+ requirement: res.requirement,
973
+ success: s.success
974
+ };
975
+ },
976
+ /**
977
+ * Senior DX: mint challenge + settle in one call.
978
+ * Wraps challenges + settle only — **not** `pay({ url })` (ADR).
979
+ */
980
+ pay: async (input) => {
981
+ this.assertX402Allowed();
982
+ validateX402ChallengeInput(input);
983
+ const challenge = await this.x402.challenge(input);
984
+ const auth = {
985
+ mode: input.mode ?? "server-signer"
986
+ };
987
+ if (input.from !== void 0) auth.from = input.from;
988
+ if (input.authorization !== void 0) auth.authorization = input.authorization;
989
+ if (input.txHash !== void 0) auth.txHash = input.txHash;
990
+ return this.x402.fulfill(challenge.requirement, auth);
991
+ }
992
+ };
685
993
  /**
686
994
  * Skill/contest **round lifecycle** (issue #13) — closes the enterRound loop.
687
995
  *
@@ -1014,6 +1322,11 @@ var Playmos = class {
1014
1322
  return this.http.get(`/listings/${encodeURIComponent(listingId)}`);
1015
1323
  }
1016
1324
  };
1325
+ /**
1326
+ * Offline mock payments for this client instance — so `verify(id)` after
1327
+ * `mock: true` pay/enterRound does not hit the live API (#204).
1328
+ */
1329
+ this.mockPayments = /* @__PURE__ */ new Map();
1017
1330
  /**
1018
1331
  * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
1019
1332
  * `get` reconciles once against chain truth; `wait` polls it to a terminal state
@@ -1063,6 +1376,9 @@ var Playmos = class {
1063
1376
  }
1064
1377
  /** Connect the player's wallet and return their address. */
1065
1378
  async connect() {
1379
+ if (this.config.mock) {
1380
+ return "0x0000000000000000000000000000000000000001";
1381
+ }
1066
1382
  return getAccount(resolveProvider(this.config.wallet));
1067
1383
  }
1068
1384
  /**
@@ -1079,18 +1395,29 @@ var Playmos = class {
1079
1395
  payEntry: async (req) => {
1080
1396
  const entry = await this.enterRound({
1081
1397
  gameId: cfg.gameId,
1398
+ // Studio roundId for bookkeeping; pin roundKey separately for hasEntered.
1082
1399
  roundId: req.roundKey,
1083
- // the round the server verifies against
1084
1400
  roundKey: req.roundKey,
1085
1401
  identity: req.identity,
1086
1402
  amount: formatMicroToUsd(req.entryUnits),
1087
1403
  playerId: req.wallet
1088
1404
  });
1089
1405
  const status = entry.status === "confirmed" ? "CONFIRMED" : entry.status === "failed" ? "FAILED" : "PENDING";
1090
- return { paymentId: entry.id, status, txHash: entry.txHash, onchain: true, identity: entry.identity };
1406
+ const onchain = !entry.mock;
1407
+ return {
1408
+ paymentId: entry.id,
1409
+ status,
1410
+ txHash: entry.txHash,
1411
+ onchain,
1412
+ identity: entry.identity ?? req.identity
1413
+ };
1091
1414
  }
1092
1415
  };
1093
1416
  }
1417
+ rememberMock(payment) {
1418
+ if (payment.mock) this.mockPayments.set(payment.id, payment);
1419
+ return payment;
1420
+ }
1094
1421
  /** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
1095
1422
  async pay(input) {
1096
1423
  const amountMicro = validateAmount(input.amount);
@@ -1099,14 +1426,16 @@ var Playmos = class {
1099
1426
  const metadata = validateMetadata(input.metadata);
1100
1427
  const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
1101
1428
  if (this.config.mock) {
1102
- return mockIapPayment({
1103
- amountMicro,
1104
- feeBps: IAP_FEE_BPS,
1105
- sku: input.sku,
1106
- playerId: input.playerId,
1107
- chain: this.env.network,
1108
- metadata
1109
- });
1429
+ return this.rememberMock(
1430
+ mockIapPayment({
1431
+ amountMicro,
1432
+ feeBps: IAP_FEE_BPS,
1433
+ sku: input.sku,
1434
+ playerId: input.playerId,
1435
+ chain: this.env.network,
1436
+ metadata
1437
+ })
1438
+ );
1110
1439
  }
1111
1440
  if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
1112
1441
  const res = await this.http.post(
@@ -1149,17 +1478,22 @@ var Playmos = class {
1149
1478
  const metadata = validateMetadata(input.metadata);
1150
1479
  const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
1151
1480
  if (this.config.mock) {
1152
- return mockEntryPayment({
1153
- amountMicro,
1154
- poolBps: POOL_BPS,
1155
- seedBps: SEED_BPS,
1156
- rakeBps: RAKE_BPS,
1157
- gameId: input.gameId,
1158
- roundId: input.roundId,
1159
- playerId: input.playerId,
1160
- chain: this.env.network,
1161
- metadata
1162
- });
1481
+ return this.rememberMock(
1482
+ mockEntryPayment({
1483
+ amountMicro,
1484
+ poolBps: POOL_BPS,
1485
+ seedBps: SEED_BPS,
1486
+ rakeBps: RAKE_BPS,
1487
+ gameId: input.gameId,
1488
+ roundId: input.roundId,
1489
+ playerId: input.playerId,
1490
+ chain: this.env.network,
1491
+ metadata,
1492
+ // B2 pins — must survive mock path for hub hasEntered parity (#204).
1493
+ roundKey: input.roundKey,
1494
+ identity: input.identity
1495
+ })
1496
+ );
1163
1497
  }
1164
1498
  if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
1165
1499
  const res = await this.http.post(
@@ -1171,7 +1505,7 @@ var Playmos = class {
1171
1505
  if (input.identity) payment2.identity = input.identity;
1172
1506
  const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
1173
1507
  if (pool) payment2.prizePoolAddress = pool.toLowerCase();
1174
- payment2.roundKey = res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
1508
+ payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
1175
1509
  return payment2;
1176
1510
  }
1177
1511
  const intent = await this.http.post(
@@ -1266,9 +1600,41 @@ var Playmos = class {
1266
1600
  /** Verify a payment by the service's on-chain read (spec §6.1). Idempotent. */
1267
1601
  async verify(paymentId) {
1268
1602
  requireField(paymentId, "paymentId");
1603
+ if (this.config.mock) {
1604
+ const cached = this.mockPayments.get(paymentId);
1605
+ if (!cached) {
1606
+ throw new ApiError(`payment not found: ${paymentId}`, { status: 404, code: "not_found" });
1607
+ }
1608
+ const m = mockVerifyResult(cached);
1609
+ return {
1610
+ id: m.id,
1611
+ status: m.status,
1612
+ amount: m.amount,
1613
+ fee: m.fee,
1614
+ net: m.net,
1615
+ txHash: m.txHash,
1616
+ playerId: m.playerId ?? "",
1617
+ sku: m.sku,
1618
+ roundId: m.roundId,
1619
+ chain: m.chain,
1620
+ verifiedVia: "cache"
1621
+ };
1622
+ }
1269
1623
  return this.http.get(`/payments/${encodeURIComponent(paymentId)}`);
1270
1624
  }
1271
1625
  // ---- internals ---------------------------------------------------------
1626
+ /**
1627
+ * x402 V1 is Base Sepolia + test keys only (ADR D5). Fail closed before any
1628
+ * network call so mainnet / live keys never silently hit the adapter.
1629
+ */
1630
+ assertX402Allowed() {
1631
+ if (!this.env.isTest || this.env.network !== "base-sepolia") {
1632
+ throw new ConfigError(
1633
+ 'playmos.x402 is Base Sepolia / test-key only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
1634
+ { network: this.env.network, isTest: this.env.isTest }
1635
+ );
1636
+ }
1637
+ }
1272
1638
  /**
1273
1639
  * Map a server-settled payment (from the `settle: "server"` response) into the
1274
1640
  * SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
@@ -1449,10 +1815,10 @@ var PayoutError = class extends Error {
1449
1815
  this.name = "PayoutError";
1450
1816
  }
1451
1817
  };
1452
- var ADDRESS_RE2 = /^0x[0-9a-fA-F]{40}$/;
1818
+ var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
1453
1819
  var BPS = 10000n;
1454
- function requireAddress(w, i) {
1455
- if (!ADDRESS_RE2.test(w)) {
1820
+ function requireAddress2(w, i) {
1821
+ if (!ADDRESS_RE3.test(w)) {
1456
1822
  throw new PayoutError(`ranking[${i}] must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(w)}`);
1457
1823
  }
1458
1824
  return w.toLowerCase();
@@ -1475,7 +1841,7 @@ function computePayout(pool, ranking, rule) {
1475
1841
  if (!Array.isArray(ranking) || ranking.length === 0) {
1476
1842
  throw new PayoutError("ranking must be a non-empty array of wallets (best-first)");
1477
1843
  }
1478
- const wallets = ranking.map((w, i) => requireAddress(w, i));
1844
+ const wallets = ranking.map((w, i) => requireAddress2(w, i));
1479
1845
  const seen = /* @__PURE__ */ new Set();
1480
1846
  for (const w of wallets) {
1481
1847
  if (seen.has(w)) throw new PayoutError(`duplicate wallet in ranking: ${w}`);
@@ -1548,13 +1914,13 @@ function computePayout(pool, ranking, rule) {
1548
1914
  }
1549
1915
 
1550
1916
  // src/settlement.ts
1551
- var ADDRESS_RE3 = /^0x[0-9a-fA-F]{40}$/;
1917
+ var ADDRESS_RE4 = /^0x[0-9a-fA-F]{40}$/;
1552
1918
  var DEFAULT_TTL_MS = 15 * 60 * 1e3;
1553
- function requireAddress2(value, field) {
1919
+ function requireAddress3(value, field) {
1554
1920
  if (typeof value !== "string" || value.trim() === "") {
1555
1921
  throw new MissingFieldError(field);
1556
1922
  }
1557
- if (!ADDRESS_RE3.test(value)) {
1923
+ if (!ADDRESS_RE4.test(value)) {
1558
1924
  throw new ConfigError(`${field} must be a 0x-prefixed 20-byte address, got: ${JSON.stringify(value)}`, {
1559
1925
  field,
1560
1926
  value
@@ -1571,7 +1937,7 @@ function requireNetwork(value) {
1571
1937
  }
1572
1938
  function createPaymentRequirement(input) {
1573
1939
  const now = (input.now ?? (() => /* @__PURE__ */ new Date()))();
1574
- const payTo = requireAddress2(input.payTo, "payTo");
1940
+ const payTo = requireAddress3(input.payTo, "payTo");
1575
1941
  const network = requireNetwork(input.network);
1576
1942
  const asset = input.asset ?? "USDC";
1577
1943
  if (asset !== "USDC") {
@@ -1614,7 +1980,7 @@ function parsePaymentRequirement(input) {
1614
1980
  asset: o.asset
1615
1981
  });
1616
1982
  }
1617
- const payTo = requireAddress2(o.payTo, "payTo");
1983
+ const payTo = requireAddress3(o.payTo, "payTo");
1618
1984
  const network = requireNetwork(o.network);
1619
1985
  if (typeof o.amount !== "string") throw new InvalidAmountError(o.amount);
1620
1986
  parseUsdToMicro(o.amount);
@@ -1661,9 +2027,13 @@ exports.computeIapSplit = computeIapSplit;
1661
2027
  exports.computePayout = computePayout;
1662
2028
  exports.computePoolSplit = computePoolSplit;
1663
2029
  exports.createPaymentRequirement = createPaymentRequirement;
2030
+ exports.createX402Challenge = createX402Challenge;
2031
+ exports.decodePaymentHeader = decodePaymentHeader;
2032
+ exports.encodePaymentHeader = encodePaymentHeader;
1664
2033
  exports.formatMicroToUsd = formatMicroToUsd;
1665
2034
  exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
1666
2035
  exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
2036
+ exports.networkToCaip2 = networkToCaip2;
1667
2037
  exports.parsePaymentRequirement = parsePaymentRequirement;
1668
2038
  exports.parseUsdToMicro = parseUsdToMicro;
1669
2039
  exports.prefixedId = prefixedId;
@@ -1673,6 +2043,8 @@ exports.previewMarketplaceSplit = previewMarketplaceSplit;
1673
2043
  exports.previewPoolSplit = previewPoolSplit;
1674
2044
  exports.previewTransferSplit = previewTransferSplit;
1675
2045
  exports.serializePaymentRequirement = serializePaymentRequirement;
2046
+ exports.toX402PaymentRequired = toX402PaymentRequired;
1676
2047
  exports.ulid = ulid;
2048
+ exports.validateX402ChallengeInput = validateX402ChallengeInput;
1677
2049
  //# sourceMappingURL=index.cjs.map
1678
2050
  //# sourceMappingURL=index.cjs.map