@playmos/sdk 0.3.13 → 0.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@ All notable changes to `@playmos/sdk`. This project adheres to [Semantic Version
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## [0.3.14] — 2026-09-11
8
+
9
+ ### Added
10
+ - **`settle: "server"`** on `new Playmos({ ... })` — explicit sandbox no-wallet path. `pay()` / `enterRound()` server-settle even when MetaMask is installed. Live keys throw. Default without the ask is unchanged (wallet present → player signs).
11
+
7
12
  ## [0.3.13] — 2026-09-11
8
13
 
9
14
  ### Docs
package/README.md CHANGED
@@ -23,7 +23,7 @@ The sandbox API also sends CORS headers so browser `pay()` / `enterRound()` work
23
23
 
24
24
  ## Quickstart — the no-wallet sandbox
25
25
 
26
- The public sandbox key `pk_test_playmos_sandbox` runs against a real Playmos test service on Base Sepolia. **You don't need a wallet:** on a `pk_test` key with no `wallet` configured, the SDK routes to a server-settle path where Playmos signs and submits the on-chain transaction for you. You still get a real, confirmed `txHash` — just signed by the service. No wallet, no gas, no crypto.
26
+ The public sandbox key `pk_test_playmos_sandbox` runs against a real Playmos test service on Base Sepolia. **You don't need a wallet:** set `settle: "server"` (the quickstart does). The SDK then server-settles even if MetaMask is installed. You still get a real, confirmed `txHash` — signed by the service. No wallet, no gas, no crypto. Omit `settle` only when you want the player to sign.
27
27
 
28
28
  The public key is wired to `game_sandbox_iap`. Skill entry (`game_sandbox_skill`) is an optional later path — not the first-hour default.
29
29
 
@@ -35,7 +35,7 @@ import { Playmos } from "@playmos/sdk";
35
35
 
36
36
  // Public sandbox key — client-safe, like Stripe's pk_test_.
37
37
  // No wallet needed: Playmos server-settles the test payment for you.
38
- const playmos = new Playmos({ apiKey: "pk_test_playmos_sandbox" });
38
+ const playmos = new Playmos({ apiKey: "pk_test_playmos_sandbox", settle: "server" });
39
39
 
40
40
  const payment = await playmos.pay({
41
41
  gameId: "game_sandbox_iap", // required for the public sandbox key
@@ -90,7 +90,7 @@ const entry = await playmos.enterRound({
90
90
 
91
91
  ### Your own contest pool — `epochs.preparePool` (you sign)
92
92
 
93
- `@playmos/sdk` **0.3.13** ships `epochs.preparePool` with `bytecode` / `unsignedTx` on the typed response. It calls the live API. Playmos sets `broadcast: false` and does **not** send the transaction.
93
+ `@playmos/sdk` **0.3.14** ships `epochs.preparePool` with `bytecode` / `unsignedTx` on the typed response. It calls the live API. Playmos sets `broadcast: false` and does **not** send the transaction.
94
94
 
95
95
  1. Fund your studio wallet on Base Sepolia (ETH + test USDC). Playmos has no faucet — use [Coinbase's faucet docs](https://docs.cdp.coinbase.com/faucets/introduction/welcome).
96
96
  2. Call `epochs.preparePool({ studioWallet })` or `POST /v1/epochs/pools/prepare`. Use `feeSink` from that response (`0xD84c190085aa59c48a9B478Ea333D50B8DF4aD42`).
@@ -76,6 +76,13 @@ interface PlaymosConfig {
76
76
  * even mock results use the REAL status union (never a synthetic "mocked").
77
77
  */
78
78
  mock?: boolean;
79
+ /**
80
+ * Explicit sandbox no-wallet path. On a test key, `pay()` / `enterRound()`
81
+ * server-settle even if MetaMask (or another injected wallet) is installed.
82
+ * Live keys (`pk_live_` / `sk_live_`) throw — never server-settle real money.
83
+ * Omit to keep today's default: server-settle only when no wallet is present.
84
+ */
85
+ settle?: "server";
79
86
  /**
80
87
  * Automatic backoff/retry on HTTP 429 (throttling). Default ON (up to 2 retries,
81
88
  * honoring a `Retry-After` header). Retries are safe — reads are idempotent and
@@ -76,6 +76,13 @@ interface PlaymosConfig {
76
76
  * even mock results use the REAL status union (never a synthetic "mocked").
77
77
  */
78
78
  mock?: boolean;
79
+ /**
80
+ * Explicit sandbox no-wallet path. On a test key, `pay()` / `enterRound()`
81
+ * server-settle even if MetaMask (or another injected wallet) is installed.
82
+ * Live keys (`pk_live_` / `sk_live_`) throw — never server-settle real money.
83
+ * Omit to keep today's default: server-settle only when no wallet is present.
84
+ */
85
+ settle?: "server";
79
86
  /**
80
87
  * Automatic backoff/retry on HTTP 429 (throttling). Default ON (up to 2 retries,
81
88
  * honoring a `Retry-After` header). Retries are safe — reads are idempotent and
package/dist/index.cjs CHANGED
@@ -2262,7 +2262,7 @@ var Playmos = class {
2262
2262
  },
2263
2263
  walletAddress: async () => getAccount(this.walletProvider()),
2264
2264
  readView: async (to, data) => {
2265
- if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
2265
+ if (!await this.shouldServerSettle() && await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
2266
2266
  const provider = this.walletProvider();
2267
2267
  return await provider.request({
2268
2268
  method: "eth_call",
@@ -3363,8 +3363,14 @@ var Playmos = class {
3363
3363
  );
3364
3364
  }
3365
3365
  }
3366
+ if (config.settle === "server" && !this.env.isTest) {
3367
+ throw new ConfigError(
3368
+ 'settle: "server" is sandbox-only. Live keys must use a player wallet \u2014 never server-settle real money.',
3369
+ { field: "settle" }
3370
+ );
3371
+ }
3366
3372
  this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
3367
- if (!config.mock) {
3373
+ if (!config.mock && config.settle !== "server") {
3368
3374
  try {
3369
3375
  resolveProvider(config.wallet, this.walletTimeoutPolicy());
3370
3376
  } catch {
@@ -3434,6 +3440,23 @@ var Playmos = class {
3434
3440
  return 0n;
3435
3441
  }
3436
3442
  }
3443
+ /**
3444
+ * Sandbox server-settle when the developer asked (`settle: "server"`) or when
3445
+ * a test key has no wallet. Never calls walletAvailable if the ask is set —
3446
+ * that is the MetaMask-hijack delete (Moonhop leftover Slice 1).
3447
+ */
3448
+ async shouldServerSettle() {
3449
+ if (this.config.settle === "server") {
3450
+ if (!this.env.isTest) {
3451
+ throw new ConfigError(
3452
+ 'settle: "server" is sandbox-only. Live keys must use a player wallet \u2014 never server-settle real money.',
3453
+ { field: "settle" }
3454
+ );
3455
+ }
3456
+ return true;
3457
+ }
3458
+ return this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
3459
+ }
3437
3460
  /**
3438
3461
  * Wallet timeout policy (#462 / PR #463 residual).
3439
3462
  * - connectTimeoutMs → eth_requestAccounts only
@@ -3561,7 +3584,7 @@ var Playmos = class {
3561
3584
  heldIdem ? { key: heldIdem, terms } : void 0
3562
3585
  );
3563
3586
  }
3564
- if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
3587
+ if (await this.shouldServerSettle()) {
3565
3588
  const res = await this.http.post(
3566
3589
  "/payments",
3567
3590
  {
@@ -3666,7 +3689,7 @@ var Playmos = class {
3666
3689
  heldIdem ? { key: heldIdem, terms } : void 0
3667
3690
  );
3668
3691
  }
3669
- const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
3692
+ const serverSettleNoWallet = await this.shouldServerSettle();
3670
3693
  if (!serverSettleNoWallet) {
3671
3694
  const pinned = typeof input.identity === "string" ? input.identity.trim() : "";
3672
3695
  if (!pinned) {
@@ -3996,7 +4019,7 @@ var Playmos = class {
3996
4019
  });
3997
4020
  let raw;
3998
4021
  try {
3999
- if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
4022
+ if (!await this.shouldServerSettle() && await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
4000
4023
  const provider = this.walletProvider();
4001
4024
  raw = await provider.request({
4002
4025
  method: "eth_call",
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Call, P as PlaymosConfig, O as OnchainStatus, N as Network, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, d as CancelRoundResult, e as RoundGetResult, A as ActiveSeriesRound, f as PayoutNoticesResult, g as PrizeBalance, h as WithdrawResult, i as AgentWallet, j as AgentFundResult, T as TransferResult, E as EscrowHoldInput, k as EscrowHoldResult, l as EscrowResolveResult, M as MarketplaceListInput, L as Listing, m as MarketplaceSaleResult, n as MarketplaceGetResult, o as PayInput, p as Payment, q as EnterRoundInput, r as TransferReconcile, s as WaitOptions, t as TransferInput, u as TransferConfirmOptions, V as VerifyResult, v as PayoutRule } from './errors-CYKgt2VU.cjs';
2
- export { w as ActiveForSeriesResult, x as AgentEconomyConfig, y as AlreadyEnteredError, z as ApiError, B as AuthError, D as ConfigError, F as ContractConfig, G as Eip1193Provider, H as GasConfig, I as GasMode, J as InsufficientGasError, K as InvalidAmountError, Q as ListingStatus, U as MarketplaceItem, X as MarketplaceSale, Y as MissingFieldError, Z as NothingToWithdrawError, _ as PaymentFailedError, $ as PaymentStatus, a0 as PayoutNotice, a1 as PayoutNoticeStatus, a2 as PlaymosError, a3 as PlaymosErrorCode, a4 as RetryOptions, a5 as RoundGetVia, a6 as RoundStatus, a7 as WalletConfig, a8 as WalletConnectionError, a9 as WalletConnector, aa as WalletTimeoutError, ab as WebhookEventType, ac as buildEpochExecuteSettlementCall } from './errors-CYKgt2VU.cjs';
1
+ import { C as Call, P as PlaymosConfig, O as OnchainStatus, N as Network, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, d as CancelRoundResult, e as RoundGetResult, A as ActiveSeriesRound, f as PayoutNoticesResult, g as PrizeBalance, h as WithdrawResult, i as AgentWallet, j as AgentFundResult, T as TransferResult, E as EscrowHoldInput, k as EscrowHoldResult, l as EscrowResolveResult, M as MarketplaceListInput, L as Listing, m as MarketplaceSaleResult, n as MarketplaceGetResult, o as PayInput, p as Payment, q as EnterRoundInput, r as TransferReconcile, s as WaitOptions, t as TransferInput, u as TransferConfirmOptions, V as VerifyResult, v as PayoutRule } from './errors-WcDNfb4q.cjs';
2
+ export { w as ActiveForSeriesResult, x as AgentEconomyConfig, y as AlreadyEnteredError, z as ApiError, B as AuthError, D as ConfigError, F as ContractConfig, G as Eip1193Provider, H as GasConfig, I as GasMode, J as InsufficientGasError, K as InvalidAmountError, Q as ListingStatus, U as MarketplaceItem, X as MarketplaceSale, Y as MissingFieldError, Z as NothingToWithdrawError, _ as PaymentFailedError, $ as PaymentStatus, a0 as PayoutNotice, a1 as PayoutNoticeStatus, a2 as PlaymosError, a3 as PlaymosErrorCode, a4 as RetryOptions, a5 as RoundGetVia, a6 as RoundStatus, a7 as WalletConfig, a8 as WalletConnectionError, a9 as WalletConnector, aa as WalletTimeoutError, ab as WebhookEventType, ac as buildEpochExecuteSettlementCall } from './errors-WcDNfb4q.cjs';
3
3
 
4
4
  /**
5
5
  * Thin REST client for the Playmos service. Every SDK method that touches the
@@ -1323,6 +1323,12 @@ declare class Playmos {
1323
1323
  */
1324
1324
  private readonly mockByIdempotencyKey;
1325
1325
  constructor(config: PlaymosConfig);
1326
+ /**
1327
+ * Sandbox server-settle when the developer asked (`settle: "server"`) or when
1328
+ * a test key has no wallet. Never calls walletAvailable if the ask is set —
1329
+ * that is the MetaMask-hijack delete (Moonhop leftover Slice 1).
1330
+ */
1331
+ private shouldServerSettle;
1326
1332
  /**
1327
1333
  * Wallet timeout policy (#462 / PR #463 residual).
1328
1334
  * - connectTimeoutMs → eth_requestAccounts only
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as Call, P as PlaymosConfig, O as OnchainStatus, N as Network, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, d as CancelRoundResult, e as RoundGetResult, A as ActiveSeriesRound, f as PayoutNoticesResult, g as PrizeBalance, h as WithdrawResult, i as AgentWallet, j as AgentFundResult, T as TransferResult, E as EscrowHoldInput, k as EscrowHoldResult, l as EscrowResolveResult, M as MarketplaceListInput, L as Listing, m as MarketplaceSaleResult, n as MarketplaceGetResult, o as PayInput, p as Payment, q as EnterRoundInput, r as TransferReconcile, s as WaitOptions, t as TransferInput, u as TransferConfirmOptions, V as VerifyResult, v as PayoutRule } from './errors-CYKgt2VU.js';
2
- export { w as ActiveForSeriesResult, x as AgentEconomyConfig, y as AlreadyEnteredError, z as ApiError, B as AuthError, D as ConfigError, F as ContractConfig, G as Eip1193Provider, H as GasConfig, I as GasMode, J as InsufficientGasError, K as InvalidAmountError, Q as ListingStatus, U as MarketplaceItem, X as MarketplaceSale, Y as MissingFieldError, Z as NothingToWithdrawError, _ as PaymentFailedError, $ as PaymentStatus, a0 as PayoutNotice, a1 as PayoutNoticeStatus, a2 as PlaymosError, a3 as PlaymosErrorCode, a4 as RetryOptions, a5 as RoundGetVia, a6 as RoundStatus, a7 as WalletConfig, a8 as WalletConnectionError, a9 as WalletConnector, aa as WalletTimeoutError, ab as WebhookEventType, ac as buildEpochExecuteSettlementCall } from './errors-CYKgt2VU.js';
1
+ import { C as Call, P as PlaymosConfig, O as OnchainStatus, N as Network, W as WebhookEvent, R as RoundOpenInput, a as RoundState, b as RoundSettleInput, S as SettleRoundResult, c as RoundCancelInput, d as CancelRoundResult, e as RoundGetResult, A as ActiveSeriesRound, f as PayoutNoticesResult, g as PrizeBalance, h as WithdrawResult, i as AgentWallet, j as AgentFundResult, T as TransferResult, E as EscrowHoldInput, k as EscrowHoldResult, l as EscrowResolveResult, M as MarketplaceListInput, L as Listing, m as MarketplaceSaleResult, n as MarketplaceGetResult, o as PayInput, p as Payment, q as EnterRoundInput, r as TransferReconcile, s as WaitOptions, t as TransferInput, u as TransferConfirmOptions, V as VerifyResult, v as PayoutRule } from './errors-WcDNfb4q.js';
2
+ export { w as ActiveForSeriesResult, x as AgentEconomyConfig, y as AlreadyEnteredError, z as ApiError, B as AuthError, D as ConfigError, F as ContractConfig, G as Eip1193Provider, H as GasConfig, I as GasMode, J as InsufficientGasError, K as InvalidAmountError, Q as ListingStatus, U as MarketplaceItem, X as MarketplaceSale, Y as MissingFieldError, Z as NothingToWithdrawError, _ as PaymentFailedError, $ as PaymentStatus, a0 as PayoutNotice, a1 as PayoutNoticeStatus, a2 as PlaymosError, a3 as PlaymosErrorCode, a4 as RetryOptions, a5 as RoundGetVia, a6 as RoundStatus, a7 as WalletConfig, a8 as WalletConnectionError, a9 as WalletConnector, aa as WalletTimeoutError, ab as WebhookEventType, ac as buildEpochExecuteSettlementCall } from './errors-WcDNfb4q.js';
3
3
 
4
4
  /**
5
5
  * Thin REST client for the Playmos service. Every SDK method that touches the
@@ -1323,6 +1323,12 @@ declare class Playmos {
1323
1323
  */
1324
1324
  private readonly mockByIdempotencyKey;
1325
1325
  constructor(config: PlaymosConfig);
1326
+ /**
1327
+ * Sandbox server-settle when the developer asked (`settle: "server"`) or when
1328
+ * a test key has no wallet. Never calls walletAvailable if the ask is set —
1329
+ * that is the MetaMask-hijack delete (Moonhop leftover Slice 1).
1330
+ */
1331
+ private shouldServerSettle;
1326
1332
  /**
1327
1333
  * Wallet timeout policy (#462 / PR #463 residual).
1328
1334
  * - connectTimeoutMs → eth_requestAccounts only
package/dist/index.js CHANGED
@@ -1818,7 +1818,7 @@ var Playmos = class {
1818
1818
  },
1819
1819
  walletAddress: async () => getAccount(this.walletProvider()),
1820
1820
  readView: async (to, data) => {
1821
- if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
1821
+ if (!await this.shouldServerSettle() && await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
1822
1822
  const provider = this.walletProvider();
1823
1823
  return await provider.request({
1824
1824
  method: "eth_call",
@@ -2919,8 +2919,14 @@ var Playmos = class {
2919
2919
  );
2920
2920
  }
2921
2921
  }
2922
+ if (config.settle === "server" && !this.env.isTest) {
2923
+ throw new ConfigError(
2924
+ 'settle: "server" is sandbox-only. Live keys must use a player wallet \u2014 never server-settle real money.',
2925
+ { field: "settle" }
2926
+ );
2927
+ }
2922
2928
  this.http = createHttpClient(this.env.apiBaseUrl, config.apiKey, config.retry);
2923
- if (!config.mock) {
2929
+ if (!config.mock && config.settle !== "server") {
2924
2930
  try {
2925
2931
  resolveProvider(config.wallet, this.walletTimeoutPolicy());
2926
2932
  } catch {
@@ -2990,6 +2996,23 @@ var Playmos = class {
2990
2996
  return 0n;
2991
2997
  }
2992
2998
  }
2999
+ /**
3000
+ * Sandbox server-settle when the developer asked (`settle: "server"`) or when
3001
+ * a test key has no wallet. Never calls walletAvailable if the ask is set —
3002
+ * that is the MetaMask-hijack delete (Moonhop leftover Slice 1).
3003
+ */
3004
+ async shouldServerSettle() {
3005
+ if (this.config.settle === "server") {
3006
+ if (!this.env.isTest) {
3007
+ throw new ConfigError(
3008
+ 'settle: "server" is sandbox-only. Live keys must use a player wallet \u2014 never server-settle real money.',
3009
+ { field: "settle" }
3010
+ );
3011
+ }
3012
+ return true;
3013
+ }
3014
+ return this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
3015
+ }
2993
3016
  /**
2994
3017
  * Wallet timeout policy (#462 / PR #463 residual).
2995
3018
  * - connectTimeoutMs → eth_requestAccounts only
@@ -3117,7 +3140,7 @@ var Playmos = class {
3117
3140
  heldIdem ? { key: heldIdem, terms } : void 0
3118
3141
  );
3119
3142
  }
3120
- if (this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
3143
+ if (await this.shouldServerSettle()) {
3121
3144
  const res = await this.http.post(
3122
3145
  "/payments",
3123
3146
  {
@@ -3222,7 +3245,7 @@ var Playmos = class {
3222
3245
  heldIdem ? { key: heldIdem, terms } : void 0
3223
3246
  );
3224
3247
  }
3225
- const serverSettleNoWallet = this.env.isTest && !await walletAvailable(this.config.wallet, this.walletTimeoutPolicy());
3248
+ const serverSettleNoWallet = await this.shouldServerSettle();
3226
3249
  if (!serverSettleNoWallet) {
3227
3250
  const pinned = typeof input.identity === "string" ? input.identity.trim() : "";
3228
3251
  if (!pinned) {
@@ -3552,7 +3575,7 @@ var Playmos = class {
3552
3575
  });
3553
3576
  let raw;
3554
3577
  try {
3555
- if (await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
3578
+ if (!await this.shouldServerSettle() && await walletAvailable(this.config.wallet, this.walletTimeoutPolicy())) {
3556
3579
  const provider = this.walletProvider();
3557
3580
  raw = await provider.request({
3558
3581
  method: "eth_call",
package/dist/server.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { a2 as PlaymosError, W as WebhookEvent } from './errors-CYKgt2VU.cjs';
2
- export { ad as enterPathToBytes32 } from './errors-CYKgt2VU.cjs';
1
+ import { a2 as PlaymosError, W as WebhookEvent } from './errors-WcDNfb4q.cjs';
2
+ export { ad as enterPathToBytes32 } from './errors-WcDNfb4q.cjs';
3
3
 
4
4
  /**
5
5
  * Webhook signature verification (spec §6.2) — server-side only.
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { a2 as PlaymosError, W as WebhookEvent } from './errors-CYKgt2VU.js';
2
- export { ad as enterPathToBytes32 } from './errors-CYKgt2VU.js';
1
+ import { a2 as PlaymosError, W as WebhookEvent } from './errors-WcDNfb4q.js';
2
+ export { ad as enterPathToBytes32 } from './errors-WcDNfb4q.js';
3
3
 
4
4
  /**
5
5
  * Webhook signature verification (spec §6.2) — server-side only.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playmos/sdk",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
4
4
  "description": "Playmos SDK — stablecoin payments for games on Base. IAP and skill contests are flat 1%. USD in, USDC on-chain, no crypto UX for players.",
5
5
  "license": "MIT",
6
6
  "private": false,