@xgamesfun/solana 0.0.0-stage → 0.24.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.
Files changed (138) hide show
  1. package/DISCLAIMER.md +41 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +13 -0
  4. package/README.md +172 -2
  5. package/bin/xsolana.mjs +189 -0
  6. package/examples/artwork-auction.json +51 -0
  7. package/examples/blind-forge.json +128 -0
  8. package/examples/coin-flip-arena.json +103 -0
  9. package/examples/duel-arena.json +137 -0
  10. package/examples/elements-set.json +274 -0
  11. package/examples/event-badges.json +67 -0
  12. package/examples/expedition-guild.json +154 -0
  13. package/examples/fair-presale.json +48 -0
  14. package/examples/falling-drop.json +90 -0
  15. package/examples/guild-missions.json +137 -0
  16. package/examples/halving-seasons.json +107 -0
  17. package/examples/hero-trials.json +93 -0
  18. package/examples/ideas.json +789 -0
  19. package/examples/level-ladder.json +65 -0
  20. package/examples/merge-2048.json +128 -0
  21. package/examples/nft-raffle.json +33 -0
  22. package/examples/paper-hands-farm.json +44 -0
  23. package/examples/partner-vault.json +112 -0
  24. package/examples/sacrifice-raffle.json +118 -0
  25. package/examples/scorched-treasury.json +84 -0
  26. package/examples/shelter-fair.json +164 -0
  27. package/examples/spark-pity.json +153 -0
  28. package/examples/stakers-draw.json +118 -0
  29. package/examples/star-fleet.json +150 -0
  30. package/examples/starforce.json +116 -0
  31. package/examples/stud-farm.json +134 -0
  32. package/examples/team-vesting.json +70 -0
  33. package/examples/trade-in-shop.json +168 -0
  34. package/js/approve/details.mjs +263 -0
  35. package/js/approve/ed25519.mjs +36 -0
  36. package/js/approve/live-check.mjs +144 -0
  37. package/js/approve/page.html +86 -0
  38. package/js/approve/page.js +203 -0
  39. package/js/approve/server.mjs +726 -0
  40. package/js/chain/base58.js +73 -0
  41. package/js/chain/borsh.js +270 -0
  42. package/js/chain/bytes.js +141 -0
  43. package/js/chain/core.js +181 -0
  44. package/js/chain/curve.js +52 -0
  45. package/js/chain/lookup.js +279 -0
  46. package/js/chain/mint.js +96 -0
  47. package/js/chain/pda.js +66 -0
  48. package/js/chain/programs.js +102 -0
  49. package/js/chain/rpc.js +219 -0
  50. package/js/chain/sha256.js +68 -0
  51. package/js/chain/token.js +85 -0
  52. package/js/chain/token2022.js +247 -0
  53. package/js/chain/tx.js +224 -0
  54. package/js/client/accounts.js +679 -0
  55. package/js/client/compressed.js +46 -0
  56. package/js/client/grant.js +299 -0
  57. package/js/client/idl/loot.js +2 -0
  58. package/js/client/idl/loot.json +17206 -0
  59. package/js/client/index.js +10 -0
  60. package/js/client/ix.js +2887 -0
  61. package/js/client/pda.js +114 -0
  62. package/js/client/policy.js +69 -0
  63. package/js/client/random.js +70 -0
  64. package/js/client/sizes.js +297 -0
  65. package/js/client/traits.js +44 -0
  66. package/js/connect/connect.mjs +182 -0
  67. package/js/economy/check.mjs +2236 -0
  68. package/js/economy/cost.mjs +185 -0
  69. package/js/economy/examples.mjs +114 -0
  70. package/js/economy/explain.mjs +239 -0
  71. package/js/economy/guide.mjs +232 -0
  72. package/js/economy/legal.mjs +15 -0
  73. package/js/economy/live.mjs +130 -0
  74. package/js/economy/model.mjs +698 -0
  75. package/js/economy/plan.mjs +1155 -0
  76. package/js/economy/schema.json +3071 -0
  77. package/js/economy/secrets.mjs +60 -0
  78. package/js/economy/simulate.mjs +1019 -0
  79. package/js/economy/txsize.mjs +45 -0
  80. package/js/economy/validate.mjs +85 -0
  81. package/js/index.js +10 -0
  82. package/js/mcp/server.mjs +113 -0
  83. package/js/mcp/tools.mjs +485 -0
  84. package/js/play/index.js +2855 -0
  85. package/js/play/keys.js +167 -0
  86. package/js/results/index.js +184 -0
  87. package/js/results/wire.js +90 -0
  88. package/package.json +100 -4
  89. package/release.json +8 -0
  90. package/skills/xsolana-economy/SKILL.md +109 -0
  91. package/skills/xsolana-economy/references/after-launch.md +124 -0
  92. package/skills/xsolana-economy/references/apps.md +193 -0
  93. package/skills/xsolana-economy/references/sentence-to-spec.md +180 -0
  94. package/skills/xsolana-economy/references/telling-the-person.md +134 -0
  95. package/types/chain/base58.d.ts +10 -0
  96. package/types/chain/borsh.d.ts +23 -0
  97. package/types/chain/bytes.d.ts +45 -0
  98. package/types/chain/core.d.ts +52 -0
  99. package/types/chain/curve.d.ts +1 -0
  100. package/types/chain/lookup.d.ts +194 -0
  101. package/types/chain/mint.d.ts +25 -0
  102. package/types/chain/pda.d.ts +25 -0
  103. package/types/chain/programs.d.ts +65 -0
  104. package/types/chain/rpc.d.ts +68 -0
  105. package/types/chain/sha256.d.ts +1 -0
  106. package/types/chain/token.d.ts +141 -0
  107. package/types/chain/token2022.d.ts +53 -0
  108. package/types/chain/tx.d.ts +60 -0
  109. package/types/client/accounts.d.ts +318 -0
  110. package/types/client/compressed.d.ts +29 -0
  111. package/types/client/grant.d.ts +170 -0
  112. package/types/client/idl/loot.d.ts +4498 -0
  113. package/types/client/index.d.ts +9 -0
  114. package/types/client/ix.d.ts +5315 -0
  115. package/types/client/pda.d.ts +95 -0
  116. package/types/client/policy.d.ts +48 -0
  117. package/types/client/random.d.ts +26 -0
  118. package/types/client/sizes.d.ts +101 -0
  119. package/types/client/traits.d.ts +12 -0
  120. package/types/economy/check.d.mts +100 -0
  121. package/types/economy/cost.d.mts +69 -0
  122. package/types/economy/examples.d.mts +27 -0
  123. package/types/economy/explain.d.mts +19 -0
  124. package/types/economy/guide.d.mts +140 -0
  125. package/types/economy/legal.d.mts +12 -0
  126. package/types/economy/live.d.mts +124 -0
  127. package/types/economy/model.d.mts +431 -0
  128. package/types/economy/plan.d.mts +1014 -0
  129. package/types/economy/secrets.d.mts +4 -0
  130. package/types/economy/simulate.d.mts +164 -0
  131. package/types/economy/txsize.d.mts +17 -0
  132. package/types/economy/validate.d.mts +3 -0
  133. package/types/index.d.ts +9 -0
  134. package/types/mcp/tools.d.mts +635 -0
  135. package/types/play/index.d.ts +880 -0
  136. package/types/play/keys.d.ts +51 -0
  137. package/types/results/index.d.ts +155 -0
  138. package/types/results/wire.d.ts +122 -0
@@ -0,0 +1,109 @@
1
+ ---
2
+ name: xsolana-economy
3
+ description: Build a web3, token or NFT economy on Solana without writing a smart contract, on loot, one shared program every project uses. Covers shops, loot boxes and gacha, packs, random NFT and token prizes from a stash of NFTs the project holds, coin flips and prize pools, lotteries and raffles, auctions where the highest bid wins and the outbid get their money back, staking farms with lock tiers and NFT boosts, crafting and merging, seasons and emissions, prediction pools and contests, quests, airdrops and capped rewards, holder-gated drops, and perks or staking for any NFT, whether the project minted it or not. Turns a person's idea into a JSON spec, then checks, simulates and prices it, explains it in plain words, and hands the person the command that puts it live in their own wallet. Use whenever someone asks for anything onchain that players or members pay, earn, stake, open, craft or win, for a game, a community, a creator or a brand, and before writing any Anchor, Rust or Solidity for it.
4
+ ---
5
+
6
+ # Build an economy on loot
7
+
8
+ An economy is one JSON spec (`xsolana.json` in the project) on **loot**, a Solana program every project shares. Nobody
9
+ writes or deploys a contract: the spec is checked, simulated and priced by the `xsolana_*` tools, and the project's
10
+ owner puts it live by approving in their own wallet. It runs on **devnet** until the program is audited.
11
+
12
+ The tools are the `xsolana_*` MCP tools. Without MCP, the same from a terminal: `npx -y @xgamesfun/solana@0.24.0 <guide|examples|check|simulate|cost|explain|publish|status> [xsolana.json]`,
13
+ or `npx -y @xgamesfun/solana@0.24.0 tool <name> '<json>'` for any tool (always with the exact version).
14
+
15
+ ## Rules (always)
16
+
17
+ 1. **Never write, build or deploy a program.** No Anchor, Rust, Solidity or custom program, and no deploy: not by you,
18
+ not by a command or page you give the person, not even "just on devnet". Unaudited code holding people's money is
19
+ the risk loot exists to remove. If the idea needs something loot can't do, follow rule 9.
20
+ 2. **You never sign, and you never ask for or handle a key.** No private key, seed phrase or keypair file: not in
21
+ chat, not in a spec, not in a file. Going live is the person running `xsolana approve` and approving in their own
22
+ wallet. If they paste a key, tell them to treat it as exposed and move its funds; write it nowhere.
23
+ 3. **Devnet only.** Nothing here is real money yet; say so.
24
+ 4. **The checker decides.** Never save or publish a spec with refusals. Fix each refusal the way its `fix` says.
25
+ 5. **Numbers come from the tools.** What a box gives back, how long a pool lasts, what going live costs: quote
26
+ `xsolana_simulate`, `xsolana_cost` and `xsolana_explain`, never your own arithmetic. What a player pays is the tools'
27
+ "in all" number for that action (price, fee, oracle, network and rent that never comes back): quote it as is.
28
+ 6. **Flag the law.** When the checker raises `W-CHANCE-PRIZE` (paid chance at money: boxes with prizes, lotteries,
29
+ contests), `W-CHANCE-ITEMS` (paid random collectibles a player can resell: loot boxes, gacha) or `W-REVENUE-SHARE` (stakers paid from sales), tell the person in plain words, every time, and never
30
+ call anything an investment, a return or passive income. For an auction, always say `W-AUCTION-SHILL`: anyone,
31
+ the seller included, can bid to push the price up. Before they go live, say as well that the program is not
32
+ independently audited and comes as is, with no warranty: they use it at their own risk, and making sure the
33
+ economy is legal where it is offered is theirs. Never promise it is safe, and never give legal advice.
34
+ 7. **Take addresses from the tools, never from memory**: the program, its accounts, the IDL. Every answer names them.
35
+ 8. **Public data only in a spec**: names, prices, percents, public addresses. Leave out `id`, `index`, `shopBox`,
36
+ and `live`: they are written when it goes live.
37
+ 9. **Compose before you say no.** Most ideas are several parts working together, not one part. Before calling
38
+ anything a limit, break the idea into its pieces (what players pay, what they get, what is random, what the chain
39
+ can't see, what is held and for how long, who is paid) and find the part for each (`references/sentence-to-spec.md`).
40
+ 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
41
+ decider's key, a list. Build the combination and say which part does which piece. When the tools' words and the
42
+ checker disagree (a field the schema takes that a sentence somewhere calls impossible), the checker and
43
+ `xsolana_guide`'s parts decide: try it in a spec and run `xsolana_check`.
44
+ A piece is a limit only when no part or combination does it. Then:
45
+ - say in one line what is missing and why;
46
+ - offer the nearest versions (`xsolana_examples` with the words of that piece returns them), best first, each with
47
+ what it gives up and its "in all" cost; recommend one, and build it;
48
+ - if they push back, offer the next version or a change that brings back what they miss (a price, a bundle, a
49
+ second step). Don't withdraw a draft that checks, and don't send them elsewhere while a version on loot is on the
50
+ table.
51
+
52
+ 10. **People finish what follows; build the button, not a server.** Every step after a player's ask (settle a roll,
53
+ draw a lottery, pull its parts (each place, a refund, a share), refund a draw the oracle never answered, claim a
54
+ win, end an auction, run a sealed schedule, take a rental's NFT home) can be sent by the person who
55
+ gains from it, from the page (`app.claims()` lists them, `app.claim(c)` sends one). A
56
+ grant's claim is the winner's alone: the winning wallet signs it, whatever proves it won; anyone may pay its fees,
57
+ nobody claims for a player. loot needs no server to finish anything. Never make a key in a page or a server for
58
+ the owner to fund: whoever signs pays.
59
+
60
+ ## The loop
61
+
62
+ 1. **Read what an economy can be made of**: `xsolana_guide` (every part and field, who acts and what it costs, what
63
+ loot cannot do). `{"part": "farms"}` for one part's nested shapes.
64
+ 2. **Start from the nearest example**: `xsolana_examples {"query": "<the person's own words>"}`, then
65
+ `xsolana_examples {"name": "<example>"}` for a whole spec to copy. The rated economies say how each real economy
66
+ fits (great, some, a-lot, cannot) and what carries it.
67
+ 3. **Write the spec** from their sentence: `references/sentence-to-spec.md` maps their words to parts. Keep their
68
+ sentence in `prompt`. Add a `scenario` with the counts they expect (how many opens a day).
69
+ 4. **Check it**: `xsolana_check {"spec": …}`. Fix every refusal; every warning is something to tell the person, in a
70
+ sentence of its own.
71
+ 5. **Simulate and explain it**: `xsolana_simulate`, then `xsolana_explain`. Tell the person, in a few short lines, what each
72
+ play costs and is worth, where the money goes, how the pools fill and drain, what going live costs, what the
73
+ authority must sign while it runs, and the flags: `references/telling-the-person.md`. Before they approve, say who
74
+ sends each step while it runs (a draw, a payout, a contest's result) and what happens if nobody does:
75
+ `references/after-launch.md`.
76
+ Prizes: give a known prize directly on whatever route wins it (a grant, a lottery place, a contest, a mission, a
77
+ recipe): one step, no oracle. Use a roll of a box only for chance. A pack of cards is one box with `slots` (five
78
+ draws an open, a guaranteed rare), opened several at once from the page; cards whose traits differ are a fresh
79
+ item drawing from `tables`. Every other shape (bundles, an NFT as the price, stashes, gates, no-loss lotteries,
80
+ amounts by rank) is in `references/sentence-to-spec.md` and `xsolana_guide`.
81
+ Then say what stays the project's job (`references/telling-the-person.md`): the files behind every item's link
82
+ (every value of a fresh item's `{attribute}`), a table's rent, and the collectible rent a pack's settle pays.
83
+ 6. **Iterate on what they say.** "Make legendaries 0.5%", "a bigger seed": change, check, simulate, explain again.
84
+ 7. **Save it**: `xsolana_save {"spec": …}` writes `xsolana.json`, only if it checks. A live file keeps its program numbers.
85
+ 8. **When they say "put it live"**: `xsolana_publish` lists every step and its cost and gives the command. Tell them to
86
+ run it themselves in the project folder, exactly as `xsolana_publish` gives it (it names the exact version:
87
+ `npx -y @xgamesfun/solana@0.24.0 approve xsolana.json`; never drop the version). It opens a page on their computer where
88
+ they connect a devnet wallet and approve; what lands is written back into `xsolana.json`. You publish nothing.
89
+ 9. **After it is live**: `xsolana_status` shows each part on chain; read it before acting on what the person says
90
+ happened. A later change is the same loop: save, publish, and the approval sends only what changed. What only the
91
+ owner signs (open or decide a contest, pay out) goes on an owner page they approve in their own wallet:
92
+ `references/after-launch.md`.
93
+
94
+ ## Building the app around it
95
+
96
+ A game, a site or a bot calls the live economy with the player's own wallet, through the package's player module
97
+ (`@xgamesfun/solana/play`: buy, open a box and collect the roll, pay, stake, compound and unstake, craft, bid and end an
98
+ auction, list an NFT for sale or for rent and read who rents it, raffle an NFT, claim what a player is owed, run what
99
+ is due, list what a player owns). Pages call `claims()` when they open and show each as a button.
100
+ `references/apps.md` shows a whole page; `xsolana_client` gives the program and the calls. Never invent account layouts,
101
+ instruction data or addresses, and never put a key or the authority's actions in a page players load.
102
+ For staking an NFT of another collection, show what custody means and get explicit acceptance before sending.
103
+
104
+ ## More
105
+
106
+ - From a sentence to a spec, with a worked example: `references/sentence-to-spec.md`
107
+ - Telling the person what they get, and what stays the project's job: `references/telling-the-person.md`
108
+ - Building the app for a live economy: `references/apps.md`
109
+ - After it goes live (who sends each step, owner pages, results from a game, a results server): `references/after-launch.md`
@@ -0,0 +1,124 @@
1
+ # After it goes live: who does what
2
+
3
+ Going live is the start. Before the person approves, tell them who sends each step the economy needs while it runs,
4
+ and what happens if nobody does. Most failures in practice are a raffle nobody draws, a contest nobody decides, or a
5
+ winner nobody pays.
6
+
7
+ ## Who sends each step
8
+
9
+ | Step | Who can send it | If nobody does |
10
+ | --- | --- | --- |
11
+ | A box's roll and settle, a shop buy | the player, in their own purchase (`js/play` does both in one prompt) | the player, a key the player named for the order (`settler`) or the project's owner or authority settle a rolled order, a normal item too (the app's backend delivers it from the `OrderSettled` receipt of a finalized transaction whose `meta.err` is null, once per order: `loot.receipts(tx)` gives none for a transaction with an error, whose logs can still read as all-success; a truncated log stays truncated when read again, so it then goes by the order's account); a roll that failed or never came refunds (`app.refund`) |
12
+ | A lottery draw, then each part pulled | **anyone**: the page (`app.claims()` lists each available step), or a sweep. The draw's ask fixes the round's money (what its places take of the pot, each pool's prize) and opens the next round at once. A pull pays one part into its owner's account, opened in the same transaction when it is not there (the sender pays its rent): an entry's own (a refund, a ticket back), its places' part of the pot or their prizes (a raffled NFT, an item, tokens, a roll), or a part owed in common (a share, what the pot gets back) | selling stops at draw time. A round nobody draws waits, with no deadline. Once drawn, every part waits in the round for its own pull, in any order, holding up nobody else; the pull that leaves nothing owed closes the round, and the rent of its record goes back to whoever fronted it (the lottery's `roundRent`, or its first buyer). An NFT that can't move is made a hold for its winner. If the oracle never answers a draw (7 days after it was first asked for), the next ask cancels it, and every buyer's refund is pulled; the page pulls the sender's own refund in the cancel's own transaction. A due round's draw rides in front of the next ticket purchase. An empty round moves on with the next ticket. A one-time lottery ends after its round |
13
+ | A schedule that is due (an emission, rent, a drip) | the project's owner or authority, or who it pays (a wallet, the treasury): `app.runSchedule` signed by one of them, or their sweep (a sealed burn: anyone) | **a run missed is missed**: one sent late pays once, for the latest run due, and the runs before it lapse, so keep a key of the project's sending it on time. Only a sealed schedule's fixed amount pays every run due (a backlog of years takes a few calls) |
14
+ | Ending an auction | **anyone**: usually the winner, from the page | it waits: whenever it is ended, the lot goes to the highest bidder. A seller whose account can't take the money then doesn't hold up the winner: the lot goes, the auction stays open owing it (`app.auctions()`: `delivered`, `proceedsOwed`), and the next end, anyone's, pays and closes it |
15
+ | Catching a farm up (`walk_farm`), when it has passed more of its seats' end times than a move can walk | **anyone**, for free: `app.claims()` lists it, `app.walkFarm(key)` sends it in parts (`app.farmWalk(key)` says how many) | moves of that farm fail until it is walked; nothing is lost by the wait: what each seat earned by its end is exact |
16
+ | A contest's entries back when it is off, or a winner's part of a contest with no prize or picks | **anyone** (each entry's money goes only to its player); a winner's first claim with a prize or picks: the **winner or the project's keys**, and past 30 days the project's keys may pick for them | they wait; nothing is lost |
17
+ | Opening a contest | the **authority** (the owner's wallet by default) | nothing happens |
18
+ | Naming its winner, calling it off | the **authority**, or **anyone** sending a verdict one of its `deciders` signed (a results server's key: `decideIx` with `verdict`, after the Ed25519 check of `verdictBytes`); a winner only once entries close, a call-off any time | after `resultDue` it is off by the clock and everyone claims back; a crowdfund short of its `minPot` when entries close is off at once |
19
+ | Moving tokens out of a pool, minting, a `decisions` payout, rewriting a collectible | the **authority** | nothing happens |
20
+ | A grant claim | the **winner** only, from the page: the winning wallet signs every claim, whatever proves it (a voucher, the list, an open grant); anyone may pay its fees, nobody claims for it | the prize waits for them |
21
+ | A prize given directly (a grant's, a lottery place's, a contest's, a mission's, a recipe's: an item, tokens, an NFT of a stash) | the winner's own claim (a grant's: the winner signs it), the place's payment, the winning entry's claim, the unstake or the craft: it is given in that transaction, the sender paying what it opens (a collectible's rent, the winner's token account) | a grant's claim or a craft of a prize it can't give is refused; a lottery's place, a contest's claim or a mission's end goes on without it (an NFT already given, an item sold out or switched off, a pool short pays what it holds) |
22
+ | A prize that is a roll of a box (a grant's, a lottery's place, a contest's winners, an auction's mystery lot, a mission's chest) | the winner's own claim (a grant's), the place's payment or the auction's end makes the winner's order (its rent goes back to the winner at settle); then the **winner or the project's keys** ask the oracle for the roll (`loot.askRollIx`, its fee the asker's) and the winner, a key the winner named, or the project's keys settle it (`loot.settleIx`), which hands over what it rolled, a stash's NFT included; `app.claims()` lists them | it waits, and is never refunded while it waits; asked and never answered, they ask again. What the box gives that has run out (an empty stash, a sold-out item) can't be won: the roll lands on its other outcomes, and a fixed prize its pool runs short of pays what the pool holds. With nothing at all in stock, only the winner asks, and that ends the roll with nothing (an auction's mystery lot: the winning bid refunded); a roll whose answer finds nothing at all waits again, for a restock |
23
+ | Putting more NFTs in a stash, or taking one out | the **owner** (or the authority), holding them: add them to the stash's `nfts` and publish again (only the new ones go in), or `loot.stashIx` with `out` to any wallet | a stash nobody refills just runs out |
24
+
25
+ "Anyone" means the page can offer a button for it, and any player can press it. Build those buttons with `js/play`:
26
+ `app.claims()` lists what this player can finish now (a draw, its cancellation, a pull, a payout, a refund, an
27
+ auction to end), and `app.claim(c)` sends one. A **sweep** is a server that sends overdue public steps for the
28
+ projects it watches. One may or may not be running for the person's project, so never rely on it: the page's buttons
29
+ are what finish them. A sweep needs only its own fee payer for draws, refunds and returns; to settle or run a schedule, it needs a project key or a key the player named.
30
+
31
+ A round of a lottery is a record of its own, opened by its first purchase (the SOL the lottery holds for it,
32
+ `roundRent`, put in at setup by default and sized from the lottery's expected rounds and entries, pays its header, its
33
+ token accounts and each entry's bytes; with too little, its first buyer fronts the header and accounts, about 0.004 SOL
34
+ and up, and each buyer their entry; all back when the round closes, to whoever paid), and it holds its ticket money in
35
+ accounts of its own until each part is pulled: shares in common and burns run after the draw, and the pot holds only
36
+ what nobody is owed. Each purchase adds an entry: funded by `roundRent`, the buyer pays only the fee and the price; a
37
+ buyer who pays its bytes themselves (about 0.0002 SOL) never gets them back (they go to the treasury when the round
38
+ closes). A share the
39
+ buyer chooses (a referrer) is paid as they buy. Cancelling (only for a draw the oracle never answered, or a round short of its least sale) owes every entry its
40
+ whole ticket payment back, with no split or burn; network and oracle fees stay spent. Prizes and outside donations
41
+ already in the pot stay there for the lottery; they are not ticket refunds. A round holds about 70,000 to 260,000
42
+ purchases, by how wide its entries are.
43
+
44
+ Every account a payment goes to is opened by the transaction that pays it, and whoever sends it pays the rent: the
45
+ treasury's or a payee's account at a settle, a winner's at a payout, an outbid bidder's at the next bid. Nothing waits
46
+ on an account, and closing one (an unwrap of wrapped SOL) never stops a sale.
47
+
48
+ An NFT the project has no authority over is used as it is, with no import step, whatever its kind: Core, Token Metadata
49
+ (legacy, pNFT) or compressed (Bubblegum v2). Any of them can be staked, held, auctioned by the owner or authority,
50
+ and shown to gates and odds (a player auctions only an NFT of the project's own collection);
51
+ a compressed one is proven in the same transaction (`prove_leaf`, which the page does by itself from the RPC's DAS
52
+ methods). What the project writes about one (a level, a use) goes in its Sheet (`["sheet", project, nft]`), written by
53
+ the authority with `collectible` `Set`, by a farm's growth, or by anyone proving the traits the project vouched for
54
+ (`app.proveTraits`).
55
+
56
+ A new mission is a new farm: add it to the spec and publish again; `runs` ends it that long after it goes live. Near the end of a farm that shares what lands, fund it at least its release span (its longest lock, a day at least) before the end: what is not released by the end, and what a seat forfeits after it, stays in its rewards vault, where the authority may take back what is free (`loot.moveTokensIx` with `from: { Farm: [id] }`), never what the farm owes its seats. No seat needs a refresh at its end. If rewards cannot move, unstaking can explicitly forfeit them.
57
+
58
+ Changing a live box's roll (its outcomes, odds, `stockOdds`, `whenOut`, `rollsOn`, `then`) fails a purchase still waiting for its
59
+ roll, and an auction's roll lot, each of which gets its payment back at once (`W-LIVE-ROLLS`). Any other roll waiting (a
60
+ grant's, a mission's chest, a lottery's place, a recipe's) holds no payment and rolls on the box as edited: nothing spent is
61
+ lost. A roll asked and left unanswered 7 days can be refunded when it holds a payment or an NFT (its refund names the
62
+ box, whose count of draws waiting it leaves: `app.refund` does). A box that stops selling when an outcome runs out (the
63
+ default) sells again once the project restocks it (more units, tokens into its pool, NFTs into its stash) or edits it. Its price, rise, fall, split, switch, window and gate change freely. A set's way out (the recipe that unbinds it)
64
+ should take no `pay` line (`W-UNBIND-PAY`): a recipe with one waits while the project is paused or the platform
65
+ suspends it, and the pieces stay locked in their farm meanwhile.
66
+
67
+ Leaving a farm needs only the player's wallet, never a server's signature; what can still keep an NFT from coming
68
+ back is the NFT issuer's own controls (`telling-the-person.md`, NFT custody).
69
+
70
+ ## The owner's own steps: an owner page
71
+
72
+ What only the authority can sign (open or decide a contest, pay out, move a pool) needs a page the owner opens and
73
+ approves in **their own wallet**, like the approve page. Build it with `@xgamesfun/solana/client` builders
74
+ (`openContestIx`, `decideIx`, `moveTokensIx`) and the wallet's `signTransaction`, served locally or on the same site
75
+ behind nothing more than the owner's wallet: the program checks the signature, so a page holds no secret.
76
+
77
+ - Show what each button does in words before the wallet asks ("Name the Owls the winner: the pot of 0.42 SOL is
78
+ shared by 7 backers; 5% goes to you").
79
+ - Never put an owner's or authority's key in a page, a file or a server, and never ask the person to export one.
80
+
81
+ ## Results from a game: who won, and paying them
82
+
83
+ When a game (not the chain) knows who won a match, cleared a quest or placed in a tournament, pay through a **grant**
84
+ the winner claims themselves. The grant's limits (per wallet in all and per window, per person, per key, per day, in
85
+ total, between two dates) are the most anything can ever pay out, whatever happens to a server.
86
+
87
+ - **A list** (an airdrop, a known set of winners): the owner writes the wallets in the grant's `list`; no server.
88
+ - **Promo codes, claim links**: the grant's `keys` are the public keys of one keypair per code (made offline from each
89
+ code; only public keys go in the spec), with `limits.perKey: 1`. The player types the code; the page derives its
90
+ keypair, signs a voucher for the player's own wallet, and claims (`app.claimGrant(key, { voucher })`). A code pays
91
+ once in all; a signature someone copies pays only the wallet it names.
92
+ - **Show a badge, claim once**: an open grant (no `vouchers`, `keys` or `list`) with a `gate` and `limits.perWallet`:
93
+ any wallet that shows the collectible claims, and each collectible shown counts too. No server.
94
+ - **Vouchers from a results server**: the grant names the server's public key in `vouchers`; the server checks the
95
+ game's result and signs a voucher (for a grant of tokens, with each winner's own `amount`, up to the grant's: rewards
96
+ by rank); the winner's page claims with it (`app.claimGrant(key, { voucher })`).
97
+ - The server can be the person's own game server (`signVoucher` from `@xgamesfun/solana/results`). It holds only that
98
+ voucher key, never the owner's. Set the grant's `total` and `perWallet`: they bound what a stolen voucher key can
99
+ pay.
100
+ - Or a **results server** the person runs or names. It checks results signed by the game's host key, which the
101
+ owner certifies once by signing a text in their wallet; the game server and pages talk to it with
102
+ `@xgamesfun/solana/results` (`report`, `giveaways`, `draws`), and winners claim what it vouched for with
103
+ `app.claimWins({ server, token })` (`token`: the player's sign-in with that server, when it asks for one; a drop
104
+ of NFTs from a stash gives one only at a signed-in claim, first come first served). Its voucher key is per
105
+ project (`GET <server>/v1/voucher-key?game=<address>`): the project's address exists only once it is live, so go
106
+ live first, then add the grant with that key and publish again. A grant written before then is saved switched
107
+ off (`"active": false`, which `xsolana_check` reports as W-OFF): tell the person it waits for that key, and switch it
108
+ on when the key goes in. Never use a key from `/v1/health`.
109
+ - **A random draw nobody can rig** (a free draw for members): a results server's computed draws (drand), or a lottery.
110
+
111
+ Per wallet per day ("2 wins a day each") uses `limits.perWalletEach: { "count": 2, "every": "1d" }`;
112
+ `perDay` separately counts all wallets together. A grant keeps every round it paid each wallet within one incarnation,
113
+ 16 bytes a round (from the grant's funds), and each wallet's count in 28: one voucher may name several rounds, each
114
+ paid once, and the same voucher sent again later pays what a limit held back. Changing limits or voucher keys does not
115
+ clear its history. Closing and recreating a grant starts a new campaign with new records and invalidates old vouchers;
116
+ anyone may then close the old records (`loot.closeShardIx`), their rent to the owner. A grant for more than about
117
+ 100,000 wallet-rounds names `wallets` (and `rounds`) so its records are kept in enough lists. A list written in xsolana.json pays each wallet once per grant incarnation: editing it lets newly added wallets
118
+ claim, never a wallet already paid (for repeated payouts, list the wallet again under a new `round`, or use vouchers). A list leaf names no grant or incarnation, so a grant closed and made again with the same list pays every listed round again: give a re-made grant a fresh list (new rounds); the approval page warns when a change does that. A results server must read the current grant incarnation when accepting
119
+ a result and include it in the voucher it signs.
120
+
121
+ ## Before you act on what the person says happened
122
+
123
+ "It's been an hour", "the Owls won", "the pool is empty": read the chain first (`xsolana_status`, or the account itself), and
124
+ say what you found. The chain's clock and balances decide what can be sent now.
@@ -0,0 +1,193 @@
1
+ # Building the app for a live economy
2
+
3
+ Once `xsolana.json` has a `live` section, players use the economy through a page (a game screen, a site, a mint page)
4
+ with their own wallet. Use the package's player module, `@xgamesfun/solana/play`: it reads the economy, builds every
5
+ transaction, has the player's wallet sign it, sends it and waits for the result. Don't hand-write instructions,
6
+ account lists or program addresses.
7
+
8
+ ```bash
9
+ npm install @xgamesfun/solana
10
+ ```
11
+
12
+ ```js
13
+ import { solanaApp, walletStandardSigner } from '@xgamesfun/solana/play';
14
+
15
+ const spec = await (await fetch('./xsolana.json')).json(); // the live spec, served with the page
16
+
17
+ // Connect a wallet (Phantom, Solflare, Backpack all register through the Wallet Standard).
18
+ const wallets = [];
19
+ const api = { register: (...w) => { wallets.push(...w); return () => {}; } };
20
+ window.addEventListener('wallet-standard:register-wallet', ({ detail }) => detail(api));
21
+ window.dispatchEvent(Object.assign(new Event('wallet-standard:app-ready'), { detail: api }));
22
+ const wallet = wallets.find((w) => w.chains.some((c) => c.startsWith('solana:')));
23
+ const { accounts: [account] } = await wallet.features['standard:connect'].connect();
24
+
25
+ const app = solanaApp({ spec, rpcUrl: 'https://api.devnet.solana.com', wallet: walletStandardSigner(wallet, account) });
26
+
27
+ const shop = await app.shop(); // [{ key, name, price, priceUnits, currency, active, left }]
28
+ await app.buy('badge', { listing: shop.find((l) => l.key === 'badge') }); // the price shown is the most it pays
29
+ const boxes = await app.boxes(); // each box's price, outcome chances and whenOut (show it: stops selling, or draws among what is left)
30
+ await app.openBox('chest', { listing: boxes.find((l) => l.key === 'chest') }); // pays, waits for the roll, settles
31
+ await app.pay('entry'); // a payment of the spec
32
+ const lotteries = await app.lotteries(); // [{ key, name, price, currency, status, round, sold, drawsAt, nftTicket, … }]
33
+ await app.buyTickets('weekly', 2, { listing: lotteries.find((l) => l.key === 'weekly') }); // an NFT ticket: { nft: asset, listing }
34
+ await app.payNft(asset, { to, reference }); // pay with an NFT (burned, or given to `to`): admit the player by the receipt
35
+ await app.stake('farm', '10', 0); // tokens, at lock tier 0; app.stakeNft('farm', asset) for a collectible
36
+ await app.harvest('farm'); // rewards; app.unstake('farm', '10') once the lock has run
37
+ await app.staked('farm'); // the player's seats: [{ asset, amount, freeAt, boostPercent, waiting, rewards }]
38
+ await app.refresh('guild', asset); // a farm with growth: writes the NFT's points and level now (anyone may)
39
+ const recipes = await app.recipes(); // [{ key, name, active, version }]
40
+ await app.craft('merge', [assetA, assetB], 1, { listing: recipes.find((r) => r.key === 'merge') }); // a changed recipe is refused
41
+ await app.auctions(); // each auction: high, nextBid, leading, secondsLeft, open, over, waiting (SOL)
42
+ await app.bid('comet', '0.6'); // the bid it tops goes back to its bidder in the same transaction
43
+ await app.endAuction('comet'); // once it is over, anyone: the lot to the winner { winner, asset?, rolled? }
44
+ await app.unwrap(); // SOL back from a bid that was topped, from wrapped SOL to plain SOL
45
+ await app.claimGrant('welcome'); // a wallet on the grant's list (or { voucher } from your server)
46
+ await app.claimWins({ server, token }); // every prize a results server vouched for or listed, claimed by the winner; [{ round, signature | error }]
47
+ // (`token`: the player's sign-in with that server, when it asks for one: a stash drop's NFT is given only at a signed-in claim)
48
+ const loan = await app.loanOffer(lender, 1); // a lender's offer: { amount, repay, lasts, risks }: show every risk's `says`
49
+ await app.borrow(lender, 1, asset, { acceptRisks: true }); // only once the player saw the risks (refused without it)
50
+ for (const c of await app.claims()) await app.claim(c); // what this player can finish: a roll to collect, a refund,
51
+ // a contest payout, a due draw, an auction to end
52
+ await app.runSchedule('drip'); // a schedule that is due (the project's keys or who it pays); null when not yet. A run missed is missed: only a sealed fixed amount pays every run due
53
+ await app.farmWalk('guild'); // { ends, needsWalk, parts }: end times the farm has passed; a move refuses (FarmNeedsWalk) past what it can walk
54
+ await app.walkFarm('guild'); // walks it in parts (anyone, free); claims() lists it when it is needed
55
+ await app.collectibles(); // what the player owns: [{ asset, item, name, uri, attributes }] (`item`: its key)
56
+ await app.balances(); // what they hold of each of the economy's tokens
57
+ ```
58
+
59
+ - **Serving it**: a plain `index.html` with a `<script type="module">` can't import a bare `@xgamesfun/solana/play` by
60
+ itself. Either use a bundler (Vite: `npm create vite@latest`, then `npm install @xgamesfun/solana`), or put this import
61
+ map before the module script and serve the folder (`npx serve .`):
62
+ ```html
63
+ <script type="importmap">{ "imports": {
64
+ "@xgamesfun/solana/play": "./node_modules/@xgamesfun/solana/js/play/index.js",
65
+ "@xgamesfun/solana/client": "./node_modules/@xgamesfun/solana/js/client/index.js"
66
+ } }</script>
67
+ ```
68
+ Tell the person the one command that starts it, and that it serves on devnet.
69
+ - **What a roll looks like**: a box of several outcomes is rolled by the oracle a few seconds after the open; `openBox`
70
+ waits for it. Show the player that it is rolling.
71
+ - **A page that closes before the settle**: the SOL its settle key holds is not lost in the same browser (on another device, with nothing stored, it can't be found yet). Build the wallet with
72
+ `walletStandardSigner(wallet, account)`: with a wallet that signs messages, the page asks once a session for a
73
+ signature over a fixed text ("loot: the keys that settle your orders…"; it moves nothing), and every settle key comes
74
+ from it, so a reload makes it again. With one that signs no messages, the key is kept in the page's storage,
75
+ encrypted. Call `app.recover()` when the page opens (after the wallet connects): it settles what rolled, sends back
76
+ what a key holds for an order that is gone, and leaves one still rolling for later. Say so to the player in a line
77
+ ("Finishing an earlier purchase…").
78
+ - **Packs**: `app.openBox(key, { listing, count: 10 })` opens ten packs in one purchase: one wallet prompt, one oracle
79
+ answer, `maxPrice` a pack's. It returns `won` (every card) and `assets`; a pack too big for one transaction is
80
+ settled in parts, each its own transaction (`parts`: their signatures), sent by the throwaway key the purchase funds (`oneSignature`, the default),
81
+ so the player signs once. More packs than one oracle answer draws (or than keep an order's rent and fee within 0.01
82
+ SOL: `loot.opensAnOrder(box)`) are several orders in that one purchase (`orders`). A pack of fixed cards (one outcome, nothing random) is bought in one step: its cards come
83
+ in the purchase, no oracle and no settle (one too big for a transaction is opened instead).
84
+ - **A roll a route makes** (a craft, a bundle bought, a grant claimed, an auction won): the page asks for it in the
85
+ route's own transaction and settles it after with a throwaway key, so the player signs once. When the rolled box has
86
+ nothing in stock, nothing is sent and the call rejects with SoldOut: tell the player to try again later. A compressed card's id is not in `assets` (it is the tree's count when minted): read it
87
+ from `claims()` or the player's assets. Show each card as it lands.
88
+ - **What an auction looks like**: show the high bid, the least next bid (`nextBid`) and a countdown (`secondsLeft`;
89
+ a late bid moves the end, so read `auctions()` again after every bid). A player who was outbid has their bid back
90
+ already, nothing to claim: in SOL it waits in their wrapped-SOL account (`waiting`), so say "You were outbid:
91
+ 0.5 SOL is back in your wallet" and offer `unwrap()` to turn it into plain SOL (any later purchase does it too).
92
+ When `over` is true, show an "End the auction" button to everyone (the winner usually presses it; it mints or
93
+ hands over the lot). There is no deadline: however late it is ended, the lot goes to the highest bidder. An auction with
94
+ `delivered` has given its lot, and still owes its seller, whose account couldn't take the money (`proceedsOwed`): keep it
95
+ on the seller's page until an end (`app.endAuction`, anyone's) pays and closes it. Don't treat it as closed at its end.
96
+ - **NFTs the project has no authority over**: a gate or odds that name a collection find the player's Core NFTs of it
97
+ themselves; for a Token Metadata NFT pass `shown: { mint, owner }`, for a compressed one `shown: { compressed:
98
+ assetId, tree }` after a `loot.proveLeafIx` in the same transaction. `app.stakeNft`, `app.unstakeNft`, `app.refresh`,
99
+ `app.harvest`, `app.hold` and `app.endAuction` take any NFT: a Core asset, a Token Metadata mint (legacy or pNFT),
100
+ or a compressed NFT's asset id; the page finds its kind itself, and proves a compressed one first (its RPC must
101
+ offer DAS: `getAsset`, `getAssetProof`). In a farm that watches, `app.harvest` shows the NFT is still the player's,
102
+ and `app.stakeNft` of an NFT someone else staked and sold takes their seat over. What the project wrote about one is in
103
+ its Sheet (`loot.decodeSheet`).
104
+ - **Off-chain traits** (the spec's `traits`): the project builds the tree with `loot.traitsTree([{ nft, attributes
105
+ }])` from its NFTs' metadata and publishes each NFT's `attributes` and `proof` with the page; a player (or the page,
106
+ before a stake) calls `app.proveTraits(nft, attributes, proof)` once per NFT, and its boosts and gates read them.
107
+ - **Rolls on an NFT**: `app.openBox(boxKey, { on: asset })` for a box that rolls on one (an enchant). Leaving a
108
+ mission that rolls, `app.unstakeNft` returns `{ signature, roll, rolled }`; a recipe that rolls, `app.craft` returns
109
+ `roll` and `rolled`; a grant that gives a roll, `app.claimGrant` returns `{ signature, rolls }`; the winner ending an
110
+ auction of a roll, `app.endAuction` returns `rolled`. Each asks the oracle in the player's one transaction, and a
111
+ throwaway key that transaction funds settles it: one prompt. `rolled` (and each of `rolls`) is what the roll gave
112
+ (`won`, each prize it set aside, and `order.fate`: the numbers moved and where the NFT went). When the step and its
113
+ ask can't go together, the roll waits: `app.collect(roll)` asks for it, waits and settles it, and a page should offer
114
+ to collect any roll of the player's still waiting.
115
+ - **What a staking page shows**: `app.staked(farmKey)` lists the player's seats in a farm, each with what it is owed
116
+ now (`waiting`, in `rewards`), the program's own sum from a dry run of the seat (nothing signed or sent), and
117
+ when its lock is over (`freeAt`). Show that; don't work out rewards from the rate yourself (a sharing farm splits
118
+ its pool by weight, and an upkeep or the farm's end stops a seat). Read it when the page opens and after a stake
119
+ or a harvest.
120
+ - **Getting out of a farm**: after the farm lock ends, `app.unstakeNft(farmKey, asset)` releases the collectible;
121
+ `app.unstake(farmKey, amount)` returns tokens. If reward transfers are blocked, offer an explicit choice to give
122
+ up rewards with `{ forfeit: true }` as the last argument. Remove a token seat's booster before taking out all
123
+ its tokens. An early exit (a tier's `exitFee`) keeps the rewards the seat was credited; what is taken out leaves at
124
+ once (no unbonding wait). Forfeiting does not bypass the lock or the token's or NFT issuer's own controls: say which custody
125
+ the farm uses and never promise an unconditional exit (`telling-the-person.md`, NFT custody).
126
+ - **Farm token hooks and Core adapters**: `stake`, `unstake`, `stakeNft`, `unstakeNft`, and `harvest` accept
127
+ `extra` account metas or `transfers` keyed by mint in their options. Static Token-2022 hooks and required memos
128
+ resolve automatically. For dynamic hooks, provide every actual `{ source, destination, authority, amount }`
129
+ transfer: the farm's rewards vault to the player when the action pays (crediting moves no tokens); token principal
130
+ too when it moves. Do not guess amounts from the vault's balance. Without that plan the app fails before signing; refresh the plan if balances or fee terms change.
131
+ Explicit `extra` replaces automatic resolution and must include all hook, memo and Core adapter accounts.
132
+ For example, `app.unstakeNft('heroes', asset, { transfers: { [rewardsMint]: rewardTransfers } })`, or
133
+ `app.unstakeNft('heroes', asset, { extra: adapterAndTokenAccounts })`. Harvest takes options third:
134
+ `app.harvest('heroes', asset, { transfers: { [rewardsMint]: rewardTransfers } })`.
135
+ A forfeit exit skips reward-mint reads and reward ATA creation; principal transfers still need their own mint.
136
+ - **Lottery steps**: `claims()` lists a due draw (`reveal`), what this player is owed (`pull`), paying everyone else
137
+ and the shares owed in common (`payout`), and for a draw the oracle never answered, asking again or, after its
138
+ timeout, the cancel-and-refund (`cancel-draw`; `after-launch.md` says how the money moves). A draw that cancels the
139
+ round (that one, or a `reveal` of a round short of its least sale) pulls the sender's own refund in the same
140
+ transaction. A ticket bought when the round's draw is due sends the draw first, in the same prompt, and buys into
141
+ the next round (the buyer pays the oracle's 0.0008 SOL for it). Show each as a button
142
+ and read the list again after each one. The parts are pulled in any order: nobody waits on anyone else.
143
+ - **Gates**: a gated buy, ticket, stake, entry, bid or claim finds the player's collectible that passes the gate by
144
+ itself ("You need a Guild Pass to buy this" when there is none). Use `collectibles()` and its `item` key to show
145
+ what a player holds; never match NFTs by name. A list gate (`{ "list": [...] }`) takes the player's proof: the
146
+ page makes it from the wallets xsolana.json lists (`loot.gateList(list).proofOf(wallet)`, which fills the list to a
147
+ power of two as the plan's root does), or takes it from a server that keeps a long list. A stake gate
148
+ (`{ "staked": … }`) needs nothing from the page: js/play passes the player's seat and the farm, and says plainly when
149
+ the stake is too small or its lock is over (show the farm's locked tiers next to the gate).
150
+ - **A no-loss lottery** (`noLoss`): once the round is drawn every ticket is owed back to its buyer (a pull, which
151
+ anyone may send, so say "your tickets come back after the draw"), and the winners then collect the pot.
152
+ - **A lottery round** finishes in a few steps whatever its size: `app.claims()` lists the draw (`reveal`, whose ask
153
+ fixes the round's money and opens the next round at once), what this player is owed (`pull`: a refund, a place's
154
+ part of the pot, its prize; a raffled NFT; a roll asked for in the same transaction and settled in the same prompt)
155
+ and paying everyone else and the parts owed in common (`payout`); `app.claim(c)` sends one. The last pull closes
156
+ the round.
157
+ - **Repaying a loan** (the player's own, or a friend's): `app.loan(asset, due)` reads it; show its `repay` (base units
158
+ of its `mint`), then `app.repay(asset, due, { repay })` pays exactly that, in the payer's one prompt (a loan whose
159
+ debt changed since is refused); a throwaway key that transaction funds sends the NFT home to its borrower, a pNFT's
160
+ or a compressed NFT's too (one prompt). A lender sees an overdue unpaid loan in `claims()` (`release`, to them).
161
+ - **A grant that pays by rank**: a voucher with an `amount` (base units of the grant's token) pays that much, up to
162
+ the grant's; `app.claimGrant(key, { voucher })` passes it, and `{ proof, round, personHash, amount }` for a list.
163
+ - **The price the player saw is the most they pay**: `app.buy` and `app.openBox` need the listing `shop()` or
164
+ `boxes()` showed (`{ listing }`, or `{ maxPrice }` in base units), `app.craft` the recipe's listing from `recipes()`
165
+ (or its `version`), `app.buyTickets` the lottery's listing from `lotteries()` (or `{ maxPrice }` a ticket), and
166
+ `app.buyListing` and `app.rentListing` the `listing` that `app.listing(asset)` or `app.listings()` showed. Without
167
+ one they refuse; a price (or recipe) that moved since is refused with the new one in words, never paid unseen. Show
168
+ the new price and let the player click again.
169
+ - **Loans against an NFT** (a lender's offer with `deal: { Lend }`): show `app.loanOffer(maker, id)` first: what is
170
+ lent, what repays it, by when, and every `risks[i].says`. A loan's token may have a freeze authority, a pause or a
171
+ transfer hook (or a permanent delegate): whoever holds one can make every repayment fail, and a lender who holds one
172
+ can take the NFT when it is due. The program takes such tokens, so `app.borrow` refuses one unless the player
173
+ accepted it (`{ acceptRisks: true }`). Lend in a plain token (SOL, or a mint with no freeze authority) to avoid it.
174
+ - **An NFT as the price** (a recipe's `trade` line, a box's `rollsOn` with `then`, a lottery's `ticketNft`): say on the
175
+ page which NFT is spent and where it goes before the player signs; it is spent at once and never refunded.
176
+ - **The owner's own wallet on the page**: by default the project's treasury is the owner's wallet. The owner may buy, play
177
+ and unwrap like anyone: a sale whose treasury account was closed opens it again.
178
+ - **A buyer's choice**: a share the box, lottery or payment lets the buyer name (`"chosen"`, a referrer; `"chosen:<item>"`, the
179
+ workshop they craft at, not in a payment) comes from the page: `app.buy(key, { listing, chosenRecipient: referrer })`, `app.openBox(key, { listing, chosenRecipient })`,
180
+ `app.buyTickets(key, n, { listing, chosenRecipient })`, `app.pay(key, { chosenRecipient })`. Take a referrer from the link (`?ref=<wallet>`); never let a buyer name
181
+ themselves (the program refuses it, and `app.pay` does). Each method refuses an option it doesn't take (`{ choice }` is
182
+ not `chosenRecipient`). A `"chosen:<item>"` share needs its collectible: offer only ones players hold
183
+ (the program refuses none, one burned, and one the project holds: staked in escrow, held, up for auction).
184
+ - **A "Claim" or "Collect" area** built on `claims()` is how players finish what is theirs, and how a due draw gets
185
+ sent. Show it. Read it when the page opens and on a Refresh button, not after every action: it searches the chain
186
+ several times, and devnet's free RPC turns away bursts. After an action, read only what it changed (a balance,
187
+ `collectibles()`), one read at a time, and keep what is on screen when one read fails.
188
+ - **Errors** come back as plain sentences (a sold-out item, a gate not met, a price above the most they agreed to):
189
+ show them.
190
+ - **Never in a page**: a private key, a keyed RPC link, or anything the authority signs (payouts, minting, rewriting
191
+ a collectible). Those are the owner's or the authority's to sign in their own wallet, or on a server they run.
192
+ - **Lower level**: `@xgamesfun/solana/client` has one builder per instruction and the account decoders, for what the
193
+ player module doesn't cover.