@playmos/sdk 0.3.9 → 0.3.11

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.
@@ -24,11 +24,19 @@ interface GasConfig {
24
24
  paymasterUrl?: string;
25
25
  }
26
26
  interface WalletConfig {
27
+ /**
28
+ * Which connector name to use when {@link WalletConfig.provider} is omitted.
29
+ * - `"injected"` — requires `globalThis.ethereum` (wallet browser / extension).
30
+ * - `"base-account"` (default) — **not** a built-in Base Account package inside
31
+ * `@playmos/sdk`. Resolution: use injected ethereum if present; otherwise you
32
+ * must install `@base-org/account` yourself, construct a provider, and pass
33
+ * it as `wallet.provider` (sdk#512). No hard dependency on `@base-org/account`.
34
+ */
27
35
  connector?: WalletConnector;
28
36
  /**
29
- * Pre-built EIP-1193 provider. If omitted, `injected` uses globalThis.ethereum
30
- * and `base-account` expects a provider supplied by the Base Account SDK host.
31
- * A raw provider is still **timeout-wrapped** on resolve (#462) — not a no-op passthrough.
37
+ * Pre-built EIP-1193 provider (host-owned). Always preferred when set.
38
+ * For Base Account / passkey hosts: construct with `@base-org/account` (or the
39
+ * Base App injection) and pass it here. Still **timeout-wrapped** on resolve (#462).
32
40
  */
33
41
  provider?: Eip1193Provider;
34
42
  }
@@ -129,6 +137,10 @@ interface PayInput {
129
137
  sku: string;
130
138
  /** Your opaque user id. */
131
139
  playerId: string;
140
+ /** sdk#508 — max wait for server-settle poll (ms). Default 60_000. */
141
+ settleTimeoutMs?: number;
142
+ /** sdk#508 — abort server-settle polling. */
143
+ signal?: AbortSignal;
132
144
  /**
133
145
  * The game this IAP belongs to. Optional: when your API key maps to exactly
134
146
  * one game the service resolves it for you (the quickstart). Supply it
@@ -150,6 +162,13 @@ interface EnterRoundInput {
150
162
  amount: string;
151
163
  playerId: string;
152
164
  idempotencyKey?: string;
165
+ /**
166
+ * sdk#508 — max wait for server-settle poll (ms). Default 60_000.
167
+ * Only applies to no-wallet sandbox server-settle path.
168
+ */
169
+ settleTimeoutMs?: number;
170
+ /** sdk#508 — abort server-settle polling. */
171
+ signal?: AbortSignal;
153
172
  metadata?: Record<string, string>;
154
173
  /**
155
174
  * Exact on-chain round key for Path B / entryProvider (#201 / #204 / #210).
@@ -387,10 +406,31 @@ interface Payment {
387
406
  status: PaymentStatus;
388
407
  /** "iap" (1% external) or "entry" (60/30/10 prize-pool). */
389
408
  kind: "iap" | "entry";
409
+ /**
410
+ * Requested amount (USD string). For prize-pool **server-settle**, this may
411
+ * differ from what moved on chain — see {@link Payment.chainAmount} (sdk#507).
412
+ */
390
413
  amount: string;
391
414
  fee: string;
392
415
  net: string;
393
- /** Present for prize-pool entries: the 60/30/10 breakdown in USD. */
416
+ /**
417
+ * On-chain entry amount (USD) when the service observed it (server-settle entry).
418
+ * PrizePool charges the **round open price**, not necessarily {@link Payment.amount}.
419
+ * Reconcile economics against this field (or a chain `getRound` read), not `amount` alone.
420
+ */
421
+ chainAmount?: string;
422
+ /** On-chain entry amount in micro-USDC (6 dp integer string), when known. */
423
+ chainAmountMicro?: string;
424
+ /**
425
+ * First-party **60/30/10 projection** (pool/seed/rake USD strings) for prize-pool
426
+ * entries — bookkeeping preview, **not** a chain-derived settlement observation
427
+ * (sdk#514). Numbers come from SDK constants (`POOL_BPS`/`SEED_BPS`/`RAKE_BPS`),
428
+ * not from reading the deployed pool’s constructor bps. On-chain, entry only
429
+ * takes the fee off the top; pool/seed split at **lock**; payable pot also
430
+ * includes inherited seedBank — so `split.pool` is **not** “what this round pays.”
431
+ * Reconcile money with {@link Payment.chainAmount} / pool `getRound`.
432
+ * Field name kept for compatibility (no rename in V1).
433
+ */
394
434
  split?: {
395
435
  pool: string;
396
436
  seed: string;
@@ -427,9 +467,17 @@ interface Payment {
427
467
  interface VerifyResult {
428
468
  id: string;
429
469
  status: PaymentStatus;
470
+ /** Requested amount (USD). May differ from {@link VerifyResult.chainAmount} on server-settle entry. */
430
471
  amount: string;
431
472
  fee: string;
432
473
  net: string;
474
+ /**
475
+ * On-chain settled entry amount (USD) when known — distinct from {@link VerifyResult.amount}
476
+ * (sdk#507 / Vault Pop DX-05b). Prefer this for pot/split economics on shared rounds.
477
+ */
478
+ chainAmount?: string;
479
+ /** On-chain settled entry amount in micro-USDC, when known. */
480
+ chainAmountMicro?: string;
433
481
  txHash?: `0x${string}`;
434
482
  playerId: string;
435
483
  sku?: string;
@@ -440,6 +488,8 @@ interface VerifyResult {
440
488
  * service can never confirm on-chain. */
441
489
  verifiedVia?: "chain" | "cache" | "degraded";
442
490
  chainReads?: "enabled" | "degraded";
491
+ /** Echo of on-chain identity when the service returns it (server-settle). */
492
+ identity?: string;
443
493
  }
444
494
  type WebhookEventType = "payment.confirmed" | "payment.failed" | "payout.settled" | "refund.processed";
445
495
  interface WebhookEvent {
@@ -493,6 +543,19 @@ interface RoundOpenInput {
493
543
  */
494
544
  roundKey?: string;
495
545
  }
546
+ /**
547
+ * Service GET /v1/rounds/:id read-path honesty (#497 / #501).
548
+ * Transport meta — not chain lifecycle. Prefer `rounds.getWithMeta` over stuffing onto RoundState.
549
+ */
550
+ type RoundGetVia = "chain" | "cache" | "stale" | "unknown";
551
+ /** Full GET round response including transport meta (#501 — Claude pick C / getWithMeta). */
552
+ interface RoundGetResult {
553
+ round: RoundState;
554
+ /** Present on live service responses after #499. Omitted under `mock: true`. */
555
+ via?: RoundGetVia;
556
+ /** Present only when eth_call itself threw (#497 C1). */
557
+ chainReadFailed?: boolean;
558
+ }
496
559
  interface RoundState {
497
560
  roundId: string;
498
561
  gameId: string;
@@ -715,4 +778,4 @@ declare class NothingToWithdrawError extends PlaymosError {
715
778
  constructor(detail?: Record<string, unknown>);
716
779
  }
717
780
 
718
- export { type RoundStatus as $, type ActiveSeriesRound as A, type Eip1193Provider as B, type CancelRoundResult as C, type GasMode as D, type EscrowHoldInput as E, InvalidAmountError as F, type GasConfig as G, type ListingStatus as H, InsufficientGasError as I, type MarketplaceItem as J, type MarketplaceSale as K, type Listing as L, type MarketplaceListInput as M, type Network as N, MissingFieldError as O, type PlaymosConfig as P, NothingToWithdrawError as Q, type RoundOpenInput as R, type SettleRoundResult as S, type TransferResult as T, PaymentFailedError as U, type VerifyResult as V, type WebhookEvent as W, type PaymentStatus as X, PlaymosError as Y, type PlaymosErrorCode as Z, type RetryOptions as _, type RoundState as a, type WalletConfig as a0, WalletConnectionError as a1, type WalletConnector as a2, WalletTimeoutError as a3, type WebhookEventType as a4, type RoundSettleInput as b, type RoundCancelInput as c, type PrizeBalance as d, type WithdrawResult as e, type AgentWallet as f, type AgentFundResult as g, type EscrowHoldResult as h, type EscrowResolveResult as i, type MarketplaceSaleResult as j, type MarketplaceGetResult as k, type PayInput as l, type Payment as m, type EnterRoundInput as n, type TransferReconcile as o, type WaitOptions as p, type TransferInput as q, type TransferConfirmOptions as r, type PayoutRule as s, type ActiveForSeriesResult as t, type AgentEconomyConfig as u, AlreadyEnteredError as v, ApiError as w, AuthError as x, ConfigError as y, type ContractConfig as z };
781
+ export { type RetryOptions as $, type ActiveSeriesRound as A, type ContractConfig as B, type CancelRoundResult as C, type Eip1193Provider as D, type EscrowHoldInput as E, type GasMode as F, type GasConfig as G, InvalidAmountError as H, InsufficientGasError as I, type ListingStatus as J, type MarketplaceItem as K, type Listing as L, type MarketplaceListInput as M, type Network as N, type MarketplaceSale as O, type PlaymosConfig as P, MissingFieldError as Q, type RoundOpenInput as R, type SettleRoundResult as S, type TransferResult as T, NothingToWithdrawError as U, type VerifyResult as V, type WebhookEvent as W, PaymentFailedError as X, type PaymentStatus as Y, PlaymosError as Z, type PlaymosErrorCode as _, type RoundState as a, type RoundGetVia as a0, type RoundStatus as a1, type WalletConfig as a2, WalletConnectionError as a3, type WalletConnector as a4, WalletTimeoutError as a5, type WebhookEventType as a6, type RoundSettleInput as b, type RoundCancelInput as c, type RoundGetResult as d, type PrizeBalance as e, type WithdrawResult as f, type AgentWallet as g, type AgentFundResult as h, type EscrowHoldResult as i, type EscrowResolveResult as j, type MarketplaceSaleResult as k, type MarketplaceGetResult as l, type PayInput as m, type Payment as n, type EnterRoundInput as o, type TransferReconcile as p, type WaitOptions as q, type TransferInput as r, type TransferConfirmOptions as s, type PayoutRule as t, type ActiveForSeriesResult as u, type AgentEconomyConfig as v, AlreadyEnteredError as w, ApiError as x, AuthError as y, ConfigError as z };
package/dist/index.cjs CHANGED
@@ -267,10 +267,10 @@ function validateMetadata(metadata) {
267
267
 
268
268
  // src/payout.ts
269
269
  var PayoutError = class extends Error {
270
- constructor(message) {
270
+ constructor(message, code = "payout_invalid") {
271
271
  super(message);
272
- this.code = "payout_invalid";
273
272
  this.name = "PayoutError";
273
+ this.code = code;
274
274
  }
275
275
  };
276
276
  var ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
@@ -315,7 +315,8 @@ function computePayout(pool, ranking, rule) {
315
315
  }
316
316
  if (splits.length > wallets.length) {
317
317
  throw new PayoutError(
318
- `top-n needs ${splits.length} ranked wallets but ranking only has ${wallets.length}`
318
+ `ranking_too_short: a top-n rule cannot be settled from a ranking shorter than splitsBps.length (need ${splits.length}, got ${wallets.length}) \u2014 open with winner-take-all, or settle with explicit winners[{wallet,amount}]`,
319
+ "ranking_too_short"
319
320
  );
320
321
  }
321
322
  let sumBps = 0;
@@ -628,7 +629,7 @@ function resolveRawProvider(wallet) {
628
629
  }
629
630
  if (injected) return injected;
630
631
  throw new WalletConnectionError(
631
- "base-account connector needs a provider. In the Base App it is injected automatically; elsewhere, create one with @base-org/account and pass it as wallet.provider."
632
+ 'base-account connector needs a provider \u2014 this is not a built-in Base Account/passkey flow in @playmos/sdk. In the Base App a provider is often injected; elsewhere install @base-org/account, construct a provider, and pass it as wallet.provider (or use connector: "injected" in a wallet browser).'
632
633
  );
633
634
  }
634
635
  function resolveProvider(wallet, opts) {
@@ -1614,19 +1615,34 @@ var Playmos = class {
1614
1615
  }
1615
1616
  return { ...result, txHash: result.txHash ?? firstTx ?? null };
1616
1617
  },
1618
+ /**
1619
+ * Round state only (stable). Prefer {@link getWithMeta} when you need service
1620
+ * read-path honesty (`via` / `chainReadFailed` — #497 / #501).
1621
+ */
1617
1622
  get: async (input) => {
1623
+ const meta = await this.rounds.getWithMeta(input);
1624
+ return meta.round;
1625
+ },
1626
+ /**
1627
+ * Round state + transport meta from GET /v1/rounds/:id (#501).
1628
+ * Does **not** attach `via` onto RoundState (Claude rejected option B — hub would
1629
+ * silently pick it up and leak into settle/cancel paths that call get()).
1630
+ * Under `mock: true`, meta fields are **omitted** (no eth_call; do not invent "cache").
1631
+ */
1632
+ getWithMeta: async (input) => {
1618
1633
  requireField(input?.roundId, "roundId");
1619
1634
  if (this.config.mock) {
1620
1635
  const existing = this.mockRounds.get(input.roundId);
1621
1636
  if (!existing) {
1622
1637
  throw new ApiError(`unknown round: ${input.roundId}`, { status: 404, code: "not_found" });
1623
1638
  }
1624
- return existing;
1639
+ return { round: existing };
1625
1640
  }
1626
- const { round } = await this.http.get(
1627
- `/rounds/${encodeURIComponent(input.roundId)}`
1628
- );
1629
- return round;
1641
+ const body = await this.http.get(`/rounds/${encodeURIComponent(input.roundId)}`);
1642
+ const out = { round: body.round };
1643
+ if (body.via != null) out.via = body.via;
1644
+ if (body.chainReadFailed === true) out.chainReadFailed = true;
1645
+ return out;
1630
1646
  },
1631
1647
  /**
1632
1648
  * Authoritative series latch (#351) — `null` means **proven free**.
@@ -1731,19 +1747,26 @@ var Playmos = class {
1731
1747
  */
1732
1748
  withdraw: async (input) => {
1733
1749
  if (this.config.mock) {
1734
- let amountMicro2 = 0n;
1735
1750
  let prizePool2 = input.prizePoolAddress ?? "0x0000000000000000000000000000000000000001";
1736
1751
  if (input.roundId) {
1737
1752
  const existing = this.mockRounds.get(input.roundId);
1738
1753
  if (existing?.prizePoolAddress) prizePool2 = existing.prizePoolAddress;
1739
- for (const [k, v] of this.mockWithdrawable) {
1740
- if (k.startsWith(`${input.roundId}:`) && v > 0n) {
1741
- amountMicro2 = v;
1742
- this.mockWithdrawable.set(k, 0n);
1743
- break;
1744
- }
1745
- }
1746
1754
  }
1755
+ if (!input.wallet || !ADDRESS_RE3.test(input.wallet)) {
1756
+ throw new ConfigError(
1757
+ "mock rounds.withdraw requires wallet (0x\u2026) \u2014 live uses the connected provider; without wallet mock would pay the first non-zero credit to the wrong player (#495)"
1758
+ );
1759
+ }
1760
+ const wallet = input.wallet.toLowerCase();
1761
+ if (!input.roundId) {
1762
+ throw new ConfigError("mock rounds.withdraw requires roundId to locate claimable credits");
1763
+ }
1764
+ const key = `${input.roundId}:${wallet}`;
1765
+ const amountMicro2 = this.mockWithdrawable.get(key) ?? 0n;
1766
+ if (amountMicro2 === 0n) {
1767
+ throw new NothingToWithdrawError({ prizePoolAddress: prizePool2, wallet });
1768
+ }
1769
+ this.mockWithdrawable.set(key, 0n);
1747
1770
  return {
1748
1771
  prizePoolAddress: prizePool2,
1749
1772
  amount: formatMicroToUsd(amountMicro2),
@@ -2015,8 +2038,16 @@ var Playmos = class {
2015
2038
  /**
2016
2039
  * Offline mock payments for this client instance — so `verify(id)` after
2017
2040
  * `mock: true` pay/enterRound does not hit the live API (#204).
2041
+ * Keyed by payment id for verify.
2018
2042
  */
2019
2043
  this.mockPayments = /* @__PURE__ */ new Map();
2044
+ /**
2045
+ * sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
2046
+ * Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
2047
+ * Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
2048
+ * Auto-minted keys still unique per call (not stored for cross-call replay).
2049
+ */
2050
+ this.mockByIdempotencyKey = /* @__PURE__ */ new Map();
2020
2051
  /**
2021
2052
  * `transfers` — read-back / confirmation for a prior `transfer()` (issues #27, #47).
2022
2053
  * `get` reconciles once against chain truth; `wait` polls it to a terminal state
@@ -2201,18 +2232,62 @@ var Playmos = class {
2201
2232
  }
2202
2233
  };
2203
2234
  }
2204
- rememberMock(payment) {
2205
- if (payment.mock) this.mockPayments.set(payment.id, payment);
2235
+ mockIdemScopeKey(gameId, heldKey) {
2236
+ return `${gameId ?? ""}:${heldKey}`;
2237
+ }
2238
+ rememberMock(payment, held) {
2239
+ if (payment.mock) {
2240
+ this.mockPayments.set(payment.id, payment);
2241
+ if (held) {
2242
+ this.mockByIdempotencyKey.set(this.mockIdemScopeKey(held.terms.gameId, held.key), {
2243
+ payment,
2244
+ terms: held.terms
2245
+ });
2246
+ }
2247
+ }
2206
2248
  return payment;
2207
2249
  }
2250
+ /** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
2251
+ mockIdemReplayOrThrow(heldKey, terms) {
2252
+ const prior = this.mockByIdempotencyKey.get(this.mockIdemScopeKey(terms.gameId, heldKey));
2253
+ if (!prior) return null;
2254
+ const t = prior.terms;
2255
+ const same = t.kind === terms.kind && t.amount === terms.amount && t.gameId === terms.gameId && t.playerId === terms.playerId && t.roundId === terms.roundId && t.identity === terms.identity && t.sku === terms.sku;
2256
+ if (!same) {
2257
+ throw new ApiError(
2258
+ `idempotencyKey was already used for a payment with DIFFERENT terms (kind/amount/gameId/playerId/roundId/identity/sku); use a fresh key (mock:true mirrors live conflict \u2014 sdk#513).`,
2259
+ {
2260
+ code: "idempotency_conflict",
2261
+ idempotencyKey: heldKey,
2262
+ priorKind: t.kind,
2263
+ priorAmount: t.amount
2264
+ }
2265
+ );
2266
+ }
2267
+ return prior.payment;
2268
+ }
2208
2269
  /** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
2209
2270
  async pay(input) {
2210
2271
  const amountMicro = validateAmount(input.amount);
2211
2272
  requireField(input.sku, "sku");
2212
2273
  requireField(input.playerId, "playerId");
2213
2274
  const metadata = validateMetadata(input.metadata);
2214
- const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
2275
+ const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
2276
+ const idempotencyKey = heldIdem ?? prefixedId("idem");
2215
2277
  if (this.config.mock) {
2278
+ const terms = {
2279
+ kind: "iap",
2280
+ amount: formatMicroToUsd(amountMicro),
2281
+ gameId: input.gameId ?? "",
2282
+ playerId: input.playerId,
2283
+ roundId: "",
2284
+ identity: "",
2285
+ sku: input.sku.trim()
2286
+ };
2287
+ if (heldIdem) {
2288
+ const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
2289
+ if (replay) return replay;
2290
+ }
2216
2291
  return this.rememberMock(
2217
2292
  mockIapPayment({
2218
2293
  amountMicro,
@@ -2221,7 +2296,8 @@ var Playmos = class {
2221
2296
  playerId: input.playerId,
2222
2297
  chain: this.env.network,
2223
2298
  metadata
2224
- })
2299
+ }),
2300
+ heldIdem ? { key: heldIdem, terms } : void 0
2225
2301
  );
2226
2302
  }
2227
2303
  if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
@@ -2243,7 +2319,13 @@ var Playmos = class {
2243
2319
  );
2244
2320
  let payment = this.mapServerPayment(res.payment, "iap", amountMicro);
2245
2321
  if (payment.status === "pending" || payment.status === "created") {
2246
- payment = await this.waitForServerSettle(payment.id, "iap", amountMicro);
2322
+ payment = await this.waitForServerSettle(
2323
+ payment.id,
2324
+ "iap",
2325
+ amountMicro,
2326
+ input.settleTimeoutMs ?? 6e4,
2327
+ input.signal
2328
+ );
2247
2329
  }
2248
2330
  return payment;
2249
2331
  }
@@ -2288,8 +2370,22 @@ var Playmos = class {
2288
2370
  requireField(input.roundId, "roundId");
2289
2371
  requireField(input.playerId, "playerId");
2290
2372
  const metadata = validateMetadata(input.metadata);
2291
- const idempotencyKey = input.idempotencyKey ?? prefixedId("idem");
2373
+ const heldIdem = typeof input.idempotencyKey === "string" && input.idempotencyKey.trim() ? input.idempotencyKey.trim() : void 0;
2374
+ const idempotencyKey = heldIdem ?? prefixedId("idem");
2292
2375
  if (this.config.mock) {
2376
+ const terms = {
2377
+ kind: "entry",
2378
+ amount: formatMicroToUsd(amountMicro),
2379
+ gameId: input.gameId,
2380
+ playerId: input.playerId,
2381
+ roundId: input.roundId,
2382
+ identity: typeof input.identity === "string" ? input.identity.trim() : "",
2383
+ sku: ""
2384
+ };
2385
+ if (heldIdem) {
2386
+ const replay = this.mockIdemReplayOrThrow(heldIdem, terms);
2387
+ if (replay) return replay;
2388
+ }
2293
2389
  this.mockAccumulateEntry(input.roundId, amountMicro);
2294
2390
  return this.rememberMock(
2295
2391
  mockEntryPayment({
@@ -2305,7 +2401,8 @@ var Playmos = class {
2305
2401
  // B2 pins — must survive mock path for hub hasEntered parity (#204).
2306
2402
  roundKey: input.roundKey,
2307
2403
  identity: input.identity
2308
- })
2404
+ }),
2405
+ heldIdem ? { key: heldIdem, terms } : void 0
2309
2406
  );
2310
2407
  }
2311
2408
  const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
@@ -2338,7 +2435,13 @@ var Playmos = class {
2338
2435
  );
2339
2436
  let payment2 = this.mapServerPayment(res.payment, "entry", amountMicro);
2340
2437
  if (payment2.status === "pending" || payment2.status === "created") {
2341
- payment2 = await this.waitForServerSettle(payment2.id, "entry", amountMicro);
2438
+ payment2 = await this.waitForServerSettle(
2439
+ payment2.id,
2440
+ "entry",
2441
+ amountMicro,
2442
+ input.settleTimeoutMs ?? 6e4,
2443
+ input.signal
2444
+ );
2342
2445
  }
2343
2446
  if (input.identity) payment2.identity = input.identity;
2344
2447
  const pool = res.clientParams?.contractAddress ?? this.config.contracts?.prizePool;
@@ -2521,10 +2624,16 @@ var Playmos = class {
2521
2624
  * until chain verify catches PaymentSettled (RPC lag) — poll so pay() matches
2522
2625
  * the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
2523
2626
  */
2524
- async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4) {
2627
+ async waitForServerSettle(paymentId, kind, amountMicro, timeoutMs = 6e4, signal) {
2525
2628
  const start = Date.now();
2526
2629
  let delayMs = 400;
2527
2630
  while (Date.now() - start < timeoutMs) {
2631
+ if (signal?.aborted) {
2632
+ throw new ApiError(
2633
+ `Aborted waiting for server-settle of ${paymentId}. Re-check with playmos.verify("${paymentId}").`,
2634
+ { paymentId, aborted: true, asyncSettle: true }
2635
+ );
2636
+ }
2528
2637
  const raw = await this.http.get(
2529
2638
  `/payments/${encodeURIComponent(paymentId)}`
2530
2639
  );
@@ -2539,12 +2648,46 @@ var Playmos = class {
2539
2648
  { paymentId, timeout: true, asyncSettle: true }
2540
2649
  );
2541
2650
  }
2651
+ /**
2652
+ * sdk#509 — recover a payment after a lost response by the idempotency key you held.
2653
+ * Requires secret sk_test_ (same as list). Returns null if not found.
2654
+ */
2655
+ async findPaymentByIdempotencyKey(idempotencyKey, opts) {
2656
+ requireField(idempotencyKey, "idempotencyKey");
2657
+ if (this.config.mock) {
2658
+ return null;
2659
+ }
2660
+ const q = new URLSearchParams({ idempotencyKey });
2661
+ if (opts?.gameId) q.set("gameId", opts.gameId);
2662
+ try {
2663
+ const res = await this.http.get(
2664
+ `/payments?${q.toString()}`
2665
+ );
2666
+ const row = res.data?.[0];
2667
+ if (!row || typeof row.id !== "string") return null;
2668
+ const kind = row.kind === "entry" ? "entry" : "iap";
2669
+ const amountMicro = validateAmount(String(row.amount ?? "0.01"));
2670
+ return this.mapServerPayment(
2671
+ row,
2672
+ kind,
2673
+ amountMicro
2674
+ );
2675
+ } catch (e) {
2676
+ if (e instanceof ApiError && (e.detail?.status === 404 || e.detail?.code === "not_found" || /not found/i.test(e.message))) {
2677
+ return null;
2678
+ }
2679
+ throw e;
2680
+ }
2681
+ }
2542
2682
  /**
2543
2683
  * Map a server-settled payment (from the `settle: "server"` response) into the
2544
2684
  * SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
2545
- * pass its authoritative `status`/`txHash`/amounts straight through and, for an
2546
- * entry, derive the 60/30/10 USD `split` locally (exactly as the mock + the
2547
- * contract compute it) from the parsed amount.
2685
+ * pass its authoritative `status`/`txHash`/amounts straight through.
2686
+ *
2687
+ * For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
2688
+ * request amount using SDK BPS constants (same integer math as mock) — not a
2689
+ * chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
2690
+ * Prefer `chainAmount` / on-chain `getRound` for money truth.
2548
2691
  */
2549
2692
  mapServerPayment(p, kind, amountMicro) {
2550
2693
  const payment = {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, C as CancelRoundResult, A as ActiveSeriesRound, d as PrizeBalance, e as WithdrawResult, f as AgentWallet, g as AgentFundResult, T as TransferResult, E as EscrowHoldInput, h as EscrowHoldResult, i as EscrowResolveResult, M as MarketplaceListInput, L as Listing, j as MarketplaceSaleResult, k as MarketplaceGetResult, l as PayInput, m as Payment, n as EnterRoundInput, o as TransferReconcile, p as WaitOptions, q as TransferInput, r as TransferConfirmOptions, V as VerifyResult, s as PayoutRule } from './errors-BMlWHsMb.cjs';
2
- export { t as ActiveForSeriesResult, u as AgentEconomyConfig, v as AlreadyEnteredError, w as ApiError, x as AuthError, y as ConfigError, z as ContractConfig, B as Eip1193Provider, G as GasConfig, D as GasMode, I as InsufficientGasError, F as InvalidAmountError, H as ListingStatus, J as MarketplaceItem, K as MarketplaceSale, O as MissingFieldError, Q as NothingToWithdrawError, U as PaymentFailedError, X as PaymentStatus, Y as PlaymosError, Z as PlaymosErrorCode, _ as RetryOptions, $ as RoundStatus, a0 as WalletConfig, a1 as WalletConnectionError, a2 as WalletConnector, a3 as WalletTimeoutError, a4 as WebhookEventType } from './errors-BMlWHsMb.cjs';
1
+ import { N as Network, P as PlaymosConfig, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, C as CancelRoundResult, d as RoundGetResult, A as ActiveSeriesRound, e as PrizeBalance, f as WithdrawResult, g as AgentWallet, h as AgentFundResult, T as TransferResult, E as EscrowHoldInput, i as EscrowHoldResult, j as EscrowResolveResult, M as MarketplaceListInput, L as Listing, k as MarketplaceSaleResult, l as MarketplaceGetResult, m as PayInput, n as Payment, o as EnterRoundInput, p as TransferReconcile, q as WaitOptions, r as TransferInput, s as TransferConfirmOptions, V as VerifyResult, t as PayoutRule } from './errors-Dpeesyop.cjs';
2
+ export { u as ActiveForSeriesResult, v as AgentEconomyConfig, w as AlreadyEnteredError, x as ApiError, y as AuthError, z as ConfigError, B as ContractConfig, D as Eip1193Provider, G as GasConfig, F as GasMode, I as InsufficientGasError, H as InvalidAmountError, J as ListingStatus, K as MarketplaceItem, O as MarketplaceSale, Q as MissingFieldError, U as NothingToWithdrawError, X as PaymentFailedError, Y as PaymentStatus, Z as PlaymosError, _ as PlaymosErrorCode, $ as RetryOptions, a0 as RoundGetVia, a1 as RoundStatus, a2 as WalletConfig, a3 as WalletConnectionError, a4 as WalletConnector, a5 as WalletTimeoutError, a6 as WebhookEventType } from './errors-Dpeesyop.cjs';
3
3
 
4
4
  /**
5
5
  * Environment resolution + the canonical address book.
@@ -518,9 +518,22 @@ declare class Playmos {
518
518
  * if still pending — never invents terminal success).
519
519
  */
520
520
  cancel: (input: RoundCancelInput) => Promise<CancelRoundResult>;
521
+ /**
522
+ * Round state only (stable). Prefer {@link getWithMeta} when you need service
523
+ * read-path honesty (`via` / `chainReadFailed` — #497 / #501).
524
+ */
521
525
  get: (input: {
522
526
  roundId: string;
523
527
  }) => Promise<RoundState>;
528
+ /**
529
+ * Round state + transport meta from GET /v1/rounds/:id (#501).
530
+ * Does **not** attach `via` onto RoundState (Claude rejected option B — hub would
531
+ * silently pick it up and leak into settle/cancel paths that call get()).
532
+ * Under `mock: true`, meta fields are **omitted** (no eth_call; do not invent "cache").
533
+ */
534
+ getWithMeta: (input: {
535
+ roundId: string;
536
+ }) => Promise<RoundGetResult>;
524
537
  /**
525
538
  * Authoritative series latch (#351) — `null` means **proven free**.
526
539
  * `GET /v1/series/:seriesKey/active?gameId=…` — not a candidate probe.
@@ -550,6 +563,11 @@ declare class Playmos {
550
563
  withdraw: (input: {
551
564
  prizePoolAddress?: `0x${string}`;
552
565
  roundId?: string;
566
+ /**
567
+ * Winner wallet (0x…). **Required under mock** (#495 Claude) — live path uses the
568
+ * connected provider. Without this, mock paid the first non-zero credit to anyone.
569
+ */
570
+ wallet?: string;
553
571
  /** Pre-check claimable; default true. Set false only if you already called `prize()`. */
554
572
  checkBalance?: boolean;
555
573
  }) => Promise<WithdrawResult>;
@@ -637,8 +655,16 @@ declare class Playmos {
637
655
  /**
638
656
  * Offline mock payments for this client instance — so `verify(id)` after
639
657
  * `mock: true` pay/enterRound does not hit the live API (#204).
658
+ * Keyed by payment id for verify.
640
659
  */
641
660
  private readonly mockPayments;
661
+ /**
662
+ * sdk#513 / residual B1–B2 — held idempotencyKey offline (live contract rehearsal).
663
+ * Keyed like service: `${gameId}:${key}` (IAP without gameId uses empty gameId).
664
+ * Stores terms so same key + different money throws (live IdempotencyConflict), not silent replay.
665
+ * Auto-minted keys still unique per call (not stored for cross-call replay).
666
+ */
667
+ private readonly mockByIdempotencyKey;
642
668
  constructor(config: PlaymosConfig);
643
669
  /**
644
670
  * Wallet timeout policy (#462 / PR #463 residual).
@@ -677,7 +703,10 @@ declare class Playmos {
677
703
  identity?: string;
678
704
  }>;
679
705
  };
706
+ private mockIdemScopeKey;
680
707
  private rememberMock;
708
+ /** Live-shaped conflict when held mock key is reused with different money terms (sdk#513 residual B1). */
709
+ private mockIdemReplayOrThrow;
681
710
  /** External-studio IAP (1%). Returns a real confirmed Payment with a txHash. */
682
711
  pay(input: PayInput): Promise<Payment>;
683
712
  /** Playmos-owned skill-game entry (10%, 60/30/10 prize pool). Closes #343. */
@@ -741,12 +770,22 @@ declare class Playmos {
741
770
  * the documented "confirmed" sandbox promise (TTFSC-T0 / cold-run).
742
771
  */
743
772
  private waitForServerSettle;
773
+ /**
774
+ * sdk#509 — recover a payment after a lost response by the idempotency key you held.
775
+ * Requires secret sk_test_ (same as list). Returns null if not found.
776
+ */
777
+ findPaymentByIdempotencyKey(idempotencyKey: string, opts?: {
778
+ gameId?: string;
779
+ }): Promise<Payment | null>;
744
780
  /**
745
781
  * Map a server-settled payment (from the `settle: "server"` response) into the
746
782
  * SDK's `Payment` shape. The service already signed + confirmed on-chain, so we
747
- * pass its authoritative `status`/`txHash`/amounts straight through and, for an
748
- * entry, derive the 60/30/10 USD `split` locally (exactly as the mock + the
749
- * contract compute it) from the parsed amount.
783
+ * pass its authoritative `status`/`txHash`/amounts straight through.
784
+ *
785
+ * For entry, `Payment.split` is a **first-party 60/30/10 projection** from the
786
+ * request amount using SDK BPS constants (same integer math as mock) — not a
787
+ * chain read of pool constructor bps or post-lock payable pot (sdk#514 residual N1).
788
+ * Prefer `chainAmount` / on-chain `getRound` for money truth.
750
789
  */
751
790
  private mapServerPayment;
752
791
  /** Prefer the service's authoritative micro-USDC amount; fall back to the
@@ -820,8 +859,8 @@ declare function previewPoolSplit(amount: string): {
820
859
  */
821
860
 
822
861
  declare class PayoutError extends Error {
823
- code: "payout_invalid";
824
- constructor(message: string);
862
+ code: string;
863
+ constructor(message: string, code?: string);
825
864
  }
826
865
  /**
827
866
  * Apply the studio's payout rule to a payable pool and ranking (best-first).
@@ -890,4 +929,4 @@ declare function ulid(seedTime?: number): string;
890
929
  /** `${prefix}_${ulid()}` — e.g. `pay_01J…`, `entry_01J…`, `idem_01J…`. */
891
930
  declare function prefixedId(prefix: string): string;
892
931
 
893
- export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundCancelInput, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };
932
+ export { ActiveSeriesRound, AgentFundResult, AgentWallet, type Authorization, CHAIN_ID, CancelRoundResult, type CreatePaymentRequirementInput, DEFAULT_API_BASE_URL, EnterRoundInput, EscrowHoldInput, EscrowHoldResult, EscrowResolveResult, Listing, MICRO_PER_USDC, MarketplaceGetResult, MarketplaceListInput, MarketplaceSaleResult, Network, PayInput, Payment, type PaymentRequirement, PayoutError, PayoutRule, PayoutRule as PayoutRuleCompute, Playmos, PlaymosConfig, PrizeBalance, RoundCancelInput, RoundGetResult, RoundOpenInput, RoundSettleInput, RoundState, SettleRoundResult, type SettlementAsset, TransferConfirmOptions, TransferInput, TransferReconcile, TransferResult, USDC_ADDRESS, USDC_DECIMALS, VerifyResult, WaitOptions, type WalletSignatureAuthorization, WebhookEvent, WithdrawResult, type X402ChallengeInput, type X402ChallengeResult, type X402FulfillAuthorization, type X402PayInput, type X402PayloadAuthorization, type X402PayloadMode, type X402PaymentRequired, type X402SettleResult, clientRuntimeSignals, computeIapSplit, computePayout, computePoolSplit, createPaymentRequirement, createX402Challenge, decodePaymentHeader, encodePaymentHeader, formatMicroToUsd, isClientRuntime, isWalletSignatureAuthorization, isX402PayloadAuthorization, networkToCaip2, parsePaymentRequirement, parseUsdToMicro, prefixedId, previewEscrowFee, previewIapSplit, previewMarketplaceSplit, previewPoolSplit, previewTransferSplit, resolveEnv, serializePaymentRequirement, toX402PaymentRequired, ulid, validateX402ChallengeInput };