@playmos/sdk 0.3.3 → 0.3.5

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
@@ -85,9 +85,26 @@ var DEFAULT_API_BASE_URL = {
85
85
  base: "https://api.playmos.io/v1",
86
86
  "base-sepolia": "https://api.sandbox.playmos.io/v1"
87
87
  };
88
+ function clientRuntimeSignals(g = globalThis) {
89
+ const any = g;
90
+ const signals = [];
91
+ if (typeof any.window !== "undefined") signals.push("window");
92
+ if (typeof any.document !== "undefined") signals.push("document");
93
+ const likelyCfWorker = typeof any.Cloudflare !== "undefined" || typeof any.WebSocketPair !== "undefined" && typeof any.window === "undefined";
94
+ if (typeof any.importScripts === "function" && !likelyCfWorker) {
95
+ signals.push("importScripts");
96
+ }
97
+ if (any.navigator?.product === "ReactNative") signals.push("ReactNative");
98
+ return signals;
99
+ }
100
+ function isClientRuntime(g = globalThis) {
101
+ return clientRuntimeSignals(g).length > 0;
102
+ }
88
103
  function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
89
104
  if (typeof apiKey !== "string" || apiKey.trim() === "") {
90
- throw new AuthError("Missing apiKey. Pass a pk_test_\u2026 or pk_live_\u2026 key.");
105
+ throw new AuthError(
106
+ 'Missing apiKey. Expected a "pk_test_", "pk_live_", "sk_test_", or "sk_live_" prefix.'
107
+ );
91
108
  }
92
109
  const isTest = apiKey.startsWith("pk_test_") || apiKey.startsWith("sk_test_");
93
110
  const isLive = apiKey.startsWith("pk_live_") || apiKey.startsWith("sk_live_");
@@ -97,12 +114,14 @@ function resolveEnv(apiKey, networkOverride, apiBaseUrlOverride) {
97
114
  { keyPreview: apiKey.slice(0, 8) }
98
115
  );
99
116
  }
117
+ const isSecret = apiKey.startsWith("sk_");
100
118
  const network = networkOverride ?? (isTest ? "base-sepolia" : "base");
101
119
  return {
102
120
  network,
103
121
  chainId: CHAIN_ID[network],
104
122
  apiBaseUrl: apiBaseUrlOverride ?? DEFAULT_API_BASE_URL[network],
105
- isTest
123
+ isTest,
124
+ isSecret
106
125
  };
107
126
  }
108
127
 
@@ -410,13 +429,6 @@ var erc20Abi = [
410
429
  { name: "amount", type: "uint256" }
411
430
  ],
412
431
  outputs: [{ type: "bool" }]
413
- },
414
- {
415
- type: "function",
416
- name: "balanceOf",
417
- stateMutability: "view",
418
- inputs: [{ name: "account", type: "address" }],
419
- outputs: [{ type: "uint256" }]
420
432
  }
421
433
  ];
422
434
  var playmosPayAbi = [
@@ -433,17 +445,6 @@ var playmosPayAbi = [
433
445
  }
434
446
  ];
435
447
  var prizePoolAbi = [
436
- {
437
- type: "function",
438
- name: "openRound",
439
- stateMutability: "nonpayable",
440
- inputs: [
441
- { name: "roundId", type: "bytes32" },
442
- { name: "series", type: "bytes32" },
443
- { name: "entry", type: "uint256" }
444
- ],
445
- outputs: []
446
- },
447
448
  {
448
449
  type: "function",
449
450
  name: "enter",
@@ -454,50 +455,6 @@ var prizePoolAbi = [
454
455
  ],
455
456
  outputs: []
456
457
  },
457
- {
458
- type: "function",
459
- name: "lockRound",
460
- stateMutability: "nonpayable",
461
- inputs: [{ name: "roundId", type: "bytes32" }],
462
- outputs: []
463
- },
464
- {
465
- type: "function",
466
- name: "settle",
467
- stateMutability: "nonpayable",
468
- inputs: [
469
- { name: "roundId", type: "bytes32" },
470
- { name: "winners", type: "address[]" },
471
- { name: "amounts", type: "uint256[]" }
472
- ],
473
- outputs: []
474
- },
475
- {
476
- type: "function",
477
- name: "getRound",
478
- stateMutability: "view",
479
- inputs: [{ name: "roundId", type: "bytes32" }],
480
- outputs: [
481
- { name: "state", type: "uint8" },
482
- { name: "series", type: "bytes32" },
483
- { name: "entry", type: "uint256" },
484
- { name: "pot", type: "uint256" },
485
- { name: "entrantCount", type: "uint256" },
486
- { name: "payable_", type: "uint256" },
487
- { name: "inheritedSeed", type: "uint256" },
488
- { name: "lockedAt", type: "uint256" }
489
- ]
490
- },
491
- {
492
- type: "function",
493
- name: "hasEntered",
494
- stateMutability: "view",
495
- inputs: [
496
- { name: "roundId", type: "bytes32" },
497
- { name: "identity", type: "bytes32" }
498
- ],
499
- outputs: [{ type: "bool" }]
500
- },
501
458
  {
502
459
  type: "function",
503
460
  name: "withdrawable",
@@ -621,7 +578,7 @@ function buildWithdrawCall(prizePool) {
621
578
  }
622
579
 
623
580
  // src/mock.ts
624
- var MOCK_TX = "0x000000000000000000000000000000000000000000000000000000000000mock";
581
+ var MOCK_TX = `0x${"0".repeat(56)}deadbeef`;
625
582
  function mockIapPayment(args) {
626
583
  const { feeMicro, netMicro } = computeIapSplit(args.amountMicro, args.feeBps);
627
584
  return {
@@ -869,10 +826,16 @@ var Playmos = class {
869
826
  }
870
827
  };
871
828
  this.payouts = {
872
- /** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). */
873
- setMode: (mode) => this.http.post("/payouts/mode", { mode }),
874
- /** Create a Bridge KYC onboarding link (fiat payout). */
875
- createOnboardingLink: () => this.http.post("/payouts/onboarding_link", {})
829
+ /** Choose how the studio is paid: "usdc" (default) or "fiat" (Bridge). Secret key only (#264). */
830
+ setMode: async (mode) => {
831
+ this.assertSecretKey("playmos.payouts.setMode");
832
+ return this.http.post("/payouts/mode", { mode });
833
+ },
834
+ /** Create a Bridge KYC onboarding link (fiat payout). Secret key only (#264). */
835
+ createOnboardingLink: async () => {
836
+ this.assertSecretKey("playmos.payouts.createOnboardingLink");
837
+ return this.http.post("/payouts/onboarding_link", {});
838
+ }
876
839
  };
877
840
  /**
878
841
  * `x402` — Phase 4 HTTP 402 / x402-shaped adapter (#153).
@@ -922,6 +885,10 @@ var Playmos = class {
922
885
  * Settle a previously minted challenge with an `x402-payload` authorization.
923
886
  * Defaults mode to `server-signer` when omitted (explicit on the wire — fail-closed
924
887
  * materialize still requires mode; the route also defaults for this path).
888
+ *
889
+ * #259/#269: settle uses `idempotency-key = requirement.id` and auto-retries gateway 504.
890
+ * A 504 replay may return **without throwing** with `success: false`, `status: "settling"`,
891
+ * `txHash: null` (in-flight). Treat as non-final — re-call or verify later; do not new-key.
925
892
  */
926
893
  fulfill: async (requirement, authorization) => {
927
894
  this.assertX402Allowed();
@@ -955,10 +922,19 @@ var Playmos = class {
955
922
  }
956
923
  if (payload.scheme === void 0) payload.scheme = "exact";
957
924
  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
- });
925
+ const res = await this.http.post(
926
+ "/x402/settle",
927
+ {
928
+ requirement,
929
+ authorization: { kind: "x402-payload", payload }
930
+ },
931
+ // Settlement idempotency anchor = requirement.id (settlement.ts: "Settlement
932
+ // idempotency key + x402 challenge id"). Without this header the settle POST is
933
+ // excluded from the safe gateway-504 auto-retry (isRetryable), and the SDK's own
934
+ // 504 message ("retry with the same idempotency key (safe)") would be a lie —
935
+ // a 504 is exactly when the on-chain settle may have already landed (#259).
936
+ { idempotencyKey: requirement.id }
937
+ );
962
938
  const s = res.settlement;
963
939
  return {
964
940
  requirementId: s.requirementId,
@@ -1001,8 +977,11 @@ var Playmos = class {
1001
977
  */
1002
978
  /** Offline mock store for rounds.open/lock/settle/get when `mock: true` (3-game dogfood). */
1003
979
  this.mockRounds = /* @__PURE__ */ new Map();
980
+ /** Mock withdrawable credits after settle — key `${roundId}:${walletLower}` (#240 offline prize). */
981
+ this.mockWithdrawable = /* @__PURE__ */ new Map();
1004
982
  this.rounds = {
1005
983
  open: async (input) => {
984
+ this.assertSecretKey("playmos.rounds.open");
1006
985
  requireField(input?.gameId, "gameId");
1007
986
  requireField(input?.roundId, "roundId");
1008
987
  requireField(input?.entryAmount, "entryAmount");
@@ -1022,34 +1001,41 @@ var Playmos = class {
1022
1001
  code: "conflict"
1023
1002
  });
1024
1003
  }
1025
- const round2 = {
1004
+ const round = {
1026
1005
  roundId: input.roundId,
1027
1006
  gameId: input.gameId,
1028
1007
  roundKey,
1008
+ // #351 — must persist seriesKey so activeForSeries mock is authoritative
1009
+ seriesKey: input.seriesKey ?? input.gameId,
1029
1010
  status: "open",
1030
1011
  entryAmount: input.entryAmount,
1031
1012
  pool: null,
1032
1013
  entrants: 0,
1033
1014
  payout: input.payout,
1034
1015
  closeAt: input.closeAt,
1035
- openTxHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
1016
+ openTxHash: MOCK_TX,
1036
1017
  prizePoolAddress: this.config.contracts?.prizePool ?? "0x0000000000000000000000000000000000000002"
1037
1018
  };
1038
- this.mockRounds.set(input.roundId, round2);
1039
- return round2;
1019
+ this.mockRounds.set(input.roundId, round);
1020
+ return round;
1040
1021
  }
1041
- const { round } = await this.http.post("/rounds", {
1042
- gameId: input.gameId,
1043
- roundId: input.roundId,
1044
- entryAmount: input.entryAmount,
1045
- payout: input.payout,
1046
- closeAt: input.closeAt,
1047
- seriesKey: input.seriesKey,
1048
- roundKey: input.roundKey
1049
- });
1050
- return round;
1022
+ const body = await this.http.post(
1023
+ "/rounds",
1024
+ {
1025
+ gameId: input.gameId,
1026
+ roundId: input.roundId,
1027
+ entryAmount: input.entryAmount,
1028
+ payout: input.payout,
1029
+ closeAt: input.closeAt,
1030
+ seriesKey: input.seriesKey,
1031
+ roundKey: input.roundKey
1032
+ },
1033
+ { acceptStatuses: [202] }
1034
+ );
1035
+ return body.round;
1051
1036
  },
1052
1037
  lock: async (input) => {
1038
+ this.assertSecretKey("playmos.rounds.lock");
1053
1039
  requireField(input?.roundId, "roundId");
1054
1040
  if (this.config.mock) {
1055
1041
  const existing = this.mockRounds.get(input.roundId);
@@ -1069,7 +1055,7 @@ var Playmos = class {
1069
1055
  // Mock payable pool ≈ 60% of entry × max(1, entrants) for shape only — not money truth.
1070
1056
  pool: existing.pool ?? existing.entryAmount,
1071
1057
  entrants: existing.entrants ?? 0,
1072
- lockTxHash: "0x000000000000000000000000000000000000000000000000000000000000loc1"
1058
+ lockTxHash: MOCK_TX
1073
1059
  };
1074
1060
  this.mockRounds.set(input.roundId, locked);
1075
1061
  return locked;
@@ -1081,6 +1067,7 @@ var Playmos = class {
1081
1067
  return round;
1082
1068
  },
1083
1069
  settle: async (input) => {
1070
+ this.assertSecretKey("playmos.rounds.settle");
1084
1071
  requireField(input?.roundId, "roundId");
1085
1072
  if (!input?.results) throw new ConfigError("results are required (ranking or winners)");
1086
1073
  if (this.config.mock) {
@@ -1111,7 +1098,7 @@ var Playmos = class {
1111
1098
  // Winner-take-all mock shape when ranking only.
1112
1099
  amount: i === 0 ? existing.pool ?? existing.entryAmount : "0"
1113
1100
  }));
1114
- const settleTxHash = "0x000000000000000000000000000000000000000000000000000000000000set1";
1101
+ const settleTxHash = MOCK_TX;
1115
1102
  const poolPaid = existing.pool ?? existing.entryAmount;
1116
1103
  const settled = {
1117
1104
  ...existing,
@@ -1120,6 +1107,19 @@ var Playmos = class {
1120
1107
  pool: poolPaid
1121
1108
  };
1122
1109
  this.mockRounds.set(input.roundId, settled);
1110
+ for (const w of winners) {
1111
+ const wAddr = String(w.wallet).toLowerCase();
1112
+ let micro = 0n;
1113
+ try {
1114
+ micro = validateAmount(String(w.amount ?? "0"));
1115
+ } catch {
1116
+ micro = 0n;
1117
+ }
1118
+ if (micro > 0n) {
1119
+ const k = `${input.roundId}:${wAddr}`;
1120
+ this.mockWithdrawable.set(k, (this.mockWithdrawable.get(k) ?? 0n) + micro);
1121
+ }
1122
+ }
1123
1123
  return {
1124
1124
  roundId: input.roundId,
1125
1125
  txHash: settleTxHash,
@@ -1130,10 +1130,65 @@ var Playmos = class {
1130
1130
  }
1131
1131
  const { settle } = await this.http.post(
1132
1132
  `/rounds/${encodeURIComponent(input.roundId)}/settle`,
1133
- { gameId: input.gameId, results: input.results }
1133
+ { gameId: input.gameId, results: input.results },
1134
+ { acceptStatuses: [202] }
1134
1135
  );
1135
1136
  return settle;
1136
1137
  },
1138
+ /**
1139
+ * Cancel an open/locked round (operator sk_ only). Refunds entrants' escrowed ~90%
1140
+ * and frees the series latch. May return `status: "cancelling"` (HTTP 202) until
1141
+ * chain Cancelled(4) reconciles — poll `rounds.get` or re-call cancel (#346).
1142
+ */
1143
+ cancel: async (input) => {
1144
+ this.assertSecretKey("playmos.rounds.cancel");
1145
+ requireField(input?.roundId, "roundId");
1146
+ if (this.config.mock) {
1147
+ const existing = this.mockRounds.get(input.roundId);
1148
+ if (!existing) {
1149
+ throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
1150
+ }
1151
+ if (existing.status === "cancelled") {
1152
+ return {
1153
+ roundId: input.roundId,
1154
+ txHash: existing.cancelTxHash ?? MOCK_TX,
1155
+ status: "cancelled",
1156
+ round: existing
1157
+ };
1158
+ }
1159
+ if (existing.status === "settled") {
1160
+ throw new ApiError(`round ${input.roundId} is already settled \u2014 cannot cancel`, {
1161
+ status: 409,
1162
+ code: "conflict"
1163
+ });
1164
+ }
1165
+ const cancelled = {
1166
+ ...existing,
1167
+ status: "cancelled",
1168
+ cancelTxHash: MOCK_TX,
1169
+ pool: null
1170
+ };
1171
+ this.mockRounds.set(input.roundId, cancelled);
1172
+ return {
1173
+ roundId: input.roundId,
1174
+ txHash: MOCK_TX,
1175
+ status: "cancelled",
1176
+ round: cancelled
1177
+ };
1178
+ }
1179
+ const body = await this.http.post(
1180
+ `/rounds/${encodeURIComponent(input.roundId)}/cancel`,
1181
+ { gameId: input.gameId },
1182
+ { acceptStatuses: [202] }
1183
+ );
1184
+ const status = body.status === "cancelling" || body.round?.status === "cancelling" ? "cancelling" : "cancelled";
1185
+ return {
1186
+ roundId: input.roundId,
1187
+ txHash: body.txHash ?? body.round?.cancelTxHash ?? null,
1188
+ status,
1189
+ round: body.round
1190
+ };
1191
+ },
1137
1192
  get: async (input) => {
1138
1193
  requireField(input?.roundId, "roundId");
1139
1194
  if (this.config.mock) {
@@ -1148,6 +1203,58 @@ var Playmos = class {
1148
1203
  );
1149
1204
  return round;
1150
1205
  },
1206
+ /**
1207
+ * Authoritative series latch (#351) — `null` means **proven free**.
1208
+ * `GET /v1/series/:seriesKey/active?gameId=…` — not a candidate probe.
1209
+ */
1210
+ activeForSeries: async (input) => {
1211
+ requireField(input?.seriesKey, "seriesKey");
1212
+ requireField(input?.gameId, "gameId");
1213
+ if (this.config.mock) {
1214
+ for (const r of this.mockRounds.values()) {
1215
+ if (r.gameId !== input.gameId) continue;
1216
+ const sk = r.seriesKey ?? r.gameId;
1217
+ if (sk !== input.seriesKey) continue;
1218
+ if (r.status === "open" || r.status === "locked" || r.status === "opening" || r.status === "cancelling") {
1219
+ return {
1220
+ roundId: r.roundId,
1221
+ roundKey: r.roundKey,
1222
+ roundIdHash: "0x" + "m".repeat(64),
1223
+ status: r.status,
1224
+ entrants: r.entrants,
1225
+ pool: r.pool,
1226
+ openTxHash: r.openTxHash ?? null,
1227
+ cancelTxHash: r.cancelTxHash ?? null,
1228
+ prizePoolAddress: r.prizePoolAddress ?? "0x0000000000000000000000000000000000000002"
1229
+ };
1230
+ }
1231
+ }
1232
+ return null;
1233
+ }
1234
+ const body = await this.http.get(
1235
+ `/series/${encodeURIComponent(input.seriesKey)}/active?gameId=${encodeURIComponent(input.gameId)}`
1236
+ );
1237
+ if (body == null || typeof body !== "object") {
1238
+ throw new ApiError("activeForSeries: malformed response (expected object body)", {
1239
+ code: "invalid_response",
1240
+ status: 502
1241
+ });
1242
+ }
1243
+ if (!("active" in body)) {
1244
+ throw new ApiError("activeForSeries: malformed response (missing active field)", {
1245
+ code: "invalid_response",
1246
+ status: 502
1247
+ });
1248
+ }
1249
+ if (body.active === null) return null;
1250
+ if (body.active == null || typeof body.active !== "object") {
1251
+ throw new ApiError("activeForSeries: malformed response (active must be object or null)", {
1252
+ code: "invalid_response",
1253
+ status: 502
1254
+ });
1255
+ }
1256
+ return body.active;
1257
+ },
1151
1258
  /**
1152
1259
  * Read a wallet's **claimable** prize balance for a round (issue #41).
1153
1260
  * Prefers the service chain read (`GET /v1/rounds/:id/prize?wallet=`); falls
@@ -1158,6 +1265,21 @@ var Playmos = class {
1158
1265
  prize: async (input) => {
1159
1266
  requireField(input?.roundId, "roundId");
1160
1267
  const wallet = requireAddressField(input?.wallet, "wallet");
1268
+ if (this.config.mock) {
1269
+ const existing = this.mockRounds.get(input.roundId);
1270
+ if (!existing) {
1271
+ throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
1272
+ }
1273
+ const claimableMicro2 = this.mockWithdrawable.get(`${input.roundId}:${wallet.toLowerCase()}`) ?? 0n;
1274
+ const prizePoolAddress2 = input.prizePoolAddress ?? existing.prizePoolAddress ?? "0x0000000000000000000000000000000000000002";
1275
+ return {
1276
+ roundId: input.roundId,
1277
+ prizePoolAddress: prizePoolAddress2,
1278
+ wallet,
1279
+ claimable: formatMicroToUsd(claimableMicro2),
1280
+ claimableMicro: claimableMicro2.toString()
1281
+ };
1282
+ }
1161
1283
  try {
1162
1284
  const res = await this.http.get(
1163
1285
  `/rounds/${encodeURIComponent(input.roundId)}/prize?wallet=${encodeURIComponent(wallet)}`
@@ -1184,11 +1306,24 @@ var Playmos = class {
1184
1306
  */
1185
1307
  withdraw: async (input) => {
1186
1308
  if (this.config.mock) {
1309
+ let amountMicro2 = 0n;
1310
+ let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
1311
+ if (input.roundId) {
1312
+ const existing = this.mockRounds.get(input.roundId);
1313
+ if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
1314
+ for (const [k, v] of this.mockWithdrawable) {
1315
+ if (k.startsWith(`${input.roundId}:`) && v > 0n) {
1316
+ amountMicro2 = v;
1317
+ this.mockWithdrawable.set(k, 0n);
1318
+ break;
1319
+ }
1320
+ }
1321
+ }
1187
1322
  return {
1188
- prizePoolAddress: input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001",
1189
- amount: "0.00",
1190
- amountMicro: "0",
1191
- txHash: "0x000000000000000000000000000000000000000000000000000000000000mock",
1323
+ prizePoolAddress: prizePool2,
1324
+ amount: formatMicroToUsd(amountMicro2),
1325
+ amountMicro: amountMicro2.toString(),
1326
+ txHash: MOCK_TX,
1192
1327
  status: "confirmed"
1193
1328
  };
1194
1329
  }
@@ -1263,23 +1398,27 @@ var Playmos = class {
1263
1398
  this.agents = {
1264
1399
  /** Assign (or return) the wallet for a game NPC id. Idempotent — safe to call wherever your NPCs spawn. */
1265
1400
  createWallet: async (input) => {
1401
+ this.assertSecretKey("playmos.agents");
1266
1402
  requireField(input?.agentId, "agentId");
1267
1403
  const { agent } = await this.http.post("/agents/wallets", { agentId: input.agentId });
1268
1404
  return agent;
1269
1405
  },
1270
1406
  /** Resolve one NPC's wallet by your id. */
1271
1407
  wallet: async (agentId) => {
1408
+ this.assertSecretKey("playmos.agents");
1272
1409
  requireField(agentId, "agentId");
1273
1410
  const { agent } = await this.http.get(`/agents/wallets/${encodeURIComponent(agentId)}`);
1274
1411
  return agent;
1275
1412
  },
1276
1413
  /** List the NPC wallets you've assigned in this studio. */
1277
1414
  list: async () => {
1415
+ this.assertSecretKey("playmos.agents");
1278
1416
  const { agents } = await this.http.get("/agents/wallets");
1279
1417
  return agents;
1280
1418
  },
1281
1419
  /** Sandbox faucet: fund an NPC with USDC from your treasury (per-NPC lifetime cap). */
1282
- fund: (input) => {
1420
+ fund: async (input) => {
1421
+ this.assertSecretKey("playmos.agents.fund");
1283
1422
  requireField(input?.agentId, "agentId");
1284
1423
  validateAmount(input.amount);
1285
1424
  const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
@@ -1290,10 +1429,17 @@ var Playmos = class {
1290
1429
  );
1291
1430
  },
1292
1431
  /** NPC→NPC (or →player) USDC transfer, by id. A thin alias of `playmos.transfer` (which is canonical). */
1293
- pay: (input) => {
1432
+ pay: async (input) => {
1433
+ this.assertSecretKey("playmos.agents.pay");
1294
1434
  requireField(input?.from, "from");
1295
1435
  requireField(input?.to, "to");
1296
- return this.transfer({ from: input.from, to: input.to, amount: input.amount, feeBps: input.feeBps, feeSink: input.feeSink });
1436
+ return this.transfer({
1437
+ from: input.from,
1438
+ to: input.to,
1439
+ amount: input.amount,
1440
+ feeBps: input.feeBps,
1441
+ feeSink: input.feeSink
1442
+ });
1297
1443
  }
1298
1444
  };
1299
1445
  /**
@@ -1477,6 +1623,15 @@ var Playmos = class {
1477
1623
  };
1478
1624
  this.config = config;
1479
1625
  this.env = resolveEnv(config.apiKey, config.network, config.apiBaseUrl);
1626
+ if (this.env.isSecret) {
1627
+ const signals = clientRuntimeSignals();
1628
+ if (signals.length > 0) {
1629
+ throw new AuthError(
1630
+ "Secret (sk_) key used in a client runtime \u2014 sk_ keys are server-only; use a pk_ key client-side.",
1631
+ { isSecret: true, clientRuntime: true, signals }
1632
+ );
1633
+ }
1634
+ }
1480
1635
  this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
1481
1636
  }
1482
1637
  /** Connect the player's wallet and return their address. */
@@ -1504,9 +1659,10 @@ var Playmos = class {
1504
1659
  roundId: req.roundKey,
1505
1660
  roundKey: req.roundKey,
1506
1661
  identity: req.identity,
1507
- amount: formatMicroToUsd(req.entryUnits),
1508
- playerId: req.wallet
1662
+ playerId: req.wallet,
1663
+ amount: formatMicroToUsd(req.entryUnits)
1509
1664
  });
1665
+ this.rememberMock(entry);
1510
1666
  const raw = String(entry.status ?? "pending").toLowerCase();
1511
1667
  const status = raw === "confirmed" ? "confirmed" : raw === "failed" ? "failed" : "pending";
1512
1668
  const onchain = !entry.mock;
@@ -1546,14 +1702,39 @@ var Playmos = class {
1546
1702
  if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
1547
1703
  const res = await this.http.post(
1548
1704
  "/payments",
1549
- { kind: "iap", amount: input.amount, sku: input.sku, playerId: input.playerId, gameId: input.gameId, studio: input.studio, metadata, settle: "server" },
1550
- { idempotencyKey }
1705
+ {
1706
+ kind: "iap",
1707
+ amount: input.amount,
1708
+ sku: input.sku,
1709
+ playerId: input.playerId,
1710
+ gameId: input.gameId,
1711
+ studio: input.studio,
1712
+ metadata,
1713
+ settle: "server",
1714
+ idempotencyKey
1715
+ /* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
1716
+ },
1717
+ { idempotencyKey, acceptStatuses: [201, 202] }
1551
1718
  );
1552
- return this.mapServerPayment(res.payment, "iap", amountMicro);
1719
+ let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
1720
+ if (payment.status === "pending" || payment.status === "created") {
1721
+ payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
1722
+ }
1723
+ return payment;
1553
1724
  }
1554
1725
  const intent = await this.http.post(
1555
1726
  "/payments",
1556
- { kind: "iap", amount: input.amount, sku: input.sku, playerId: input.playerId, gameId: input.gameId, studio: input.studio, metadata },
1727
+ {
1728
+ kind: "iap",
1729
+ amount: input.amount,
1730
+ sku: input.sku,
1731
+ playerId: input.playerId,
1732
+ gameId: input.gameId,
1733
+ studio: input.studio,
1734
+ metadata,
1735
+ idempotencyKey
1736
+ /* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
1737
+ },
1557
1738
  { idempotencyKey }
1558
1739
  );
1559
1740
  const provider = resolveProvider(this.config.wallet);
@@ -1602,23 +1783,48 @@ var Playmos = class {
1602
1783
  );
1603
1784
  }
1604
1785
  if (this.env.isTest && !await walletAvailable(this.config.wallet)) {
1786
+ const pinnedRoundKey = input.roundKey ?? input.roundId;
1605
1787
  const res = await this.http.post(
1606
1788
  "/payments",
1607
- { kind: "entry", amount: input.amount, gameId: input.gameId, roundId: input.roundKey ?? input.roundId, identity: input.identity, playerId: input.playerId, metadata, settle: "server" },
1608
- { idempotencyKey }
1789
+ {
1790
+ kind: "entry",
1791
+ amount: input.amount,
1792
+ gameId: input.gameId,
1793
+ roundId: pinnedRoundKey,
1794
+ identity: input.identity,
1795
+ playerId: input.playerId,
1796
+ metadata,
1797
+ settle: "server",
1798
+ idempotencyKey
1799
+ /* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
1800
+ },
1801
+ { idempotencyKey, acceptStatuses: [201, 202] }
1609
1802
  );
1610
- const payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
1803
+ let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
1804
+ if (payment2.status === "pending" || payment2.status === "created") {
1805
+ payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
1806
+ }
1611
1807
  if (input.identity) payment2.identity = input.identity;
1612
1808
  const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
1613
1809
  if (pool) payment2.prizePoolAddress = pool.toLowerCase();
1614
- payment2.roundKey = res.clientParams?.roundKey ?? input.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
1810
+ payment2.roundKey = input.roundKey ?? res.clientParams?.roundKey ?? res.clientParams?.roundId ?? payment2.roundId ?? input.roundId;
1615
1811
  return payment2;
1616
1812
  }
1617
1813
  const intent = await this.http.post(
1618
1814
  "/payments",
1619
1815
  // When the game supplies its own roundKey, that IS the round the service
1620
1816
  // stores + verifies against (so on-chain enter and server hasEntered align).
1621
- { kind: "entry", amount: input.amount, gameId: input.gameId, roundId: input.roundKey ?? input.roundId, identity: input.identity, playerId: input.playerId, metadata },
1817
+ {
1818
+ kind: "entry",
1819
+ amount: input.amount,
1820
+ gameId: input.gameId,
1821
+ roundId: input.roundKey ?? input.roundId,
1822
+ identity: input.identity,
1823
+ playerId: input.playerId,
1824
+ metadata,
1825
+ idempotencyKey
1826
+ /* MUST be in the BODY — the service reads body.idempotencyKey, not the header (#257 P1b) */
1827
+ },
1622
1828
  { idempotencyKey }
1623
1829
  );
1624
1830
  const provider = resolveProvider(this.config.wallet);
@@ -1661,6 +1867,7 @@ var Playmos = class {
1661
1867
  * NPC `from` requires `sk_test_` and settles gaslessly (NPC signs; service relays).
1662
1868
  */
1663
1869
  async transfer(input, opts) {
1870
+ this.assertSecretKey("playmos.transfer");
1664
1871
  const amountMicro = validateAmount(input.amount);
1665
1872
  const from = input.from === void 0 ? void 0 : partyRef(input.from, "from");
1666
1873
  const to = partyRef(input.to, "to");
@@ -1730,17 +1937,56 @@ var Playmos = class {
1730
1937
  }
1731
1938
  // ---- internals ---------------------------------------------------------
1732
1939
  /**
1733
- * x402 V1 is Base Sepolia + test keys only (ADR D5). Fail closed before any
1734
- * network call so mainnet / live keys never silently hit the adapter.
1940
+ * Secret (sk_) surfaces agents / transfer / x402. Capability tracks key type
1941
+ * in code, not README prose (#260). pk_ keys are client-safe and refuse these.
1942
+ */
1943
+ assertSecretKey(surface) {
1944
+ if (!this.env.isSecret) {
1945
+ throw new AuthError(
1946
+ `${surface} requires a secret (sk_) key \u2014 use sk_test_\u2026 / sk_live_\u2026 on the server. Publishable (pk_) keys are client-safe and cannot call this surface.`,
1947
+ { surface, isSecret: false }
1948
+ );
1949
+ }
1950
+ }
1951
+ /**
1952
+ * x402 V1 is Base Sepolia + secret test keys only (ADR D5 + #260). Fail closed
1953
+ * before any network call so mainnet / live / publishable keys never hit the adapter.
1954
+ * Does not change fulfill/settle body shape (A owns #261 / #259 money path).
1735
1955
  */
1736
1956
  assertX402Allowed() {
1957
+ this.assertSecretKey("playmos.x402");
1737
1958
  if (!this.env.isTest || this.env.network !== "base-sepolia") {
1738
1959
  throw new ConfigError(
1739
- 'playmos.x402 is Base Sepolia / test-key only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
1740
- { network: this.env.network, isTest: this.env.isTest }
1960
+ 'playmos.x402 is Base Sepolia / sk_test_ only in V1 (ADR D5). Use apiKey sk_test_\u2026 and network "base-sepolia".',
1961
+ { network: this.env.network, isTest: this.env.isTest, isSecret: this.env.isSecret }
1741
1962
  );
1742
1963
  }
1743
1964
  }
1965
+ /**
1966
+ * Poll GET /payments/:id after server-settle until terminal status.
1967
+ * - Entry (#221): service may return 202 pending (openRound+enter > DO edge).
1968
+ * - IAP: service often returns 201 + txHash while status is still "created"
1969
+ * until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
1970
+ * the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
1971
+ */
1972
+ async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
1973
+ const start = Date.now();
1974
+ let delayMs = 400;
1975
+ while (Date.now() - start < timeoutMs) {
1976
+ const raw = await this.http.get(
1977
+ `/payments/${encodeURIComponent(paymentId)}`
1978
+ );
1979
+ if (raw.status === "confirmed" || raw.status === "failed") {
1980
+ return this.mapServerPayment(raw, kind, amountMicro);
1981
+ }
1982
+ await new Promise((r) => setTimeout(r, delayMs));
1983
+ delayMs = Math.min(Math.floor(delayMs * 1.4), 2e3);
1984
+ }
1985
+ throw new ApiError(
1986
+ `Timed out after ${timeoutMs}ms waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
1987
+ { paymentId, timeout: true, asyncSettle: true }
1988
+ );
1989
+ }
1744
1990
  /**
1745
1991
  * Map a server-settled payment (from the `settle: "server"` response) into the
1746
1992
  * SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
@@ -2129,6 +2375,7 @@ exports.PlaymosError = PlaymosError;
2129
2375
  exports.USDC_ADDRESS = USDC_ADDRESS;
2130
2376
  exports.USDC_DECIMALS = USDC_DECIMALS;
2131
2377
  exports.WalletConnectionError = WalletConnectionError;
2378
+ exports.clientRuntimeSignals = clientRuntimeSignals;
2132
2379
  exports.computeIapSplit = computeIapSplit;
2133
2380
  exports.computePayout = computePayout;
2134
2381
  exports.computePoolSplit = computePoolSplit;
@@ -2137,6 +2384,7 @@ exports.createX402Challenge = createX402Challenge;
2137
2384
  exports.decodePaymentHeader = decodePaymentHeader;
2138
2385
  exports.encodePaymentHeader = encodePaymentHeader;
2139
2386
  exports.formatMicroToUsd = formatMicroToUsd;
2387
+ exports.isClientRuntime = isClientRuntime;
2140
2388
  exports.isWalletSignatureAuthorization = isWalletSignatureAuthorization;
2141
2389
  exports.isX402PayloadAuthorization = isX402PayloadAuthorization;
2142
2390
  exports.networkToCaip2 = networkToCaip2;
@@ -2148,9 +2396,8 @@ exports.previewIapSplit = previewIapSplit;
2148
2396
  exports.previewMarketplaceSplit = previewMarketplaceSplit;
2149
2397
  exports.previewPoolSplit = previewPoolSplit;
2150
2398
  exports.previewTransferSplit = previewTransferSplit;
2399
+ exports.resolveEnv = resolveEnv;
2151
2400
  exports.serializePaymentRequirement = serializePaymentRequirement;
2152
2401
  exports.toX402PaymentRequired = toX402PaymentRequired;
2153
2402
  exports.ulid = ulid;
2154
2403
  exports.validateX402ChallengeInput = validateX402ChallengeInput;
2155
- //# sourceMappingURL=index.cjs.map
2156
- //# sourceMappingURL=index.cjs.map