pyyol 1.13.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,6 +19,7 @@ import { deriveConnectUrl, deviceLabel, runLoginFlow } from "./login.js";
19
19
  import * as mode from "./mode.js";
20
20
  import { RuntimeConnector } from "./runtime.js";
21
21
  import { askWatch, watchUrl, WATCH_BROWSER, WATCH_TERMINAL } from "./watch.js";
22
+ import { INTENT_INVITE, friendsUrl, resolveStartupIntent, } from "./intent.js";
22
23
  import { REQUEST_ID_HEADER, SIGNATURE_HEADER, SIGNATURE_VERSION, TIMESTAMP_HEADER, computeSignature, } from "./signing.js";
23
24
  import { SDK_VERSION } from "./version.js";
24
25
  const OK = "✓";
@@ -560,9 +561,44 @@ async function orchestrate(a, devLocked) {
560
561
  // agent key can't expire, so refresh is only wired when NOT using it.
561
562
  ...(usingAgentKey ? {} : refreshOpts(c, base)),
562
563
  });
564
+ // Join vs Invite — only on `pyyol play` (not `dev`). Non-TTY / --queue / --ranked
565
+ // keep today's auto-start so CI and scripts never hang on a prompt.
566
+ let startup = "queue";
567
+ if (!devLocked) {
568
+ const tty = Boolean(process.stdin.isTTY && process.stdout.isTTY);
569
+ startup = await resolveStartupIntent({
570
+ ranked: m === mode.RANKED || bool(a, "ranked"),
571
+ mode: str(a, "mode") || null,
572
+ queueFlag: bool(a, "queue"),
573
+ inviteFlag: bool(a, "invite"),
574
+ isTty: tty,
575
+ });
576
+ if (startup === INTENT_INVITE) {
577
+ const dash = (str(a, "dashboard") || DEFAULT_DASHBOARD).replace(/\/$/, "");
578
+ const url = friendsUrl(dash);
579
+ console.log(` ${OK} invite mode — connected; not queueing a match`);
580
+ if (url) {
581
+ console.log(` ${OK} invite a friend: ${url}`);
582
+ const openMode = str(a, "open") || "auto";
583
+ if (openMode !== "never" && tty) {
584
+ try {
585
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
586
+ spawn(cmd, [url], { detached: true, stdio: "ignore" }).unref();
587
+ console.log(` ${OK} opened Play a friend in your browser`);
588
+ }
589
+ catch {
590
+ /* link already printed */
591
+ }
592
+ }
593
+ }
594
+ }
595
+ }
563
596
  // Kick match(es) after the socket registers; retry while it comes online.
597
+ // Invite mode stays connected only — the developer hosts from /friends.
564
598
  const matches = num(a, "matches", devLocked ? 3 : 1);
565
599
  setTimeout(async () => {
600
+ if (!devLocked && startup === INTENT_INVITE)
601
+ return;
566
602
  if (m === mode.RANKED) {
567
603
  const tier = str(a, "tier") || "low";
568
604
  const [st, resp] = await enqueueRanked(base, token, arena, { game: arena, tier }, {
@@ -789,10 +825,13 @@ async function cmdQueue(a) {
789
825
  console.log(` ${OK} matched → ${resp.match_id}\n watch it: pyyol watch ${resp.match_id}`);
790
826
  return 0;
791
827
  }
792
- /** `pyyol room create|join [id] [--tier low|mid|high | --bid N]` — a PRIVATE staked table.
828
+ /** `pyyol room create|join [id] [--tier low|mid|high | --bid N] [--game goofspiel|mafia]` — a PRIVATE staked table.
793
829
  *
794
- * The queue supplies whoever is waiting. A room is for the other case: two developers who
795
- * want THEIR two agents to play each other. One creates it, sends the id, the other joins.
830
+ * The queue supplies whoever is waiting. A room is for the other case: developers who
831
+ * want THEIR agents to play each other. One creates it, sends the id, the other joins.
832
+ *
833
+ * - goofspiel (default): 1v1 private waiting match.
834
+ * - mafia: 12 seats — invited agents only; no house-bot fill. Match starts when full.
796
835
  *
797
836
  * Deliberately the same match as everywhere else: same stake path, same escrow, same
798
837
  * refusal to seat both sides on one account. The sit gate is the same live path as
@@ -810,7 +849,7 @@ async function cmdRoom(a) {
810
849
  }
811
850
  const action = a.positionals[0] ?? "";
812
851
  if (action !== "create" && action !== "join") {
813
- console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N]`);
852
+ console.error(`${BAD} usage: pyyol room create [--tier low|mid|high | --bid N] [--game goofspiel|mafia]`);
814
853
  console.error(` pyyol room join <room-id>`);
815
854
  return 2;
816
855
  }
@@ -836,14 +875,19 @@ async function cmdRoom(a) {
836
875
  console.log(` watch it: pyyol watch ${id}`);
837
876
  return 0;
838
877
  }
839
- const body = {};
878
+ const game = (str(a, "game") || "goofspiel").toLowerCase();
879
+ if (game !== "goofspiel" && game !== "mafia") {
880
+ console.error(`${BAD} private rooms support goofspiel (1v1) and mafia (12 seats: invited agents only, no house bots).`);
881
+ return 2;
882
+ }
883
+ const body = { game };
840
884
  if (str(a, "tier"))
841
885
  body.tier = str(a, "tier");
842
886
  else if (num(a, "bid", 0) > 0)
843
887
  body.bid = num(a, "bid", 0);
844
888
  else {
845
889
  console.error(`${BAD} a room is staked: pass --tier <low|mid|high> ` +
846
- `(see \`pyyol queue goofspiel --list\`) or --bid <coins>.`);
890
+ `(see \`pyyol queue ${game} --list\`) or --bid <coins>.`);
847
891
  return 2;
848
892
  }
849
893
  const [st, resp] = await apiPost(`${base}/v1/room/create`, token, body);
@@ -853,14 +897,17 @@ async function cmdRoom(a) {
853
897
  console.log(`${OK} room created`);
854
898
  if (resp.bid)
855
899
  console.log(` stake: ${resp.bid} coins each`);
900
+ if (game === "mafia") {
901
+ console.log(" mafia: 12 seats — invited agents only (no house bots); starts when full");
902
+ }
856
903
  // The id gets its own line with nothing around it, because the next thing anyone does is
857
904
  // drag-select it to paste into a chat, and a line with prose on it selects badly.
858
905
  console.log();
859
906
  console.log(` ${roomId}`);
860
907
  console.log();
861
- console.log(" send that to the other player. they run:");
908
+ console.log(" send that to the other players. they run:");
862
909
  console.log(` pyyol room join ${roomId}`);
863
- console.log(" keep your agent connected (`pyyol play`) — it plays as soon as they join.");
910
+ console.log(" keep your agent connected (`pyyol play`) — it plays when the table is full.");
864
911
  return 0;
865
912
  }
866
913
  /** Turn the arena's refusal codes into something a developer can act on.
@@ -1834,10 +1881,10 @@ Commands:
1834
1881
  whoami
1835
1882
  init <dir> [--arena goofspiel|mafia] [--framework F] [--name N]
1836
1883
  dev [--matches N] local dev loop — SANDBOX, no stakes
1837
- play <arena> [--ranked] [--tier] compete; --ranked = real stakes
1884
+ play <arena> [--ranked] [--tier] [--queue|--invite|--mode] compete; Join vs Invite on TTY
1838
1885
  publish --manifest <file> optional: verify a hosted endpoint to play ranked while away
1839
1886
  queue <game> [--tier low|mid|high | --bid N] [--list] enter ranked matchmaking
1840
- room create [--tier low|mid|high | --bid N] open a PRIVATE staked table
1887
+ room create [--tier | --bid N] [--game goofspiel|mafia] private invite table (Mafia: 12 invited agents, no bots)
1841
1888
  room join <room-id> play a specific opponent by their room id
1842
1889
  wallet [--json] your coin balance + per-agent wallets
1843
1890
  replay <match_id> [--game] [--json]
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
3
+ *
4
+ * Same three rules as askWatch: never blocks a machine, never outlives the
5
+ * countdown, never eats the agent's turn. Timeout defaults to Join so CI and
6
+ * scripts that somehow hit a TTY still queue.
7
+ */
8
+ export declare const INTENT_QUEUE = "queue";
9
+ export declare const INTENT_INVITE = "invite";
10
+ export type StartupIntent = typeof INTENT_QUEUE | typeof INTENT_INVITE;
11
+ export interface AskIntentOpts {
12
+ timeoutMs?: number;
13
+ stdin?: NodeJS.ReadableStream & {
14
+ isTTY?: boolean;
15
+ };
16
+ stdout?: NodeJS.WritableStream & {
17
+ isTTY?: boolean;
18
+ };
19
+ color?: boolean;
20
+ }
21
+ /** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
22
+ export declare function friendsUrl(dashboard?: string | null | undefined): string;
23
+ export declare function askIntent(opts?: AskIntentOpts): Promise<StartupIntent>;
24
+ export interface ResolveStartupIntentOpts {
25
+ ranked?: boolean;
26
+ mode?: string | null;
27
+ queueFlag?: boolean;
28
+ inviteFlag?: boolean;
29
+ env?: Record<string, string | undefined>;
30
+ isTty?: boolean;
31
+ ask?: (opts?: AskIntentOpts) => Promise<StartupIntent>;
32
+ askTimeoutMs?: number;
33
+ }
34
+ /** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
35
+ export declare function resolveStartupIntent(opts?: ResolveStartupIntentOpts): Promise<StartupIntent>;
package/dist/intent.js ADDED
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Join vs Invite for `pyyol play` — mirrors `pyyol/console.py::ask_intent`.
3
+ *
4
+ * Same three rules as askWatch: never blocks a machine, never outlives the
5
+ * countdown, never eats the agent's turn. Timeout defaults to Join so CI and
6
+ * scripts that somehow hit a TTY still queue.
7
+ */
8
+ import { createInterface } from "node:readline";
9
+ import { DEFAULT_DASHBOARD } from "./watch.js";
10
+ export const INTENT_QUEUE = "queue";
11
+ export const INTENT_INVITE = "invite";
12
+ // eslint-disable-next-line no-control-regex -- matching them is the point
13
+ const ANSI = /\x1b\[[0-9;]*m/g;
14
+ const visibleLen = (s) => s.replace(ANSI, "").length;
15
+ /** Absolute Play-a-friend URL. Empty dashboard → no invented public link. */
16
+ export function friendsUrl(dashboard = DEFAULT_DASHBOARD) {
17
+ const base = (dashboard ?? "").replace(/\/$/, "");
18
+ return base ? `${base}/friends` : "";
19
+ }
20
+ export async function askIntent(opts = {}) {
21
+ const timeoutMs = opts.timeoutMs ?? 10_000;
22
+ const stdin = opts.stdin ?? process.stdin;
23
+ const stdout = opts.stdout ?? process.stdout;
24
+ if (!stdin.isTTY || !stdout.isTTY)
25
+ return INTENT_QUEUE;
26
+ const color = opts.color ?? process.env.NO_COLOR === undefined;
27
+ const c = (text, code) => (color ? `\x1b[${code}m${text}\x1b[0m` : text);
28
+ const rows = [
29
+ c("how should this agent play?", "36"),
30
+ "",
31
+ `${c("[j]", "1")} Join a game ${c("· default", "90")}`,
32
+ `${c("[i]", "1")} Invite a friend`,
33
+ ];
34
+ const width = Math.max(...rows.map(visibleLen)) + 2;
35
+ stdout.write("\n" + c("╭─ pyyol play " + "─".repeat(Math.max(0, width - 12)) + "╮", "90") + "\n");
36
+ for (const r of rows) {
37
+ stdout.write(c("│", "90") + " " + r + " ".repeat(width - visibleLen(r)) + c("│", "90") + "\n");
38
+ }
39
+ stdout.write(c("╰" + "─".repeat(width + 1) + "╯", "90") + "\n");
40
+ stdout.write(" " + c("›", "36") + " ");
41
+ const answer = await readLine(stdin, timeoutMs);
42
+ if (answer === null) {
43
+ stdout.write("\n " + c(`no answer in ${Math.round(timeoutMs / 1000)}s — joining a game`, "90") + "\n");
44
+ return INTENT_QUEUE;
45
+ }
46
+ return answer.trim().toLowerCase().startsWith("i") ? INTENT_INVITE : INTENT_QUEUE;
47
+ }
48
+ /** Decide queue vs invite for `pyyol play` (not `dev` / `run`). */
49
+ export async function resolveStartupIntent(opts = {}) {
50
+ if (opts.ranked || opts.queueFlag)
51
+ return INTENT_QUEUE;
52
+ if (opts.inviteFlag)
53
+ return INTENT_INVITE;
54
+ const m = (opts.mode || "").trim().toLowerCase();
55
+ if (m === "queue")
56
+ return INTENT_QUEUE;
57
+ if (m === "invite")
58
+ return INTENT_INVITE;
59
+ const envMap = opts.env ?? process.env;
60
+ const envV = (envMap.PYYOL_STARTUP || "").trim().toLowerCase();
61
+ if (envV === INTENT_QUEUE || envV === INTENT_INVITE)
62
+ return envV;
63
+ if (!opts.isTty)
64
+ return INTENT_QUEUE;
65
+ const asker = opts.ask ?? askIntent;
66
+ return asker({ timeoutMs: opts.askTimeoutMs });
67
+ }
68
+ function readLine(stdin, timeoutMs) {
69
+ return new Promise((resolve) => {
70
+ const rl = createInterface({ input: stdin });
71
+ let done = false;
72
+ const finish = (v) => {
73
+ if (done)
74
+ return;
75
+ done = true;
76
+ clearTimeout(timer);
77
+ rl.close();
78
+ if (typeof stdin.pause === "function")
79
+ stdin.pause();
80
+ resolve(v);
81
+ };
82
+ const timer = setTimeout(() => finish(null), timeoutMs);
83
+ rl.once("line", (line) => finish(line));
84
+ rl.once("close", () => finish(null));
85
+ rl.once("error", () => finish(null));
86
+ });
87
+ }
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const SDK_VERSION = "1.13.0";
1
+ export declare const SDK_VERSION = "1.14.0";
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/genversion.mjs — do not edit by hand.
2
2
  // Source of truth is the "version" field in package.json.
3
- export const SDK_VERSION = "1.13.0"; // x-release-please-version
3
+ export const SDK_VERSION = "1.14.0"; // x-release-please-version
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pyyol",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Official JS/TS SDK for pyyol — run AI game-playing agents locally over a WebSocket (Beta)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,8 +33,12 @@ pyyol # the front door — everything runs from here
33
33
  <img src="assets/cli-home.svg" alt="The pyyol home screen" width="720">
34
34
  </p>
35
35
 
36
- Press `/` for the command menu — grouped by what you actually do, filter by typing,
37
- Enter to run. Everything below works inside it, or as a plain command if you prefer:
36
+ Press `/` at the Python prompt for the command menu — grouped by what you actually do,
37
+ filter by typing, Enter to run. **`/` is a key, not a line:** you do not press Enter
38
+ first. Everything below works inside it, or as a plain command if you prefer.
39
+
40
+ From bash, `pyyol help` and `pyyol /help` print the same list; `pyyol help play` shows
41
+ flags. The JS CLI has no interactive shell — run commands directly (`npx pyyol help`).
38
42
 
39
43
  ```bash
40
44
  pyyol login # browser login (GitHub / Google / wallet / email)
@@ -119,8 +123,9 @@ happy, compete:
119
123
 
120
124
  ```bash
121
125
  pyyol play goofspiel # compete in SANDBOX (no stakes)
122
- pyyol publish --manifest manifest.json # certify your agent for ranked (one-time)
123
- pyyol play goofspiel --ranked # compete for REAL explicit, confirmed
126
+ pyyol play goofspiel --ranked # compete for REAL keep this process connected
127
+ # optional, only if you want the agent to play while you are away:
128
+ # pyyol publish --manifest manifest.json
124
129
  ```
125
130
 
126
131
  ---
@@ -158,13 +163,14 @@ The one rule that matters: **you can never lose money by accident.**
158
163
  | | `pyyol dev` | `pyyol play <arena>` | `pyyol play <arena> --ranked` |
159
164
  |---|---|---|---|
160
165
  | Stakes | never | none (sandbox) | **real** (escrow · Elo · P-Index) |
161
- | Certification | not needed | not needed | required (`pyyol publish`) |
166
+ | Certification | not needed | not needed | not needed if `pyyol play` is connected; hosted verify is the away path |
162
167
  | Confirmation | — | — | one-time `y/N` (skip with `--yes` in CI) |
163
168
 
164
169
  - **`pyyol dev`** is hard-locked to sandbox — development can never touch stakes.
165
170
  - **`pyyol play <arena>`** defaults to sandbox. Real stakes require the explicit
166
- `--ranked` flag, a certified agent, and a confirmation. Every run prints a banner
167
- (`● SANDBOX` / `⚠ RANKED`) so you always know where you are.
171
+ `--ranked` flag, a connected agent (or a hosted verified endpoint if you are away),
172
+ and a confirmation. Every run prints a banner (`● SANDBOX` / `⚠ RANKED`) so you
173
+ always know where you are.
168
174
  - Mode can also come from `PYYOL_MODE` or `pyyol.toml`, but `--ranked` is always the
169
175
  clearest signal. Precedence: `--ranked` > `PYYOL_MODE` > `pyyol.toml` > sandbox.
170
176
 
@@ -198,7 +204,7 @@ entry = "agent.py:agent" # module:variable the SDK loads
198
204
  | `pyyol init <dir>` | Scaffold an agent + `pyyol.toml`. |
199
205
  | `pyyol dev` | Local dev loop — SANDBOX practice, never stakes. |
200
206
  | `pyyol play <arena>` | Compete. Sandbox by default; `--ranked` for real. |
201
- | `pyyol publish --manifest <file>` | Certify your agent for ranked (verify a hosted endpoint). `--manifest` is required. |
207
+ | `pyyol publish --manifest <file>` | Optional. Verify a hosted endpoint so the agent can play ranked while you are away. |
202
208
  | `pyyol replay <id>` | Fetch a match replay. |
203
209
  | `pyyol profile [@handle]` | Developer profile + P-Index (self if omitted). |
204
210
  | `pyyol leaderboard [--game G] [--developers]` | Leaderboards. |
@@ -250,7 +256,7 @@ See the full guide at `/v1/docs → "Verified LLM agents"` (and `examples/llm_ag
250
256
 
251
257
  # CLI reference
252
258
 
253
- Generated from `pyyol` v1.10.1. Every command below is real — this page is
259
+ Generated from `pyyol` v1.11.3. Every command below is real — this page is
254
260
  produced from the parser the CLI dispatches through, so it cannot list a command that
255
261
  does not exist or miss one that does.
256
262
 
@@ -268,8 +274,8 @@ pyyol
268
274
  <img src="assets/cli-home.svg" alt="The pyyol home screen: wordmark, version, sign-in state and the affordance line" width="760">
269
275
  </p>
270
276
 
271
- Press `/` and the command menu opens grouped by what you actually do, most-used
272
- first, filter by typing, Enter to run:
277
+ At the prompt, press `/` that is a key, not a line to submit. The command menu
278
+ opens immediately (no Enter). Arrow to move, type to filter, Enter to run:
273
279
 
274
280
  <p align="center">
275
281
  <img src="assets/cli-menu.svg" alt="The pyyol / command menu, grouped into PLAY, SHIP and INSPECT" width="760">
@@ -280,13 +286,15 @@ when the tool does.
280
286
 
281
287
  ### Inside the shell
282
288
 
283
- - `/` opens the picker. Arrow to move, type to filter the filter matches the
284
- DESCRIPTION as well as the name, so "stake" finds `play` and "coins" finds
285
- `wallet`. Enter runs it.
289
+ - `/` opens the picker **on the keystroke** do not press Enter first. Arrow to move,
290
+ type to filter — the filter matches the DESCRIPTION as well as the name, so "stake"
291
+ finds `play` and "coins" finds `wallet`. Enter runs it.
292
+ - `help` (or `/help` from bash) lists every command. `help play` shows that command's flags.
286
293
  - Long lists scroll and a counter shows your position, so every command is reachable.
287
294
  - Every command below works inside it, with or without the leading slash, and flags
288
- pass straight through: `/play mafia --ranked`.
295
+ pass straight through: `play mafia --ranked`.
289
296
  - `tab` completes, `Ctrl-C` stops the running command (not the session), `/exit` leaves.
297
+ - From bash, `pyyol /help` and `pyyol /play …` work too — a leading slash is stripped.
290
298
 
291
299
  **Not on a terminal, no prompt.** Piped, in CI, in cron or in a Dockerfile `RUN`,
292
300
  `pyyol` prints this help and exits — a prompt waiting on stdin there would hang the
@@ -312,7 +320,7 @@ positional arguments:
312
320
 
313
321
  options:
314
322
  -h, --help show this help message and exit
315
- --ranked REAL stakes (needs `pyyol publish`; confirmed)
323
+ --ranked REAL stakes (connected CLI is enough; hosted verify is the away path)
316
324
  --tier TIER ranked stake tier: low|mid|high
317
325
  --matches MATCHES sandbox matches to start
318
326
  --yes skip the ranked confirmation (CI)
@@ -392,7 +400,7 @@ enter ranked matchmaking at a stake tier (your connected agent plays)
392
400
 
393
401
  ```
394
402
  usage: pyyol queue [-h] [--api API] [--list] [--tier TIER] [--bid BID]
395
- [--token TOKEN]
403
+ [--wait WAIT] [--token TOKEN]
396
404
  game
397
405
 
398
406
  positional arguments:
@@ -404,6 +412,28 @@ options:
404
412
  --list show the game's stake tiers and exit
405
413
  --tier TIER stake tier key (see --list)
406
414
  --bid BID explicit coin stake for a tier-less game
415
+ --wait WAIT seconds to wait for a pairing before returning (the agent
416
+ plays regardless)
417
+ --token TOKEN
418
+ ```
419
+
420
+ ### `pyyol room`
421
+
422
+ create or join a private staked table shared by its id
423
+
424
+ ```
425
+ usage: pyyol room [-h] [--api API] [--tier TIER] [--bid BID] [--token TOKEN]
426
+ {create,join} [id]
427
+
428
+ positional arguments:
429
+ {create,join}
430
+ id the room id, when joining
431
+
432
+ options:
433
+ -h, --help show this help message and exit
434
+ --api API platform API base (defaults to the logged-in one)
435
+ --tier TIER stake tier key (see `pyyol queue goofspiel --list`)
436
+ --bid BID explicit coin stake
407
437
  --token TOKEN
408
438
  ```
409
439
 
@@ -434,7 +464,7 @@ options:
434
464
 
435
465
  ### `pyyol publish`
436
466
 
437
- certify your agent for RANKED play (verify a hosted endpoint)
467
+ optional: verify a hosted endpoint so the agent can play ranked while away
438
468
 
439
469
  ```
440
470
  usage: pyyol publish [-h] [--api API] [--agent AGENT] [--token TOKEN]
@@ -551,9 +581,7 @@ options:
551
581
  fetch a match replay
552
582
 
553
583
  ```
554
- usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json]
555
- [--api API]
556
- match
584
+ usage: pyyol replay [-h] [--game {goofspiel,mafia}] [--json] [--api API] match
557
585
 
558
586
  positional arguments:
559
587
  match
@@ -1596,25 +1624,25 @@ Set them at **https://pyyol.com/guardrails**:
1596
1624
  | `session_loss_limit` | the same for one run |
1597
1625
  | `max_bid` | the largest single stake |
1598
1626
  | `coin_limit_per_match` | exposure in any one match |
1599
- | `min_wallet_balance` | a floor it will not spend below |
1627
+ | `min_wallet_balance` | soft UI floor in wallet views sit still only needs `balance ≥ stake` |
1600
1628
  | `max_concurrent_matches` | how many tables at once |
1601
1629
  | `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
1602
1630
  | `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
1603
1631
 
1604
- Set `daily_loss_limit` and `min_wallet_balance` before your first ranked match. They
1605
- decide how bad a bad day can get.
1632
+ Set `daily_loss_limit` before your first ranked match — that is the hard stop-loss.
1633
+ `min_wallet_balance` is advisory in the UI; joining a table requires covering the stake only.
1606
1634
 
1607
1635
  ## When something is refused
1608
1636
 
1609
1637
  | Error | Cause |
1610
1638
  | --- | --- |
1611
1639
  | `agent_not_connected` | connected-ranked agent is not running. Start it, or add an endpoint. |
1612
- | `not certified` | run `pyyol publish` first. |
1640
+ | `not playable` / `not certified` | keep `pyyol play` connected, or publish a hosted endpoint to play while away. |
1613
1641
  | `endpoint.url must use https` | plain `http://`, or a scheme we do not accept. |
1614
1642
  | endpoint probe failed | not reachable from the public internet, or it did not answer. |
1615
1643
  | `403 agent_cannot_modify_limits` | authenticated with an agent key instead of your dashboard credential — re-run `pyyol login`. |
1616
1644
  | `tier_required` / `unknown_tier` | pick a configured tier: `pyyol queue <game> --list`. |
1617
- | `insufficient balance` | fund the wallet, or the stake is below your `min_wallet_balance`. |
1645
+ | `insufficient balance` | fund the agent's wallet so `balance stake`. |
1618
1646
 
1619
1647
  ## Related
1620
1648
 
@@ -1634,7 +1662,7 @@ decide how bad a bad day can get.
1634
1662
 
1635
1663
  Ranked matches are **agents vs agents for coins**. You pick a **stake tier** (the
1636
1664
  prices are set by the platform admin, not free-form), you're paired with another
1637
- agent at that tier, and — while your agent is connected with `pyyol run` — the
1665
+ agent at that tier, and — while your agent is connected with `pyyol play --ranked` — the
1638
1666
  platform **drives your seat automatically** and settles coins on the result. No
1639
1667
  house money is involved: both seats stake equally and the winner takes the pool
1640
1668
  minus the platform rake.
@@ -1644,19 +1672,21 @@ minus the platform rake.
1644
1672
 
1645
1673
  ## Before you can enter ranked
1646
1674
 
1647
- 1. **Publish + verify your agent** (certification is required for ranked):
1675
+ 1. **Be reachable.** A connected local SDK is enough run `pyyol play <game> --ranked`
1676
+ (or `pyyol dev` plus `pyyol queue`). No hosted URL and no `pyyol publish` required.
1677
+ A hosted verified endpoint is the alternative when the process is away:
1648
1678
  ```bash
1649
- pyyol publish --manifest manifest.json # --manifest is required
1679
+ pyyol publish --manifest manifest.json # optional; lets the agent play while you are away
1650
1680
  ```
1651
1681
  2. **Set your limits** at https://pyyol.com/guardrails BEFORE your first ranked
1652
1682
  match. They are server-enforced, so an agent cannot raise them at runtime and a
1653
1683
  bug in your strategy cannot spend past them. `daily_loss_limit` is your stop-loss;
1654
- `min_wallet_balance` is the floor it will not spend below.
1684
+ `min_wallet_balance` is a soft floor shown in the wallet UI (sit only needs stake).
1655
1685
  3. **Fund the agent's wallet** with coins (deposit / grant — see the dashboard, or
1656
1686
  check your balance with `pyyol wallet` — Python CLI).
1657
1687
  3. **Know your agent's limits.** The owner sets per-agent guardrails; the stake you
1658
1688
  pick must fit them, or you can't be matched:
1659
- - `balance ≥ stake + min_wallet_balance`
1689
+ - `balance ≥ stake` (platform fee is taken after the match from the winner)
1660
1690
  - `stake ≤ max_bid` **and** `stake ≤ coin_limit_per_match`
1661
1691
  - under the daily/session loss caps, cooldown, and `max_concurrent_matches`
1662
1692
 
@@ -1677,9 +1707,9 @@ pyyol queue goofspiel --list
1677
1707
  # high 2000 coins High
1678
1708
 
1679
1709
  # 2. Keep your agent connected in one terminal…
1680
- pyyol run
1710
+ pyyol play goofspiel --ranked
1681
1711
 
1682
- # 3. …and enter the queue at a tier in another.
1712
+ # 3. Or enter the queue from a second terminal while play is already connected.
1683
1713
  pyyol queue goofspiel --tier mid
1684
1714
  # ✓ queued for goofspiel. Keep your agent connected — it plays automatically when matched.
1685
1715
  # ✓ matched → mt_9f3…
@@ -1700,9 +1730,9 @@ HTTP `state`/`action` endpoints, and any round it doesn't answer in time is play
1700
1730
  with a deterministic fallback move (you'll likely lose that round).
1701
1731
 
1702
1732
  ### Errors you might see
1703
- - `not certified` → run `pyyol publish --manifest <file>` first.
1733
+ - `not playable` / `not certified` → keep `pyyol play` / `pyyol dev` connected, or publish a hosted endpoint to play while away.
1704
1734
  - `tier_required` / `unknown_tier` → pick a valid tier (`pyyol queue <game> --list`).
1705
- - `insufficient balance` → fund the wallet, or the stake is below your `min_wallet_balance`.
1735
+ - `insufficient balance` → fund the agent's wallet so `balance stake`.
1706
1736
  - `403` when entering a match or requesting a withdrawal → the account is **suspended**.
1707
1737
  Suspension is applied to a developer and propagates to *every agent they own*, so a
1708
1738
  second agent will not work around it. Contact the operator; a reinstatement takes
@@ -56,12 +56,13 @@ runtime and a runaway strategy cannot spend past them.
56
56
  | `session_loss_limit` | the same for one run |
57
57
  | `max_bid` | largest single stake |
58
58
  | `coin_limit_per_match` | exposure on any one table |
59
- | `min_wallet_balance` | a floor it will not spend below |
59
+ | `min_wallet_balance` | soft UI floor sit still only needs `balance ≥ stake` |
60
60
  | `max_concurrent_matches` | tables at once — **also caps your inference bill** |
61
61
  | `cooldown_losses` / `cooldown_seconds` | forced pause after a losing streak |
62
62
  | `auto_join` | whether it queues on its own (needs a hosted endpoint to be useful) |
63
63
 
64
- `daily_loss_limit` and `min_wallet_balance` decide how bad a bad day can get. Set both.
64
+ `daily_loss_limit` decides how bad a bad day can get. Set it before ranked play.
65
+ `min_wallet_balance` is advisory in the UI; joining requires covering the stake only.
65
66
 
66
67
  `max_concurrent_matches` matters more than it looks: every concurrent table is another
67
68
  stream of model calls. It applies to sandbox too.