@xgamesfun/solana 0.28.7 → 0.28.8

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/README.md CHANGED
@@ -77,7 +77,7 @@ codex plugin add xsolana@xgamesfun
77
77
 
78
78
  **claude.ai, ChatGPT or Claude Desktop:** add `https://xgames.fun/mcp` as a custom connector. Nothing to install.
79
79
 
80
- **Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.7 xsolana mcp`.
80
+ **Cursor, Cline, opencode or any MCP client:** run `npx -y --package=@xgamesfun/solana@0.28.8 xsolana mcp`.
81
81
 
82
82
  **Any agent that reads skills** (Copilot, Gemini CLI, Windsurf…): `npx skills add XGamesFun/solana-kit`.
83
83
 
@@ -88,26 +88,26 @@ are in [docs/harnesses.md](docs/harnesses.md).
88
88
 
89
89
  ```bash
90
90
  # find a worked example close to your idea
91
- npx -y @xgamesfun/solana@0.28.7 examples shop with loot boxes
91
+ npx -y @xgamesfun/solana@0.28.8 examples shop with loot boxes
92
92
 
93
93
  # using a coin you already launched? read it from the chain into its "tokens" entry
94
- npx -y @xgamesfun/solana@0.28.7 token <its mint address>
94
+ npx -y @xgamesfun/solana@0.28.8 token <its mint address>
95
95
 
96
96
  # does it work? where does the money go? what does it cost?
97
- npx -y @xgamesfun/solana@0.28.7 check xsolana.json
98
- npx -y @xgamesfun/solana@0.28.7 simulate xsolana.json
99
- npx -y @xgamesfun/solana@0.28.7 cost xsolana.json
97
+ npx -y @xgamesfun/solana@0.28.8 check xsolana.json
98
+ npx -y @xgamesfun/solana@0.28.8 simulate xsolana.json
99
+ npx -y @xgamesfun/solana@0.28.8 cost xsolana.json
100
100
 
101
101
  # the whole economy in plain words, and what a cheater could get
102
- npx -y @xgamesfun/solana@0.28.7 explain xsolana.json
103
- npx -y @xgamesfun/solana@0.28.7 review xsolana.json
102
+ npx -y @xgamesfun/solana@0.28.8 explain xsolana.json
103
+ npx -y @xgamesfun/solana@0.28.8 review xsolana.json
104
104
 
105
105
  # go live: a key file for the project (it prints the address to send SOL to, never the key)…
106
- npx -y @xgamesfun/solana@0.28.7 keygen ~/.config/xsolana/my-game.json
106
+ npx -y @xgamesfun/solana@0.28.8 keygen ~/.config/xsolana/my-game.json
107
107
  # …then everything going live would send, and a code; nothing is sent
108
- npx -y @xgamesfun/solana@0.28.7 approve xsolana.json --keypair ~/.config/xsolana/my-game.json
108
+ npx -y @xgamesfun/solana@0.28.8 approve xsolana.json --keypair ~/.config/xsolana/my-game.json
109
109
  # …and with that code, exactly what it listed is sent
110
- npx -y @xgamesfun/solana@0.28.7 approve xsolana.json --keypair ~/.config/xsolana/my-game.json --yes <code>
110
+ npx -y @xgamesfun/solana@0.28.8 approve xsolana.json --keypair ~/.config/xsolana/my-game.json --yes <code>
111
111
  ```
112
112
 
113
113
  Each command names an exact version, so you always run the release you meant to. `xsolana help` lists them all.
@@ -0,0 +1,198 @@
1
+ // Traps the program takes as written, found by a game that went live with them (Bubble Day, 2026-11-11): a ticket
2
+ // token bought with another token, so a cancelled round refunds the ticket token and not what was paid for it; a way
3
+ // back from that token left open at all times; a free grant of a token that is also sold and converts back; a least
4
+ // sale the scenario's players never reach; a ticket that needs a craft first, two wallet prompts unless both go in one
5
+ // transaction; and a token still earned after the last draw it was good for. Each is a warning: the person decides.
6
+ import { prizeLine, prizeToken, toSeconds, toTime } from './model.mjs';
7
+ import { DEFAULT_PER_DAY } from './simulate.mjs';
8
+
9
+ const DAY = 86_400;
10
+ const num = x => Number(x ?? 0);
11
+ const at = seconds =>
12
+ new Date(seconds * 1000)
13
+ .toISOString()
14
+ .replace('T', ' ')
15
+ .replace(/:\d\d\.\d+Z$/, ' UTC');
16
+ const named = part => `“${part?.name ?? part?.key}”`;
17
+ const words = list => (list.length < 2 ? list.join('') : `${list.slice(0, -1).join(', ')} and ${list.at(-1)}`);
18
+
19
+ /** The token a token line pays in or gives, by key; null for an item, an NFT or a roll. */
20
+ function lineToken(line, m) {
21
+ const prize = prizeLine(line);
22
+ if (prize?.gives !== 'tokens') return null;
23
+ return prizeToken(prize, m)?.key ?? prize.currency ?? m.currencyKey;
24
+ }
25
+
26
+ /** What a recipe takes in tokens: [{ token, amount }]. */
27
+ const paysIn = (r, m) =>
28
+ (r.in ?? []).filter(l => l.takes === 'pay').map(l => ({ token: l.currency ?? m.currencyKey, amount: num(l.amount) }));
29
+
30
+ /** What a recipe gives in tokens: [{ token, amount }]. */
31
+ const givesOut = (r, m) =>
32
+ (r.out ?? []).map(l => ({ token: lineToken(l, m), amount: num(l.amount) })).filter(x => x.token);
33
+
34
+ /** The token a lottery's tickets are paid in, or null for a free ticket or one paid with an NFT. */
35
+ const ticketOf = (l, m) =>
36
+ l.ticketNft || !(num(l.price) > 0) ? null : (l.ticketCurrency ?? l.currency ?? m.currencyKey);
37
+
38
+ /**
39
+ * Warns of the traps above. `warn(code, where, message, fix)` as check() calls it; `now` in seconds.
40
+ */
41
+ export function challenge(m, warn, now) {
42
+ const sym = key => m.tokens.get(key)?.symbol ?? key;
43
+ const on = r => r.active !== false;
44
+ /** The recipes that sell token `t` for another token: [{ recipe, pays: [{ token, amount }], makes }]. */
45
+ const sellers = t =>
46
+ m.recipes
47
+ .map(r => ({
48
+ recipe: r,
49
+ pays: paysIn(r, m).filter(p => p.token !== t),
50
+ makes: givesOut(r, m).find(g => g.token === t)?.amount ?? 0,
51
+ }))
52
+ .filter(s => s.makes > 0 && s.pays.length);
53
+ /** The recipes that turn token `t` back into another token: [{ recipe, takes, gives: [{ token, amount }] }]. */
54
+ const backs = t =>
55
+ m.recipes
56
+ .map(r => ({
57
+ recipe: r,
58
+ takes: paysIn(r, m).find(p => p.token === t)?.amount ?? 0,
59
+ gives: givesOut(r, m).filter(g => g.token !== t),
60
+ }))
61
+ .filter(b => b.takes > 0 && b.gives.length);
62
+ const priced = list => words(list.map(p => `${p.amount} ${sym(p.token)}`));
63
+ const tickets = new Map();
64
+ for (const l of m.lotteries) {
65
+ const t = ticketOf(l, m);
66
+ if (t) tickets.set(t, [...(tickets.get(t) ?? []), l]);
67
+ }
68
+
69
+ // ---- W-REFUND-NOT-PAID: a cancelled round refunds what reached it, in its ticket token.
70
+ for (const l of m.lotteries) {
71
+ const t = ticketOf(l, m);
72
+ // Tickets in the game's currency or SOL refund what the player paid.
73
+ if (!t || t === m.currencyKey || m.tokens.get(t)?.source === 'sol' || !(num(l.minSold) > 0)) continue;
74
+ const sold = sellers(t);
75
+ if (!sold.length) continue;
76
+ const s = sold[0];
77
+ const ways = sold.map(x => `${named(x.recipe)} (${priced(x.pays)} for ${x.makes} ${sym(t)})`);
78
+ warn(
79
+ 'W-REFUND-NOT-PAID',
80
+ `economy.lotteries.${l.key}.minSold`,
81
+ `${named(l)}’s tickets are paid in ${sym(t)}, which players buy with another token: ${words(ways)}. A round short of its ${l.minSold} tickets is cancelled, and its refund pays back ${sym(t)}, the ticket token, not the ${priced(s.pays)} a player paid for it: that stays where ${named(s.recipe)} sent it. A player who wanted their ${sym(s.pays[0].token)} back has no way to get it unless the project opens one.`,
82
+ `Price the tickets in ${sym(s.pays[0].token)} itself ("currency": "${s.pays[0].token}", "price": ${s.pays[0].amount / s.makes}, "rest": "burn" if they should burn), so a refund pays back what was paid; or keep a recipe that turns ${sym(t)} back into ${sym(s.pays[0].token)} switched off ("active": false) and switch it on after a cancelled round.`,
83
+ );
84
+ }
85
+
86
+ // ---- W-SWAP-BACK-OPEN: a way back from a ticket token, open at all times.
87
+ for (const [t, lots] of tickets) {
88
+ const made = sellers(t);
89
+ if (!made.length) continue;
90
+ const paidWith = new Set(made.flatMap(s => s.pays.map(p => p.token)));
91
+ for (const b of backs(t)) {
92
+ const r = b.recipe;
93
+ if (!on(r) || r.closes !== undefined || r.opens !== undefined) continue;
94
+ if (!b.gives.some(g => paidWith.has(g.token))) continue;
95
+ warn(
96
+ 'W-SWAP-BACK-OPEN',
97
+ `economy.recipes.${r.key}`,
98
+ `${named(r)} turns ${sym(t)}, the ticket of ${words(lots.map(named))}, back into ${priced(b.gives)} for ${b.takes}, and it is open with no "opens" or "closes": anyone holding ${sym(t)} can undo their purchase at any time, before a draw or after losing interest, and every ${sym(t)} given free becomes ${priced(b.gives)}. A way back is for a cancelled round.`,
99
+ `Make it "active": false and switch it on only after a cancelled round (an edit by the project), or give it "opens" and "closes" around the time it is for.`,
100
+ );
101
+ }
102
+ }
103
+
104
+ // ---- W-FREE-IS-PAID: a free grant of a token that is also sold, and turns back into another token.
105
+ for (const g of m.grants) {
106
+ const prize = prizeLine(g.prize ?? g.gives);
107
+ const t = lineToken(prize, m);
108
+ // The project's own currency, or SOL, given as a reward is what a grant is for.
109
+ if (!t || t === m.currencyKey || m.tokens.get(t)?.source === 'sol') continue;
110
+ const sold = sellers(t);
111
+ const back = backs(t);
112
+ if (!sold.length || !back.length) continue;
113
+ const b = back.find(x => on(x.recipe)) ?? back[0];
114
+ const per = num(prize.amount) / b.takes;
115
+ const worth = b.gives.map(x => ({ token: x.token, amount: +(x.amount * per).toFixed(9) }));
116
+ warn(
117
+ 'W-FREE-IS-PAID',
118
+ `economy.grants.${g.key}`,
119
+ `${named(g)} gives ${num(prize.amount)} ${sym(t)} for nothing, the token ${named(sold[0].recipe)} sells for ${priced(sold[0].pays)}, and ${named(b.recipe)} turns it back into ${priced(b.gives)}${on(b.recipe) ? '' : ' once it is switched on'}: each claim is worth ${priced(worth)}, and the grant’s limits are all that hold that back.`,
120
+ `Give the free one a token of its own that turns into nothing (a free-entry token a lottery also takes, or an NFT ticket), keep every way back switched off, or set the grant's "limits" to what you would pay out.`,
121
+ );
122
+ }
123
+
124
+ // ---- W-MINSOLD-VS-SCENARIO: a least sale above what the scenario's players buy in a round.
125
+ for (const l of m.lotteries) {
126
+ if (!(num(l.minSold) > 0)) continue;
127
+ const every = toSeconds(l.every);
128
+ const untilDraw = l.firstDraw ? toTime(l.firstDraw, now) - now : 0;
129
+ const roundDays = every ? every / DAY : untilDraw > 0 ? untilDraw / DAY : 0;
130
+ if (!(roundDays > 0)) continue;
131
+ const said = m.scenario.perDay?.[`lottery:${l.key}`];
132
+ const perDay = num(said ?? DEFAULT_PER_DAY);
133
+ let sold = perDay * roundDays;
134
+ const players = num(m.scenario.players);
135
+ const each = num(l.perWallet);
136
+ const capped = players > 0 && each > 0 && players * each < sold;
137
+ if (capped) sold = players * each;
138
+ sold = Math.floor(sold);
139
+ if (sold >= num(l.minSold)) continue;
140
+ const round = every ? 'a round' : `its one round, until its draw at ${at(toTime(l.firstDraw, now))}`;
141
+ warn(
142
+ 'W-MINSOLD-VS-SCENARIO',
143
+ `economy.lotteries.${l.key}.minSold`,
144
+ `${named(l)} is cancelled unless ${l.minSold} tickets sell in ${round}, and the scenario sells about ${sold}${capped ? ` (${players} players, at most ${each} each)` : said === undefined ? ` (it names no tickets for it: ${DEFAULT_PER_DAY} a day assumed)` : ` (${perDay} a day)`}. A cancelled round refunds every ticket and draws nothing. Its least sale can change only before the round's first ticket: the round keeps the terms it opened with.`,
145
+ `Lower "minSold" to what you expect to sell (or leave it out), or set the scenario ("scenario.perDay", "scenario.players") to what you really expect and check again.`,
146
+ );
147
+ }
148
+
149
+ // ---- W-TWO-PROMPTS: a ticket the player crafts first, then buys: one wallet prompt each, unless one transaction.
150
+ for (const [t, lots] of tickets) {
151
+ const made = sellers(t).filter(s => on(s.recipe) && !(s.recipe.out ?? []).some(o => o.gives === 'roll'));
152
+ if (!made.length || t === m.currencyKey) continue;
153
+ const s = made[0];
154
+ for (const l of lots)
155
+ warn(
156
+ 'W-TWO-PROMPTS',
157
+ `economy.lotteries.${l.key}`,
158
+ `A player buys ${named(l)}’s tickets with ${sym(t)}, which they get from ${named(s.recipe)}: a craft, then a purchase, two wallet prompts a buy (more, for each ticket crafted one at a time).`,
159
+ `Send both in one transaction: the play client's buyTickets(“${l.key}”, count, { listing, make: { recipe: “${s.recipe.key}”, times: ${num(l.price) / s.makes === 1 ? 'count' : `count × ${+(num(l.price) / s.makes).toFixed(9)}`}, listing } }) crafts what the tickets cost and buys them with one approval (kit 0.28.7 and later).`,
160
+ );
161
+ }
162
+
163
+ // ---- W-AFTER-THE-END: a token still given after the last draw it was good for.
164
+ for (const [t, lots] of tickets) {
165
+ if (t === m.currencyKey || m.tokens.get(t)?.source === 'sol' || lots.some(l => toSeconds(l.every))) continue;
166
+ if (lots.some(l => !l.firstDraw)) continue;
167
+ const used =
168
+ m.recipes.some(r => on(r) && paysIn(r, m).some(p => p.token === t)) ||
169
+ m.allBoxes.some(b => b.currency === t) ||
170
+ m.auctions.some(a => a.currency === t) ||
171
+ m.contests.some(c => c.currency === t) ||
172
+ m.payments.some(p => p.currency === t) ||
173
+ m.farms.some(f => f.stakes === t);
174
+ if (used) continue;
175
+ const last = Math.max(...lots.map(l => toTime(l.firstDraw, now)));
176
+ const lastLot = lots.find(l => toTime(l.firstDraw, now) === last);
177
+ const after = `${sym(t)} has no use after the last draw (${named(lastLot)}, ${at(last)})`;
178
+ for (const g of m.grants) {
179
+ if (g.active === false || lineToken(prizeLine(g.prize ?? g.gives), m) !== t) continue;
180
+ warn(
181
+ 'W-AFTER-THE-END',
182
+ `economy.grants.${g.key}`,
183
+ `${named(g)} keeps giving ${sym(t)} for as long as it is switched on, and ${after}: players go on claiming a token that does nothing.`,
184
+ `Switch it off after the draw ("active": false, an edit by the project), make the lottery repeat ("every"), or give ${sym(t)} a use after it.`,
185
+ );
186
+ }
187
+ for (const r of m.recipes) {
188
+ if (!on(r) || !givesOut(r, m).some(x => x.token === t)) continue;
189
+ if (r.closes !== undefined && toTime(r.closes, now) <= last) continue;
190
+ warn(
191
+ 'W-AFTER-THE-END',
192
+ `economy.recipes.${r.key}`,
193
+ `${named(r)} keeps making ${sym(t)}${r.closes === undefined ? ' with no "closes"' : ` until ${at(toTime(r.closes, now))}`}, and ${after}: a player can still pay for a token that does nothing.`,
194
+ `Give it "closes" at the draw (${at(last)}) or before, or switch it off after the draw.`,
195
+ );
196
+ }
197
+ }
198
+ }
@@ -62,6 +62,7 @@ import {
62
62
  } from './model.mjs';
63
63
  // plan.mjs imports check() in turn: each module only calls the other's functions, never at load time.
64
64
  import { boxArgs, boxOrderBytes, placeholderContext, recipeArgs, setupSteps, stepAloneBytes } from './plan.mjs';
65
+ import { challenge } from './challenge.mjs';
65
66
  import { findSecrets } from './secrets.mjs';
66
67
  import { simulate } from './simulate.mjs';
67
68
  import { BUDGET, MAX_TRANSACTION_BYTES, transactionLocks, transactionSize, WALLET_ROOM } from './txsize.mjs';
@@ -5823,6 +5824,10 @@ export function check(economy, { now = Math.floor(Date.now() / 1000), project =
5823
5824
  }
5824
5825
  }
5825
5826
 
5827
+ // ---- traps the program takes as written: a ticket token bought with another token, a way back left open, a free
5828
+ // grant worth a paid one, a least sale the scenario never reaches, two wallet prompts a buy, a token after its end
5829
+ if (!refusals.length) challenge(m, warn, now);
5830
+
5826
5831
  // ---- pools that run dry at the scenario's rates (W-POOL-DRY above names a pool nothing fills): one that is filled
5827
5832
  // but pays out faster drains, and then skips or fails what it owes.
5828
5833
  if (!refusals.length) {
@@ -1,6 +1,7 @@
1
1
  // The economy in plain words, from the checker, the simulation, the cost and the play-time steps: what players do
2
2
  // and what it costs them, how the pools fill and drain, what runs by itself, what the authority must sign, what
3
3
  // going live costs, and what the owner must know. Deterministic. Pure.
4
+ import { walkThrough } from './walk.mjs';
4
5
  import { check, describe } from './check.mjs';
5
6
  import { cost } from './cost.mjs';
6
7
  import {
@@ -616,6 +617,8 @@ export function explainEconomy(economy, { now = Math.floor(Date.now() / 1000) }
616
617
  '',
617
618
  `${realMoney() ? 'This economy is on mainnet: what it takes and what it pays is real money, and loot has not been independently audited.' : 'This economy is on devnet: nothing here is real money.'} Going live is signed on your own machine by the project’s key file, which is what “your wallet” means above. The command first prints everything it would send and sends nothing; it sends only when run again with the code it printed, which your agent is told to do only after your yes. No seed phrase or wallet key is ever asked for.`,
618
619
  );
620
+ const walk = walkThrough(m, { now });
621
+ if (walk.length) lines.push('', ...walk);
619
622
  return {
620
623
  ok: true,
621
624
  text: lines.join('\n').trim(),
@@ -24,6 +24,7 @@ import {
24
24
  toSeconds,
25
25
  } from './model.mjs';
26
26
  import { TRUST_LEVELS, TRUST_WORDS } from './trust.mjs';
27
+ import { walkThrough } from './walk.mjs';
27
28
 
28
29
  /** Severities, most serious first. */
29
30
  export const SEVERITIES = Object.freeze(['high', 'medium', 'low', 'info']);
@@ -710,6 +711,12 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
710
711
  ['W-LOTTERY-WEIGHT', 'R-LOTTERY-WEIGHT', 'info'],
711
712
  ['W-ZERO', 'R-ZERO', 'low'],
712
713
  ['W-ANSWER', 'R-ANSWER', 'low'],
714
+ ['W-REFUND-NOT-PAID', 'R-REFUND-NOT-PAID', 'medium'],
715
+ ['W-SWAP-BACK-OPEN', 'R-SWAP-BACK-OPEN', 'medium'],
716
+ ['W-FREE-IS-PAID', 'R-FREE-IS-PAID', 'medium'],
717
+ ['W-MINSOLD-VS-SCENARIO', 'R-MINSOLD-VS-SCENARIO', 'medium'],
718
+ ['W-AFTER-THE-END', 'R-AFTER-THE-END', 'low'],
719
+ ['W-TWO-PROMPTS', 'R-TWO-PROMPTS', 'low'],
713
720
  ])
714
721
  for (const w of warned(code)) find(id, severity, routeOf(w.where), w.message, w.fix);
715
722
 
@@ -718,7 +725,7 @@ export function review(economy, { now = Math.floor(Date.now() / 1000) } = {}) {
718
725
  ok: !findings.some(f => f.severity === 'high'),
719
726
  routes,
720
727
  findings,
721
- tradeoffs: tradeoffs(routes, findings),
728
+ tradeoffs: [tradeoffs(routes, findings), walkThrough(m, { now }).join('\n')].filter(Boolean).join('\n\n'),
722
729
  };
723
730
  }
724
731
 
@@ -52,7 +52,7 @@ import {
52
52
  } from './model.mjs';
53
53
 
54
54
  const DAY = 86_400;
55
- const DEFAULT_PER_DAY = 100;
55
+ export const DEFAULT_PER_DAY = 100;
56
56
  /** How many wallets the simulation's crowd is, next to one: what a person with many wallets could take from a grant. */
57
57
  const MANY_WALLETS = 100;
58
58
  /** Bids a day on an auction the scenario doesn't name (`auction:<key>`); a grant it doesn't name is claimed by nobody. */
@@ -0,0 +1,313 @@
1
+ // The walk-through at the end of `xsolana explain` and the review: each route of the spec, state by state (before it
2
+ // opens, while it runs, cancelled or failed, won, after it ends, a change of mind), as a player, the owner and a
3
+ // cheater see it, and what can change only before its first sale or claim. Words the program's rules back
4
+ // (PROGRAM.md), filled with the spec's own names, prices and dates, so an agent has the trade-offs in front of it at
5
+ // the moment it decides.
6
+ import { boxRolls, ORACLE_FEE, prizeLine, prizeToken, prizeWords, toSeconds, toTime } from './model.mjs';
7
+
8
+ const num = x => Number(x ?? 0);
9
+ const at = seconds =>
10
+ new Date(seconds * 1000)
11
+ .toISOString()
12
+ .replace('T', ' ')
13
+ .replace(/:\d\d\.\d+Z$/, ' UTC');
14
+ const named = part => `“${part?.name ?? part?.key}”`;
15
+ const words = list => (list.length < 2 ? list.join('') : `${list.slice(0, -1).join(', ')} and ${list.at(-1)}`);
16
+ const span = seconds =>
17
+ seconds % 86_400 === 0
18
+ ? `${seconds / 86_400} d`
19
+ : seconds % 3600 === 0
20
+ ? `${seconds / 3600} h`
21
+ : seconds % 60 === 0
22
+ ? `${seconds / 60} min`
23
+ : `${seconds} s`;
24
+
25
+ /** What a draw or a roll pays the oracle, in SOL. */
26
+ const oracleSol = Number(ORACLE_FEE) / 1e9;
27
+
28
+ /** The walk-through, as lines: a title, then for each route its name and one line a state. [] for a spec with no route. */
29
+ export function walkThrough(m, { now }) {
30
+ const sym = key => m.tokens.get(key)?.symbol ?? key;
31
+ const on = x => x.active !== false;
32
+ const lineToken = l => {
33
+ const p = prizeLine(l);
34
+ return p?.gives === 'tokens' ? (prizeToken(p, m)?.key ?? p.currency ?? m.currencyKey) : null;
35
+ };
36
+ const inWords = r =>
37
+ words(
38
+ (r.in ?? []).map(l =>
39
+ l.takes === 'pay'
40
+ ? `${num(l.amount)} ${sym(l.currency ?? m.currencyKey)}`
41
+ : l.item
42
+ ? `${l.count ?? 1} ${named(m.item.get(l.item) ?? { key: l.item })} (${l.takes === 'burn' ? 'burned' : l.takes === 'use' || l.takes === 'renew' ? 'used, kept' : l.takes === 'staked' ? 'staked, kept' : l.takes})`
43
+ : l.takes === 'show'
44
+ ? 'an NFT of a collection, shown and kept'
45
+ : l.takes === 'trade'
46
+ ? 'an NFT of a collection, traded in'
47
+ : l.takes,
48
+ ),
49
+ ) || 'nothing';
50
+ const outWords = r => words((r.out ?? []).map(o => prizeWords(o, m))) || 'nothing';
51
+ /** Recipes that make token `t` from something else, and recipes that turn it back into another token. */
52
+ const makers = t => m.recipes.filter(r => (r.out ?? []).some(o => lineToken(o) === t) && (r.in ?? []).length);
53
+ const backs = t =>
54
+ m.recipes.filter(
55
+ r =>
56
+ (r.in ?? []).some(l => l.takes === 'pay' && (l.currency ?? m.currencyKey) === t) &&
57
+ (r.out ?? []).some(o => lineToken(o) && lineToken(o) !== t),
58
+ );
59
+ const backWords = (t, how) => {
60
+ const b = backs(t);
61
+ if (!b.length) return '';
62
+ const open = b.filter(on);
63
+ return open.length
64
+ ? ` ${words(open.map(named))} ${how} (open now).`
65
+ : ` ${words(b.map(named))} ${how} once the project switches it on.`;
66
+ };
67
+ const windowWords = (x, what, none) =>
68
+ !on(x)
69
+ ? `It is switched off ("active": false): ${none} until the project switches it on.`
70
+ : x.opens !== undefined
71
+ ? `${none[0].toUpperCase()}${none.slice(1)} before ${at(toTime(x.opens, now))}.`
72
+ : `It ${what} from the moment it is live.`;
73
+ const perWalletWords = x =>
74
+ x.perWallet ? ` (at most ${x.perWallet} a wallet: a wallet is free, so it counts wallets, not people)` : '';
75
+ const routes = [];
76
+ const route = (title, rows) =>
77
+ routes.push(title, ...rows.filter(([, text]) => text).map(([s, text]) => ` - ${s}: ${text}`));
78
+
79
+ for (const l of m.lotteries) {
80
+ const t = l.ticketNft ? null : (l.ticketCurrency ?? l.currency ?? m.currencyKey);
81
+ const price = num(l.price);
82
+ const every = toSeconds(l.every);
83
+ const draw = l.firstDraw ? at(toTime(l.firstDraw, now)) : null;
84
+ const made = t ? makers(t) : [];
85
+ const closesBefore = num(toSeconds(l.closesBefore));
86
+ const ticket = l.ticketNft
87
+ ? 'an NFT that passes its rule'
88
+ : price > 0
89
+ ? `${price} ${sym(t)}`
90
+ : 'nothing but the player fee';
91
+ route(
92
+ `${named(l)}: a lottery, ${every ? `drawn every ${span(every)}` : `drawn once${draw ? `, at ${draw}` : ''}`}`,
93
+ [
94
+ [
95
+ 'Before it opens',
96
+ `It sells from the moment it is live; its first ticket opens the round.${l.gate ? ' Only wallets its gate admits buy.' : ''}`,
97
+ ],
98
+ [
99
+ 'While it runs',
100
+ `Player: pays ${ticket} a ticket${made.length ? ` (${sym(t)} comes from ${words(made.map(named))}: a craft before the purchase)` : ''}${perWalletWords(l)}, and holds an entry in the round, not a token: it can't be sold or taken back. Sales close ${closesBefore ? `${span(closesBefore)} before` : 'at'} the draw. Owner: nothing to send; an edit reaches the next round, never the one running. Cheater: many wallets buy many tickets; nobody sees or picks the result, drawn from the oracle's answer after sales close.`,
101
+ ],
102
+ [
103
+ 'Cancelled or failed',
104
+ `${num(l.minSold) > 0 ? `A round short of ${l.minSold} tickets is cancelled when its draw is asked, with no draw. ` : ''}A draw the oracle hasn't answered 7 days after it was asked is cancelled at the next ask. Player: every ticket is refunded${t && price > 0 ? ` in ${sym(t)}` : ''}${l.ticketNft ? ' (the NFT back)' : ''}, the player fee not; anyone may pull a refund to its buyer.${made.length ? ` What was paid for the ${sym(t)} does not come back with it:${backWordsOr(backWords(t, 'turns it back'), ` nothing turns ${sym(t)} back.`)}` : ''} Owner: nothing to send; a round nobody asks to draw waits, with no deadline. Cheater: nothing to gain: a refund goes to the entry's own buyer.`,
105
+ ],
106
+ [
107
+ 'Won',
108
+ `Once it is due anyone asks for the draw, paying the oracle ${oracleSol} SOL, and anyone pulls each winner's part to the winner: ${words((l.places ?? []).map((p, i) => `place ${i + 1} ${p.prize ? prizeWords(p.prize, m) : `${num(p.percent)}% of the pot`}`)) || 'the pot'}. An item is given while it has stock${(l.places ?? []).some(p => prizeLine(p.prize)?.gives === 'item' && m.item.get(prizeLine(p.prize).item)?.kind !== 'collectible') ? '; a normal item is an off-chain prize, yours to deliver from its receipt' : ''}.`,
109
+ ],
110
+ [
111
+ 'After it ends',
112
+ every
113
+ ? 'The next round sells at once, on the lottery’s terms as they are then.'
114
+ : `It sells no more tickets.${t && price > 0 && t !== m.currencyKey ? ` ${sym(t)} left in wallets does only what something else takes it for.` : ''} The owner may close it once its round is paid out, for its rent back.`,
115
+ ],
116
+ [
117
+ 'A change of mind',
118
+ `No way out: a ticket stays in the round until its draw or a cancel.${t && made.length ? backWords(t, `turns ${sym(t)} not yet spent on tickets back`) : ''}`,
119
+ ],
120
+ [
121
+ 'Only before the first ticket',
122
+ `The round copies the lottery's terms (price, places, least sale, draw date) at its first ticket: an edit after it reaches only the next round${every ? '' : ', and a lottery that draws once has none'}.`,
123
+ ],
124
+ ],
125
+ );
126
+ }
127
+
128
+ for (const b of m.allBoxes) {
129
+ const item = b.shopOf ? m.item.get(b.shopOf) : null;
130
+ const source = item ?? b;
131
+ const rolls = boxRolls(b, m);
132
+ const cur = sym(b.currency ?? m.currencyKey);
133
+ const price = num(b.price);
134
+ const what = item ? `${named(item)} from the shop` : named(b);
135
+ route(`${what}: ${item ? 'an item for sale' : rolls ? 'a box, rolled by the oracle' : 'a box'}`, [
136
+ ['Before it opens', windowWords(source, 'sells', 'nothing is sold')],
137
+ [
138
+ 'While it runs',
139
+ `Player: pays ${price > 0 ? `${price} ${cur}` : 'nothing but the player fee'}${perWalletWords(source)}; ${rolls ? `the oracle rolls what they get (paid ${oracleSol} SOL), which lands with its answer` : 'gets what it gives at once'}. Owner: nothing to send${rolls ? '; an edit of its entries or odds refunds every paid roll still waiting, at once; a price change reaches the next purchase' : ''}. Cheater: ${rolls ? "can't see or pick a roll" : 'nothing to take: it sells at its price'}${item?.supply || (!item && rolls) ? '; bots can buy limited stock first' : ''}.`,
140
+ ],
141
+ [
142
+ 'Cancelled or failed',
143
+ rolls
144
+ ? "A roll the oracle hasn't answered is asked again once it is stale, by its player or your keys; nothing times it out. A prize out of stock is never set aside for a roll."
145
+ : 'A purchase that can’t give what it sells fails whole: nothing is taken.',
146
+ ],
147
+ [
148
+ 'After it ends',
149
+ source.closes !== undefined
150
+ ? `Nothing sells after ${at(toTime(source.closes, now))}.`
151
+ : 'It sells until its stock runs out or the project switches it off.',
152
+ ],
153
+ ['A change of mind', 'No refund once paid.'],
154
+ [
155
+ 'Only before the first sale',
156
+ item?.kind === 'collectible' ? 'The item’s collection can’t change once a unit is out.' : '',
157
+ ],
158
+ ]);
159
+ }
160
+
161
+ for (const r of m.recipes) {
162
+ const takes = (r.in ?? []).filter(l => l.takes === 'pay').map(l => l.currency ?? m.currencyKey);
163
+ const back = takes.length
164
+ ? m.recipes.filter(
165
+ x =>
166
+ x !== r &&
167
+ (x.out ?? []).some(o => takes.includes(lineToken(o))) &&
168
+ (x.in ?? []).some(
169
+ l => l.takes === 'pay' && (r.out ?? []).some(o => lineToken(o) === (l.currency ?? m.currencyKey)),
170
+ ),
171
+ )
172
+ : [];
173
+ route(`${named(r)}: a recipe`, [
174
+ ['Before it opens', windowWords(r, 'crafts', 'nothing is crafted')],
175
+ [
176
+ 'While it runs',
177
+ `Player: gives ${inWords(r)}, gets ${outWords(r)}, in one transaction, all or nothing${perWalletWords(r)}. Owner: nothing to send; an edit reaches the next craft. Cheater: crafts as often as they hold what it takes.`,
178
+ ],
179
+ ['Cancelled or failed', 'A craft that can’t give every line fails whole: nothing is taken.'],
180
+ [
181
+ 'After it ends',
182
+ r.closes !== undefined
183
+ ? `Nothing crafts after ${at(toTime(r.closes, now))}.`
184
+ : 'It crafts until the project switches it off.',
185
+ ],
186
+ [
187
+ 'A change of mind',
188
+ back.length
189
+ ? `${words(back.map(named))} turns it back${back.some(on) ? '' : ' once the project switches it on'}.`
190
+ : 'No undo: what it took is spent.',
191
+ ],
192
+ ]);
193
+ }
194
+
195
+ for (const g of m.grants) {
196
+ const prize = prizeWords(g.prize ?? g.gives, m);
197
+ const how = g.vouchers?.length
198
+ ? 'a voucher from the project’s results server'
199
+ : g.list
200
+ ? 'a place on its list'
201
+ : 'nothing but its limits';
202
+ const limits = Object.entries(g.limits ?? {})
203
+ .map(([k, v]) => `${k} ${typeof v === 'object' ? JSON.stringify(v) : v}`)
204
+ .join(', ');
205
+ route(`${named(g)}: a grant`, [
206
+ [
207
+ 'Before it opens',
208
+ on(g)
209
+ ? 'Claims work from the moment it is live.'
210
+ : 'It is switched off ("active": false): no claim until the project switches it on.',
211
+ ],
212
+ [
213
+ 'While it runs',
214
+ `Player: claims ${prize}, proving it with ${how}; only their own wallet signs the claim. Owner: ${g.vouchers?.length ? 'the results server must be up to sign vouchers (no server, no claims), and ' : ''}its funds pay each claim's rent: keep them topped up. Cheater: ${g.vouchers?.length ? `gets whatever the server signs (${g.trust ?? 'what it checks'}): a signed result is worth what the server checks; ` : g.list ? '' : 'every wallet may claim; '}its limits are the ceiling (${limits || 'none'}).`,
215
+ ],
216
+ [
217
+ 'Cancelled or failed',
218
+ 'A claim whose prize can’t be given (a pool run dry, an item out of stock) is refused and keeps its round: it can be claimed later.',
219
+ ],
220
+ ['After it ends', 'It gives until its limits are reached or the project switches it off: it has no end date.'],
221
+ ]);
222
+ }
223
+
224
+ for (const a of m.auctions) {
225
+ route(`${named(a)}: an auction`, [
226
+ [
227
+ 'Before it opens',
228
+ a.start !== undefined ? `No bid before ${at(toTime(a.start, now))}.` : 'Bids from the moment it opens.',
229
+ ],
230
+ [
231
+ 'While it runs',
232
+ `Player: bids at least ${num(a.reserve) || 1} ${sym(a.currency ?? m.currencyKey)}; a higher bid sends theirs back in the same instruction. Owner: nothing to send; its terms are fixed when it opens. Cheater: anyone, the seller included, can bid to push the price${a.extendBy ? `; a bid in the last ${a.extendBy} moves the end` : ''}.`,
233
+ ],
234
+ [
235
+ 'Cancelled or failed',
236
+ 'With no bid it ends unsold and the lot goes home. The owner or authority may call off a sale whose lot has not moved: the bid back, the lot home.',
237
+ ],
238
+ ['Won', 'Anyone ends it after its end: the lot to the high bidder, the bid to the seller and the shares.'],
239
+ ['A change of mind', 'A bid can’t be taken back: only a higher bid returns it before the end.'],
240
+ ['Only before the first bid', 'Every term is fixed when it opens.'],
241
+ ]);
242
+ }
243
+
244
+ for (const c of m.contests) {
245
+ route(`${named(c)}: a contest`, [
246
+ [
247
+ 'While it runs',
248
+ `Player: enters one side${c.stake ? `, a stake of ${num(c.stake)}` : ''}${c.entriesClose ? ` (entries close: ${c.entriesClose})` : ''}; a wallet backs one side, never two. Owner: ${c.decidedBy === 'authority' || !c.deciders ? 'the result is yours to decide once entries close' : 'its deciders sign the result once entries close'}. Cheater: the result is worth what its decider checks; nobody names a side before entries close.`,
249
+ ],
250
+ [
251
+ 'Cancelled or failed',
252
+ `With no result by its result deadline${c.minPot ? `, or less than ${num(c.minPot)} in its sides when entries close` : ''}, or a call-off, every entry is refunded.`,
253
+ ],
254
+ ['Won', 'The winners claim their share of the pot, by what each put in.'],
255
+ ['A change of mind', 'An entry stays until the result or a refund.'],
256
+ ['Only before the first entry', 'Every term is fixed when it opens and never edited.'],
257
+ ]);
258
+ }
259
+
260
+ for (const f of m.farms) {
261
+ route(`${named(f)}: a farm`, [
262
+ [
263
+ 'While it runs',
264
+ `Player: stakes ${f.stakes === 'collectibles' ? 'NFTs' : sym(f.stakes ?? m.currencyKey)} and earns while staked, by tier. Owner: ${f.rate ? 'its rate pays from its rewards' : 'its stakers share what arrives in its rewards vault: nothing arrives, nothing is paid'}. Cheater: ${f.custody === 'watch' ? 'a watched NFT sold keeps earning until someone refreshes its seat' : 'many wallets stake as one: rewards follow the stake, not the person'}.`,
265
+ ],
266
+ [
267
+ 'After it ends',
268
+ f.runs
269
+ ? `It takes no stake after its end (${f.runs}); seats keep what they earned.`
270
+ : 'It has no end: it runs until it is closed.',
271
+ ],
272
+ ['A change of mind', 'A stake leaves once its tier’s lock has run (earlier only where a tier sets an exit fee).'],
273
+ ['Only before it opens', 'A farm is never edited once open: a change is a new farm.'],
274
+ ]);
275
+ }
276
+
277
+ for (const s of m.schedules)
278
+ route(`${named(s)}: a schedule`, [
279
+ [
280
+ 'While it runs',
281
+ s.sealed
282
+ ? 'Its recipient sends each run when it is due; nobody can stop it.'
283
+ : 'Owner: you, or a key of the project’s, sends each run when it is due: nothing runs it otherwise, and a missed run lapses. Player: nothing to do. Cheater: nothing: only the project’s keys send it.',
284
+ ],
285
+ ]);
286
+
287
+ for (const p of m.payments)
288
+ route(`${named(p)}: a payment`, [
289
+ [
290
+ 'While it runs',
291
+ `Player: pays ${p.any ? 'what they choose' : `${num(p.amount)} ${sym(p.currency ?? m.currencyKey)}`}. Owner: what it pays for is yours to deliver from its receipt.`,
292
+ ],
293
+ ['A change of mind', 'No refund: the program keeps no record to undo.'],
294
+ ]);
295
+
296
+ for (const h of m.holds)
297
+ route(`${named(h)}: a hold`, [
298
+ [
299
+ 'While it runs',
300
+ 'Player: hands an NFT to the project until the time they chose. Owner: until then the authority may release it, to any wallet. Cheater: whoever holds the authority’s key could release it elsewhere before its time: a hold trusts the project.',
301
+ ],
302
+ ['After it ends', 'Anyone sends it home once its time is up.'],
303
+ ]);
304
+
305
+ if (!routes.length) return [];
306
+ return [
307
+ 'Each route, state by state (as a player, the owner and a cheater see it)',
308
+ 'The owner’s pause stops every purchase, ticket, entry, stake and bid coming in; nothing owed waits on it: claims, refunds, unstakes, draws and payouts go on.',
309
+ ...routes,
310
+ ];
311
+ }
312
+
313
+ const backWordsOr = (text, otherwise) => text || otherwise;
package/js/mcp/server.mjs CHANGED
@@ -15,9 +15,14 @@ import { withCluster } from './with-cluster.mjs';
15
15
  /** Where loot runs, for every agent that connects. */
16
16
  const WHERE = `loot ${VERSION} runs on devnet (program ${CLUSTERS.devnet.program}) and on mainnet (program ${CLUSTERS.mainnet.program}). An economy is on devnet, where nothing is real money, unless its file says "network": "mainnet", where everything is. Build and try an economy on devnet first. loot has not been independently audited.`;
17
17
 
18
+ /** How to meet a request: the simplest version first, its trade said, before building what was asked. */
19
+ const CHALLENGE =
20
+ 'Challenge before you build. For every request, name the simplest version that delivers most of it. Say what it gives up and what the asked-for version costs: wallet prompts, moving parts, failure states, who must act later. Recommend one. Build the asked-for version only after the person has seen that trade and still wants it. Challenge economy, flow and irreversible choices; don’t challenge taste. Composing parts is still how you find the versions; xsolana_explain ends with each route’s states as a player, the owner and a cheater see them.';
21
+
18
22
  export const INSTRUCTIONS = [
19
23
  'loot is one shared Solana program for game and community economies (shops, loot boxes, lotteries, auctions, staking farms, crafting, contests, schedules, perks for holders of any NFT, capped rewards). An economy is a JSON spec, not a new smart contract: never write Anchor or Rust for what loot covers.',
20
24
  'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec; xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets); xsolana_save; xsolana_publish when they say to put it live. Going live is signed by a key file on their own machine (xsolana keygen makes one; never ask for, read or print a key) with xsolana approve --keypair, which prints everything it would send and sends nothing until it is run again with the code it printed: do that only after they have read the printout and said yes. That key owns the project for good: a change to an economy that is already live is signed by the key that owns it, never by a new one. A browser wallet’s signing page (approve --browser) only if they ask for one, and they run it themselves.',
25
+ CHALLENGE,
21
26
  WHERE,
22
27
  ].join(' ');
23
28
 
@@ -27,6 +32,7 @@ export const INSTRUCTIONS = [
27
32
  */
28
33
  export const REMOTE_INSTRUCTIONS = [
29
34
  INSTRUCTIONS.split(' Start with')[0],
35
+ CHALLENGE,
30
36
  'Start with xsolana_guide, then xsolana_examples with the person’s own words; write the spec and pass it whole as "spec" to every tool (this hosted server reads no files); xsolana_check after every change; xsolana_simulate and xsolana_explain before telling them what it does; xsolana_review before going live (fix its high findings, say honestly what each voucher server checks, and tell the person what a cheater gets).',
31
37
  `To go live, the spec goes in a file named xsolana.json on the person’s own computer and is signed there by a key file. In that folder: npx -y @xgamesfun/solana@${PACKAGE_VERSION} keygen ~/.config/xsolana/<name>.json makes the key file (it prints the address, never the key; the person sends that address devnet SOL), then npx -y @xgamesfun/solana@${PACKAGE_VERSION} approve xsolana.json --keypair <that file> prints everything that would be sent and sends nothing. When the person has read that and said yes, the same command with --yes <the code it printed> sends it. Run them yourself if you have a terminal there; otherwise hand them to the person. Never ask for, read or print a key. Say once what the key file is: the project’s owner for good (whoever can read it controls the project; they back it up themselves; nothing restores a lost one) and, unless the spec names a treasury, where what the economy earns lands. A change to an economy that is already live is signed by the key that owns it: never make a new key for it. A browser wallet’s signing page (the same approve command with --browser in place of --keypair), which the person runs themselves, only if they ask for one or a browser wallet owns the project. (xsolana_save and xsolana_publish, which tool texts may name, run only in the local server.)`,
32
38
  WHERE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xgamesfun/solana",
3
- "version": "0.28.7",
3
+ "version": "0.28.8",
4
4
  "mcpName": "io.github.XGamesFun/solana-kit",
5
5
  "description": "Loot boxes, gacha, shops, lotteries, auctions, NFT staking, crafting and rewards on Solana without writing a smart contract: one shared program, its JS client, a CLI, and an MCP server and skill so Claude Code, Codex or any AI agent can design, check, simulate and launch an economy.",
6
6
  "license": "Apache-2.0",
@@ -11,9 +11,9 @@ signed by a key file on the owner's own machine, once they have read what will b
11
11
  **devnet**, where nothing is real money, and on **mainnet**, where everything is: an economy is on devnet unless its
12
12
  file says `"network": "mainnet"`.
13
13
 
14
- The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.7 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
15
- `npx -y @xgamesfun/solana@0.28.7 token <mint>` for a coin made elsewhere,
16
- or `npx -y @xgamesfun/solana@0.28.7 tool <name> '<json>'` for any tool (always with the exact version).
14
+ The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.28.8 <guide|examples|check|simulate|cost|explain|review|publish|status> [xsolana.json]`,
15
+ `npx -y @xgamesfun/solana@0.28.8 token <mint>` for a coin made elsewhere,
16
+ or `npx -y @xgamesfun/solana@0.28.8 tool <name> '<json>'` for any tool (always with the exact version).
17
17
 
18
18
  ## Rules (always)
19
19
 
@@ -56,22 +56,24 @@ or `npx -y @xgamesfun/solana@0.28.7 tool <name> '<json>'` for any tool (always w
56
56
  7. **Take addresses from the tools, never from memory**: the program, its accounts, the IDL. Every answer names them.
57
57
  8. **Public data only in a spec**: names, prices, percents, public addresses. Leave out `id`, `index`, `shopBox`,
58
58
  and `live`: they are written when it goes live.
59
- 9. **Compose before you say no.** Most ideas are several parts working together, not one part. Before calling
60
- anything a limit, break the idea into its pieces (what players pay, what they get, what is random, what the chain
61
- can't see, what is held and for how long, who is paid) and find the part for each (`references/sentence-to-spec.md`).
62
- What the chain can't see (a match, a score, something done offline) is vouched for by a signer: a results server's voucher, a
63
- decider's key, a list. The signer is not the end of the story: the program checks its signature and limits, never
64
- what happened, so a signed result is worth what that server checks (it decides the result, replays it, or only
65
- bounds what the page reports: rule 12). Build the combination and say which part does which piece. When the tools' words and the
66
- checker disagree (a field the schema takes that a sentence somewhere calls impossible), the checker and
67
- `xsolana_guide`'s parts decide: try it in a spec and run `xsolana_check`.
68
- A piece is a limit only when no part or combination does it. Then:
69
- - say in one line what is missing and why;
70
- - offer the nearest versions (`xsolana_examples` with the words of that piece returns them), best first, each with
71
- what it gives up and its "in all" cost; recommend one, and build it;
72
- - if they push back, offer the next version or a change that brings back what they miss (a price, a bundle, a
73
- second step). Don't withdraw a draft that checks, and don't send them elsewhere while a version on loot is on the
74
- table.
59
+ 9. **Challenge before you build.** For every request, name the simplest version that delivers most of it. Say what
60
+ it gives up and what the asked-for version costs: wallet prompts, moving parts, failure states, who must act
61
+ later. Recommend one. Build the asked-for version only after the person has seen that trade and still wants it.
62
+ Challenge economy, flow and irreversible choices; don't challenge taste. `xsolana_explain` ends with each route's
63
+ states (before it opens, while it runs, cancelled or failed, won, after it ends, a change of mind) as a player, the
64
+ owner and a cheater see them, and what can change only before a first sale or claim; the checker's warnings name
65
+ the traps a game already fell into. Read both before you answer.
66
+ Composing parts is still how you find the versions. Most ideas are several parts working together: break the idea
67
+ into its pieces (what players pay, what they get, what is random, what the chain can't see, what is held and for
68
+ how long, who is paid) and find the part for each (`references/sentence-to-spec.md`). What the chain can't see (a
69
+ match, a score, something done offline) is vouched for by a signer: a results server's voucher, a decider's key, a
70
+ list. The program checks the signer's signature and limits, never what happened, so a signed result is worth what
71
+ that server checks (it decides the result, replays it, or only bounds what the page reports: rule 12). When the
72
+ tools' words and the checker disagree (a field the schema takes that a sentence somewhere calls impossible), the
73
+ checker and `xsolana_guide`'s parts decide: try it in a spec and run `xsolana_check`. A piece is a limit only when
74
+ no part or combination does it: say in one line what is missing and why, and offer the nearest versions
75
+ (`xsolana_examples` with the words of that piece returns them), best first, each with what it gives up and its "in
76
+ all" cost.
75
77
 
76
78
  10. **Design for many wallets and bots.** Wallets are free and bots play well: bound every reward with totals
77
79
  (`perDay`, `total`), gate open rewards on holding something or count people by vouchers (`perPerson`, with the
@@ -141,7 +143,7 @@ or `npx -y @xgamesfun/solana@0.28.7 tool <name> '<json>'` for any tool (always w
141
143
  9. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the commands, which you
142
144
  run in the project folder exactly as given (they name the exact version; never drop it):
143
145
  - **A new economy gets a key file**, unless the project has one:
144
- `npx -y @xgamesfun/solana@0.28.7 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
146
+ `npx -y @xgamesfun/solana@0.28.8 keygen ~/.config/xsolana/<name>.json`. Give the person the address it prints. A
145
147
  Solana key file they already have and name themselves (`~/.config/solana/id.json`) works the same; never go
146
148
  looking for one.
147
149
  - **Say what the key file is** before anything is sent: the project's owner, and, unless the spec says otherwise,
@@ -156,7 +158,7 @@ or `npx -y @xgamesfun/solana@0.28.7 tool <name> '<json>'` for any tool (always w
156
158
  devnet SOL for an economy on devnet, real SOL for one on mainnet), any seed in a coin that already exists, and every NFT a stash, a raffle or an auction names. A
157
159
  collection they already have needs the key's address as its update authority. If they would rather not move
158
160
  those, offer the browser wallet.
159
- - **Show it**: `npx -y @xgamesfun/solana@0.28.7 approve xsolana.json --keypair <key file>` prints every step with
161
+ - **Show it**: `npx -y @xgamesfun/solana@0.28.8 approve xsolana.json --keypair <key file>` prints every step with
160
162
  what it pays and trusts, the warnings, the notice and a code, and sends nothing. Give the person that printout as
161
163
  it is, not a summary of it, and ask.
162
164
  - **On their yes**, the same command with `--yes <code>`. What lands is written back into `xsolana.json`. If it
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Warns of the traps above. `warn(code, where, message, fix)` as check() calls it; `now` in seconds.
3
+ */
4
+ export declare function challenge(m: any, warn: any, now: any): void;
@@ -1,3 +1,4 @@
1
+ export declare const DEFAULT_PER_DAY = 100;
1
2
  export declare const round: (x: any, digits?: number) => any;
2
3
  /** A budget of sales to walk, shared by every salePrices call of one simulation. */
3
4
  export declare const walkBudget: () => {
@@ -0,0 +1,4 @@
1
+ /** The walk-through, as lines: a title, then for each route its name and one line a state. [] for a spec with no route. */
2
+ export declare function walkThrough(m: any, { now }: {
3
+ now: any;
4
+ }): any[];
@@ -299,10 +299,10 @@ export declare function runTool(name: any, input?: {}, ctx?: {}): Promise<{
299
299
  })[];
300
300
  text: string;
301
301
  } | {
302
+ token?: undefined;
302
303
  mint?: undefined;
303
304
  ok: boolean;
304
305
  text: string;
305
- token?: undefined;
306
306
  } | {
307
307
  mint?: undefined;
308
308
  ok: boolean;
@@ -745,10 +745,10 @@ export declare const tools: ({
745
745
  additionalProperties: boolean;
746
746
  };
747
747
  handler(input: any, ctx: any): Promise<{
748
- token?: undefined;
749
748
  mint?: undefined;
750
749
  ok: boolean;
751
750
  text: string;
751
+ token?: undefined;
752
752
  } | {
753
753
  mint?: undefined;
754
754
  ok: boolean;