@xgamesfun/solana 0.20.1 → 0.25.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 (112) hide show
  1. package/README.md +95 -52
  2. package/bin/xsolana.mjs +23 -8
  3. package/examples/blind-forge.json +2 -4
  4. package/examples/coin-flip-arena.json +4 -12
  5. package/examples/duel-arena.json +1 -4
  6. package/examples/elements-set.json +2 -9
  7. package/examples/expedition-guild.json +1 -8
  8. package/examples/fair-presale.json +1 -3
  9. package/examples/guild-missions.json +3 -10
  10. package/examples/halving-seasons.json +4 -9
  11. package/examples/hero-trials.json +15 -6
  12. package/examples/holder-den.json +38 -0
  13. package/examples/holders-drop.json +105 -0
  14. package/examples/ideas.json +788 -788
  15. package/examples/match-predictions.json +24 -0
  16. package/examples/merge-2048.json +5 -5
  17. package/examples/nft-raffle.json +1 -0
  18. package/examples/pack-league.json +108 -0
  19. package/examples/paper-hands-farm.json +4 -16
  20. package/examples/partner-vault.json +20 -7
  21. package/examples/sacrifice-raffle.json +2 -10
  22. package/examples/shelter-fair.json +1 -0
  23. package/examples/stakers-draw.json +4 -15
  24. package/examples/star-fleet.json +1 -8
  25. package/examples/starforce.json +6 -6
  26. package/examples/stride-sneakers.json +164 -0
  27. package/examples/trade-in-shop.json +2 -3
  28. package/js/approve/details.mjs +122 -52
  29. package/js/approve/ed25519.mjs +5 -1
  30. package/js/approve/live-check.mjs +83 -26
  31. package/js/approve/page.html +217 -78
  32. package/js/approve/page.js +43 -15
  33. package/js/approve/server.mjs +290 -73
  34. package/js/chain/base58.js +7 -1
  35. package/js/chain/borsh.js +24 -11
  36. package/js/chain/bytes.js +11 -8
  37. package/js/chain/core.js +35 -7
  38. package/js/chain/curve.js +1 -1
  39. package/js/chain/lookup.js +124 -36
  40. package/js/chain/mint.js +6 -2
  41. package/js/chain/pda.js +2 -2
  42. package/js/chain/programs.js +6 -6
  43. package/js/chain/rpc.js +44 -16
  44. package/js/chain/token.js +13 -2
  45. package/js/chain/token2022.js +55 -16
  46. package/js/chain/tx.js +32 -22
  47. package/js/client/accounts.js +653 -131
  48. package/js/client/compressed.js +4 -3
  49. package/js/client/grant.js +267 -83
  50. package/js/client/idl/loot.js +1 -1
  51. package/js/client/idl/loot.json +2406 -2176
  52. package/js/client/index.js +1 -0
  53. package/js/client/ix.js +3059 -664
  54. package/js/client/pda.js +39 -33
  55. package/js/client/policy.js +52 -24
  56. package/js/client/random.js +70 -0
  57. package/js/client/sizes.js +414 -16
  58. package/js/client/traits.js +14 -5
  59. package/js/connect/connect.mjs +59 -17
  60. package/js/economy/check.mjs +4153 -725
  61. package/js/economy/cost.mjs +403 -64
  62. package/js/economy/examples.mjs +176 -35
  63. package/js/economy/explain.mjs +466 -111
  64. package/js/economy/guide.mjs +296 -72
  65. package/js/economy/legal.mjs +12 -6
  66. package/js/economy/live.mjs +83 -19
  67. package/js/economy/model.mjs +462 -124
  68. package/js/economy/plan.mjs +1306 -219
  69. package/js/economy/schema.json +390 -484
  70. package/js/economy/secrets.mjs +17 -7
  71. package/js/economy/simulate.mjs +883 -223
  72. package/js/economy/txsize.mjs +15 -3
  73. package/js/economy/validate.mjs +61 -24
  74. package/js/mcp/server.mjs +42 -14
  75. package/js/mcp/tools.mjs +307 -88
  76. package/js/play/index.js +2864 -619
  77. package/js/play/keys.js +193 -0
  78. package/js/results/index.js +223 -30
  79. package/js/results/server.js +230 -0
  80. package/js/results/wire.js +64 -16
  81. package/package.json +6 -4
  82. package/release.json +5 -5
  83. package/skills/xsolana-economy/SKILL.md +39 -21
  84. package/skills/xsolana-economy/references/after-launch.md +58 -38
  85. package/skills/xsolana-economy/references/apps.md +97 -50
  86. package/skills/xsolana-economy/references/sentence-to-spec.md +148 -121
  87. package/skills/xsolana-economy/references/telling-the-person.md +61 -17
  88. package/types/chain/core.d.ts +4 -2
  89. package/types/chain/lookup.d.ts +5 -2
  90. package/types/chain/rpc.d.ts +3 -1
  91. package/types/client/accounts.d.ts +217 -56
  92. package/types/client/grant.d.ts +72 -27
  93. package/types/client/idl/loot.d.ts +602 -549
  94. package/types/client/index.d.ts +1 -0
  95. package/types/client/ix.d.ts +3884 -247
  96. package/types/client/pda.d.ts +22 -16
  97. package/types/client/policy.d.ts +39 -14
  98. package/types/client/random.d.ts +26 -0
  99. package/types/client/sizes.d.ts +93 -5
  100. package/types/economy/check.d.mts +5 -10
  101. package/types/economy/cost.d.mts +2 -1
  102. package/types/economy/guide.d.mts +5 -0
  103. package/types/economy/live.d.mts +16 -1
  104. package/types/economy/model.d.mts +151 -38
  105. package/types/economy/plan.d.mts +237 -194
  106. package/types/economy/simulate.d.mts +21 -13
  107. package/types/mcp/tools.d.mts +21 -13
  108. package/types/play/index.d.ts +453 -53
  109. package/types/play/keys.d.ts +51 -0
  110. package/types/results/index.d.ts +7 -6
  111. package/types/results/server.d.ts +126 -0
  112. package/types/results/wire.d.ts +1 -1
@@ -3,114 +3,126 @@
3
3
  Read the sentence for **what players pay, what they can get, where the money goes, and who decides**. Each phrase
4
4
  maps to a part of the spec (`xsolana_guide` has every field).
5
5
 
6
- | They say | Write |
7
- | --- | --- |
8
- | "a shop", "sell hats for 0.05 SOL", "limited to 100" | `items` with `price` (in the project's `currency`) and `supply`: bought in one transaction (`buy`), the item handed over at once. `{serial}` in an item's `uri` gives each unit its own art |
9
- | "free to claim", "a free starter item" | an item with `"free": true` (a gate or a supply keeps bots out), or a grant with limits |
10
- | "costs more each time", "+3% per sale" | an item's or box's `rise` (`growthPercent`, or a `step`) |
11
- | "price drops every minute until it sells", "Dutch auction" | `fall` (`step`, `every`, `floor` above 0): a Dutch auction is a falling price, not `auctions`. By default a sale holds the price where it was paid and restarts the countdown, so it falls only while nobody buys; `"clock": "shared"` keeps it falling on its own clock however often it sells (a Dutch drop of many units). Say which |
12
- | "drops at 6pm Friday", "on sale for a week", "the sale ends Sunday" | an item's, box's or recipe's `opens` and `closes` (times: "publish+2d" or a date) |
13
- | "one per wallet", "max 2 per wallet", "5 a day each" | an item's, box's or recipe's `perWallet` (in all) and `perWalletEach` (`{ "count": 5, "every": "1d" }`): the chain counts each wallet (a person with many wallets gets around it: say so) |
14
- | "auction", "highest bid wins", "bids", "losing bids are returned", "1 of 1 to the top bidder" | `auctions`: a `lot` (`{ "item": key }` for a new one; `{ "asset": address }` for one the owner already holds, once live), `reserve`, `minRaise` (percent), `runs`, `extendBy` (anti-sniping), a `split` (the artist's or a collaborator's share) |
15
- | "auction my NFT", "my 1-of-1" (and you can't tell whether they already hold it) | a new item lot: an item with `supply: 1` and `lot: { "item": key }`, minted to the winner. A collectible they hold can only go up once the economy is live (`E-AUCTION`): say so, and save the spec as `xsolana.json` all the same |
16
- | "sealed bids", "nobody sees the bids", "second-price", "Vickrey" | loot cannot: say so, and offer an open auction (`auctions`) |
17
- | "loot box", "gacha", "mystery box", "pack", "60/25/10/4/1" | `boxes` with `outcomes` of `gives: "item"` and a `chance` each |
18
- | "a starter pack: a sword, a shield and some gold", "three cards in one pack", "an item and coins together" | an outcome `{ "gives": "bundle", "prizes": [ … ] }` of up to three (items, `pay` from a pool, `tokens` minted, `stash`, `nft`, `roll`); more than three: make the third prize a roll of another box (`{ "gives": "roll", "box": key }`, that box `"active": false` and itself a bundle), so one purchase gives five or more, the rolled part after the oracle. A box of one outcome with nothing random and no NFT it rolls on is bought in one step: no order, no oracle, nothing to settle |
19
- | "coin flip", "double or nothing", "plinko", "a prize" | a box whose outcomes `pay` from a pool; its `split` fills that pool. The box sells only while the pool holds every fixed prize (seed it at least that much), and a prize won is set aside for its winner at once |
20
- | "jackpot", "a tenth of the jackpot" | an outcome `gives: "share"` of a pool (`percent`) |
21
- | "give away our partner's NFTs", "a 5% chance at an NFT from another collection", "random NFTs from our treasury", "NFTs we already own" | a `stashes` entry (`nfts`: their addresses, held by the owner's wallet at go-live; `collection` for Core NFTs of another collection; a Token Metadata NFT as `{ "mint", "programmable" }`) and a box outcome `{ "gives": "stash", "stash": "<key>" }`: a roll picks one at random and takes it out of the stash at once. Rarity tiers: one stash per tier, each an outcome with its chance. A compressed NFT goes in after go-live (`E-STASH`) |
22
- | "a prize", "the winner gets", "reward" (known in advance) on a grant, a raffle, a tournament, a quest, a mission, a recipe | a prize line given in the same transaction: `{ "gives": "item", "item": key }`, `{ "gives": "tokens", "amount": n, "from": "mint", "currency": key }` or `"from": "pool:<key>"`, `{ "gives": "share", "pool": key, "percent": p }`, `{ "gives": "nft", "stash": key, "nft": "<address>" }` (an NFT a stash lists). One step for the player, no oracle |
23
- | "a random prize" for a grant, a raffle, a tournament, a quest, a mission | the prize `{ "gives": "roll", "box": "<box>" }`: a box (`"active": false`) of whatever it may give (items, `pay`, `share`, `stash`), rolled for the winner. Anyone asks the oracle for the roll and settles it; it is never refunded while it waits. Use it only for chance |
24
- | "a free daily chest", "quest reward: a random item", "a promo code for a mystery box" | a grant with `"prize": { "gives": "roll", "box": "<box>" }` (its funds pay the roll's order, back to the winner at settle); a known reward is a direct `prize` instead |
25
- | "raffle my NFT", "the winner gets the 1-of-1", "first place wins an NFT", "refund everyone if not enough tickets sell" | a lottery with `"raffle": "<address>"` (or `{ "nft", "collection" }` for an NFT of another collection) and no `every`: the NFT waits in the lottery's own stash, which nothing but the draw can empty, and the winner gets it before any ticket money moves. `minSold`: a round that sold fewer is cancelled at its draw and refunded. The ticket money goes where its `split` says (`"treasury"`, the seller's). A seller raffling their own NFT is the owner of a project of their own (`app.raffle`) |
26
- | "sell my shield for 3 SOL", "list it at a price", "rent out my mech for a few days", "scholarships" | a listing, made from the app's page at run time, not in xsolana.json: `app.listForSale(asset, { price })`, `app.listForRent(asset, { price, lasts, maxPeriods })`; a buyer `app.buyListing(asset, { listing })`, a renter `app.rentListing(asset, { periods, listing })` (the `listing` the page showed); the app reads who rents it with `app.rentedTo(asset)` (one account read). On-chain gates don't read rentals |
27
- | "presale", "fair launch", "10% of the supply split by what each put in", "allocation by contribution" | a contest with one side, a `minPot`, a split of 100% to the treasury and a token `prize` with `"split": true`: set aside whole when it is decided (or the decision is refused), each backer's part by what they put in; short of `minPot`, everyone is refunded |
28
- | "vesting", "cliff then monthly", "locked team tokens", "nobody can take it back", "unlock schedule" | a schedule `{ "does": "pay", "sealed": true, "currency": key, "to": "wallet:<address>", "amount": n, "start": "publish+60d", "every": "30d", "maxRuns": 6, "fund": total, "fundFrom": "mint" }`: its own vault, never edited or stopped by anyone, the last run pays the rest. An ordinary schedule from a pool is only the owner's promise (`W-SCHEDULE-PAY`) |
29
- | "leave early for a 10% fee", "paper hands penalty", "unstaking takes 7 days", "2% harvest tax", "compound" | a farm of tokens: a tier's `exitFee` (percent) with the farm's `exitFeeTo` (`"burn"` or `"pool:<key>"`: its own rewards pool releases it to those who stay, over the farm's longest lock), `unbond` ("7d"), `toll` `{ "percent": 2, "to": "burn" }`; compounding is `app.compound(farm)` when the rewards are the staked token, the lock unchanged. A farm of collectibles leaves early by a recipe that lowers its `lockAttribute` |
30
- | "monthly payouts from a list", "each wallet gets a different amount", "points to tokens by list" | a grant of tokens whose `list` entries are `{ "wallet", "amount", "round" }`: each claims its amount once; the same wallet under another `round` claims again |
31
- | "the tournament winner gets a trophy NFT", "a random NFT for every correct pick" | a contest's `prize`, given to each winning entry when it is claimed, besides its part of the pot: one NFT goes to the first entry claimed (`W-CONTEST-PRIZE`; a contest whose sides are the players has one winner), a roll gives each winner a random one |
32
- | "auction a mystery box", "bid on a mystery lot" | an auction `lot: { "roll": "<box>" }`: the winner gets a roll of the box; the price goes to the treasury. Say what the box can give, with its odds (`W-AUCTION`) |
33
- | "the expedition brings back a chest", "missions drop random loot" | a farm tier's `prize`: `{ "gives": "roll", "box": "<box>" }`, rolled when the mission is completed (its lock stayed, its upkeep never ran dry, not forfeited) while the NFT goes home; a known reward (tokens, an item, an NFT) is given as it leaves |
34
- | "only pass holders can buy it", "members only" | a `gate` of the item alone: `{ "item": "pass" }` |
35
- | "only holders can open it", "level 5 and up" | a `gate`: `{ "item": "pass", "attribute": "level", "atLeast": 5 }` |
36
- | "only these players", "just the two of them", "the tournament's entrants" | a `gate` of wallets: `{ "wallets": ["<address>", …] }` (at most 16) |
37
- | "stake to enter", "staking tiers for a token sale", "only people who lock 100 of our token can buy" | a farm of a token with no `rewards` when staking pays nothing (an empty pool is opened for it), and a `gate` `{ "staked": { "farm": "<farm of a token>", "atLeast": 100 } }`: the wallet's stake in that farm, counted only while its tier's lock runs and from the second after it is made (one stake can't move to another wallet and count again; a tier with no lock never passes, `W-STAKED`). The app passes the seat and the farm |
38
- | "allowlist", "whitelist of 5,000 wallets", "a snapshot of our holders", "early supporters only" | a `gate` `{ "list": ["<address>", …] }` of any size: the program keeps only its root, the page passes each player's proof (`W-LIST`); a new list is a new root, published again |
39
- | "rarer pulls for higher levels", "a charm stat improves the odds" | a box's `odds`: `{ "outcome": 3, "when": { "attribute": "charm" }, "add": { "perPointPercent": 10, "upTo": 9 } }` |
40
- | "stake to earn", "x% house edge to stakers", "fees go to holders" | a `split` share `"to": "pool:<rewards>"` and a `farms` entry whose `rewards` is that pool |
41
- | "lock for longer, earn more", "30-day lock pays 3x" | a farm's `tiers`: `[{ "lock": "1d", "multiplier": 1 }, { "lock": "30d", "multiplier": 3 }]` |
42
- | "stake your NFT", "rarer heroes earn more" | a farm with `"stakes": "collectibles"`, `boosts` on an attribute, `maxBoostPercent` |
43
- | "earn 12 a day per NFT", "daily emissions per NFT", "a fixed rate per NFT" | a farm's `rate`: `{ "perDay": 12, "minted": true }` (the reward a token the project mints), the same for each NFT whoever else stakes; without `minted` it pays from the rewards pool while it lasts |
44
- | "XP", "levels up while staked", "level 5 after a week", "evolve at level 10" | a farm's `growth`: `{ "perDay": 100, "levels": [100, 300, 600] }` writes `xp` and `level` on the NFT for time staked, with nobody's signature; a boost per `level`, a gate, box `odds` or a recipe's `needs` read it (evolve: a recipe that burns the hero and makes the next form, `needs` level 10). `"streak": true` resets it when the NFT leaves |
45
- | "a mission can fail", "the hero comes back hurt", "the hero can be lost / captured" | the tier's `roll`: a box (`"active": false`, `rollsOn` the mission's NFTs) whose outcomes say what happens: an item or a prize on success, `"shifts": [{ "attribute": "affection", "by": -2 }]` when hurt, `"then": "burn"` (or `"treasury"`, `{ "giveTo": "<wallet>" }`) when lost; `odds` by level make success likelier. The farm then needs freeze or escrow custody for a loss to bite (a watched NFT sold first is simply not rolled) |
46
- | "enchant: 60% +1 level, 30% -1 level, 10% it breaks", "upgrade can fail" | a box with `rollsOn` (the NFT the player brings) and outcomes with `shifts` and `then`; one that can burn or give the NFT away holds it until the roll settles, so say so |
47
- | "risky forge", "merge with a chance of failure" | a recipe output `{ "gives": "roll", "box": "<box>", "on": 0 }` (the box `"active": false`): on a kept collectible, or with no `on`, a roll whose outcomes are items or prizes |
48
- | "merging costs a level", "lose a stat" | a recipe's `raises` with a negative `by`: `{ "place": 0, "key": "level", "by": -1 }` (never below 0) |
49
- | "missions", "expeditions", "send heroes on a 3-day quest for loot", "random rewards" | a farm tier with a `lock` and a `prize`: `{ "gives": "tokens", "amount": 2, "from": "mint", "currency": "<ticket>" }` (grown by boosts); a box priced in that ticket gives the random loot, its `odds` raised by level or trait. A mission needs a lock (`E-FARM`) |
50
- | "1 level a day", "+2 affection a day" | a farm's `growth` without `levels`: the points are the attribute itself, `[{ "attribute": "level", "perDay": 1 }, { "attribute": "affection", "perDay": 2 }]` (up to 4) |
51
- | "a mission open for one week", "a season that ends" | a farm's `runs`: `"7d"` from when it goes live; it takes no stake at its end or after it, nor one whose lock would run past it. A sharing farm keeps what it has not released by the end in the pool (the project's): fund it at least its release span (its longest lock, a day at least) before the end. A mission that pays no tokens has `"rate": { "perDay": 0, "minted": true }`. Released later: add the farm to the spec and publish again |
52
- | "level 3 to join", "needs level 100 and 6 affection" | a farm's `gate`, one rule or a list the NFT must all pass: `[{ "collection": "<address>", "attribute": "level", "atLeast": 100 }, { …, "attribute": "affection", "atLeast": 6 }]` |
53
- | "fuel", "food", "upkeep", "drills need fuel to dig", "runs out", "license for 30 days" | a farm of collectibles' `upkeepAttribute` (`"fuel_until"`): a staked NFT earns and grows only until that time, and it runs out by itself; a recipe sells it: `in` the NFT (`use`) and a `pay` line, `raises`: `[{ "place": 0, "key": "fuel_until", "by": "1d", "upTo": "3d", "fromNow": true }]` (a dry tank fills from now; at most 3 days ahead). A recipe reaches an NFT staked in escrow; the play page refreshes its seat in the same transaction. A mission's prize needs the fuel to last its lock |
54
- | "leave early for a fee", "early unstake penalty", "break the lock", "recall" | a farm of collectibles' `lockAttribute` (`"free_at"`: the lock is written on the NFT as the time it ends) and a recipe that lowers it for a price: `raises`: `[{ "place": 0, "key": "free_at", "by": "-1d", "fromNow": true }]` with a `pay` line burned, or paid into the farm's rewards pool (released to the seats there over the farm's longest lock, a day at least). Leaving early takes no mission prize. A farm of tokens uses its tier's `exitFee` instead |
55
- | "jail", "cooldown after a raid", "arrested for 24 hours" | a roll's outcome `"shifts": [{ "attribute": "jailed", "by": "1d", "upTo": "1d", "fromNow": true }]`, and the mission's gate `{ "item": "<hero>", "attribute": "jailed", "time": "passed" }`; on a staked NFT with `lockAttribute`, a shift on that key keeps it in longer |
56
- | "set bonus", "collect all four elements", "a full set earns more", "bonus for a complete set staked" | a set bound into a badge: a recipe whose `in` takes each piece `{ "takes": "staked", "item": "<piece>", "farm": "<its farm>" }` (the farm it is staked in, escrowed or frozen) and whose `out` is a badge item, with `raises` on each piece `{ "key": "bound", "by": 1, "upTo": 1 }` (one badge a set) and `{ "key": "free_at", "by": "20000d" }` (that farm has `"lockAttribute": "free_at"`, so they stay staked while the badge exists; a raise on a staked piece must move its farm's lock, or the craft is refused); the badge earns in a farm of its own. Unbinding: `in` the badge burned and the pieces `staked` in the same farm, `needs` each `bound` at least 1, `raises` `bound` by -1 and `free_at` by "-20000d", and no `pay` line (one waits while the platform suspends the project, keeping the pieces locked: charge when binding). Up to 4 pieces (8 raises; one transaction). The `elements-set` example |
57
- | "only with fuel left", "pass still valid" | a rule's `"time": "ahead"` (a time still to come) or `"passed"`; a farm's gate reads a time as the NFT comes in |
58
- | "the NFT stays in the guild while it goes on the mission", "can still sell it" | `"custody": "watch"` on the mission (and on any farm whose NFTs must stay free); the guild itself may freeze |
59
- | "my pNFTs", "compressed NFTs", "any NFT" | nothing to add: every route takes Core, Token Metadata (legacy, pNFT) and compressed NFTs |
60
- | "a badge for everyone who comes", "airdrop an NFT to 20,000 wallets", "POAPs", "mint cNFTs" | the prize `{ "gives": "item", "item": "<badge>", "compressed": true }` (a grant's, a place's, a contest's, a mission's or a recipe's): a compressed NFT of the collectible, no rent each (a Core one is about 0.003 SOL), minted into a tree the setup opens (`compressedTree: { "holds": n }`, 16,384 by default, about 0.23 SOL once). Write it before going live: the setup's collection takes compressed NFTs only if it is made with them in the spec. Not soulbound, not a box outcome; give a rare one as a Core collectible |
61
- | "blue background gets 10%", "a trait in the metadata" | a boost on the attribute (`{ "when": { "collection": "<address>", "attribute": "Background", "equals": "Blue" }, "add": { "addPercent": 10 } }`) and the spec's `traits` (the root of `loot.traitsTree` of the collection's off-chain metadata): each NFT's traits are proven into its Sheet once |
62
- | "pay to level up", "train", "upgrade a stat for 100 shards", "max level 10" | a recipe: `in` the collectible kept (`use`) and a `pay` line, and `raises`: `[{ "place": 0, "key": "level", "by": 1, "upTo": 10 }]`. A farm's growth never takes a bought level back down |
63
- | "upgrade an NFT of another collection in our game", "stats for NFTs from another collection" | a recipe line `{ "takes": "show", "collection": "<address>" }` and `raises` on it: the stats live in the project's Sheet of that NFT (or on the NFT, when its Attributes plugin names the project); the authority imports stats that only exist off the chain with a `set` decision |
64
- | "earns its power a day", "rate by rarity or stat" | a farm `rate` and a boost `perPointPercent` on the stat: `{ "when": { "attribute": "power" }, "add": { "perPointPercent": 100, "upTo": 100 } }` and `maxBoostPercent` up to 10,000 (x101) |
65
- | "level-up rewards", "claim a badge at level 15" | an open grant gated by `{ "attribute": "level", "atLeast": 15 }`, `perWallet` and the NFT counted once (show the badge, claim once) |
66
- | "weekly lottery", "three places", "one grand prize and seven smaller" | `lotteries` with `every: "7d"`, `places: [60, 25, 15]`. A lottery has a pot of its own (`currency`, the project's by default; `seed` fills it at go-live): only its winners are paid from it, so the owner can't drain it by mistake |
67
- | "a raffle", "one draw at the end of the month" | a lottery with no `every` and a `firstDraw`: it draws once and ends |
68
- | "x% of tickets go to me" | the lottery's `cutPercent` (a share to the treasury, paid after a successful draw) |
69
- | "20% of every ticket to the school fund", "the referrer gets 8% of the tickets they bring" | the lottery's `split`: `{ "to": "wallet:<fund>", "percent": 20 }`, `{ "to": "chosen", "percent": 8 }` (the buyer fixes the referrer when they buy; shares are paid after a successful draw) |
70
- | "max 10 tickets each" | the lottery's `perWallet` (tickets one wallet holds in a round) |
71
- | "fed by 10% of every flip" | a 10% share in the box's `split` to `"lottery:<key>"` |
72
- | "burn X for Y", "merge two into one", "craft", "breed", "upgrade" | `recipes`: `in` (`burn`, `use`, `renew`, `pay`, `trade`, `staked`, `show`) and `out` (`item`, `tokens`, `share`, `nft`, `roll`) |
73
- | "trade in any NFT of ours for 10 gold", "swap an NFT of another collection for a badge", "we keep what's traded in and give it away again" | a recipe line `{ "takes": "trade", "collection": "<address>" (left out: the project's own), "then": "burn" \| { "stash": "<key>" } \| { "giveTo": "<wallet>" } }`: the NFT is the price. Into a stash, it becomes another player's prize (a box outcome `{ "gives": "stash" }` resells it) |
74
- | "a trade-in box: give an NFT, roll for a new one" | a box with `rollsOn` (which NFTs it takes) and `"then": { "stash": "<key>" }` (or `"burn"`): every outcome keeps the NFT paid, and its outcomes give the prizes |
75
- | "tickets cost an NFT", "sacrifice an NFT for a raffle ticket" | a lottery's `ticketNft`: `{ "rule": { "item": key } or { "collection": address }, "then": "burn" or { "stash": key } }`, with no `price`, `split` or `ticketCurrency`; the pot is its `seed` and its places' prizes. An NFT spent on a ticket is never refunded (`W-NFT-PRICE`) |
76
- | "no-loss lottery", "prize savings", "you never lose your ticket", "the sponsor pays the prize" | a lottery with `"noLoss": true`: every ticket (tokens or an NFT) goes back to its buyer once the round is drawn, and the winners take only the pot: its `seed`, a sponsor's payments, shares of other sales (`"lottery:<key>"`). No `split`, `cutPercent` or `ticketCurrency`. Say that tickets are not weighted by time: someone may buy most of them right before the draw and risk only the fees |
77
- | "stake an NFT on the duel", "winner takes the loser's NFT" | a contest's `stakeNft` (a rule): each entry stakes one NFT; each winning entry takes its own back and one loser's as it is claimed, and the opener gets what the winners leave |
78
- | "bid with NFTs", "the most NFTs wins" | an auction's `bidNfts` (a rule): a bid is up to three Core or Token Metadata NFTs (never compressed), the most wins, `reserve` counts NFTs |
79
- | "pay with an NFT to enter", "burn an NFT for an attempt" | not a spec part: the page calls `loot.payNftIx` (burned, or given to a wallet) and the app admits by the receipt, as for a payment |
80
- | "points", "tickets", "shards", "attempts", "sparks" | a token the project mints: a `tokens` entry `source: "project"`, given by a recipe's `out` (`from: "mint"`) or a schedule |
81
- | "a pass that claims daily", "cooldown" | an item with `life.cooldown`; a recipe that `use`s it and pays `tokens` from a pool |
82
- | "ready after 7 days", "rots in 3 days", "6 uses" | an item's `life`: `readyAfter`, `lasts`, `uses`. Only recipes read `life` (a recipe refuses an item not ready, expired or used up); gates, odds and boosts never do |
83
- | "monthly membership", "renew", "pass valid until", "expires unless renewed", "a week's access" | a pass item with a time attribute (`paid_until`), a renew recipe (`in`: the pass `use`d and a `pay` line; `raises`: `[{ "place": 0, "key": "paid_until", "by": "30d", "fromNow": true, "upTo": "60d" }]`), and gates that read `{ "item": "pass", "attribute": "paid_until", "time": "ahead" }`. Nothing charges a wallet by itself: the member renews, the page reminds them |
84
- | "every day mint 5,000 into the pool, halving every 90 days" | a `schedules` entry `does: "issue"`, `every: "1d"`, `halveEvery: 90` |
85
- | "burn half a percent of the pool daily", "move 5% weekly" | a schedule `does: "burn"` or `does: "move"` with `percent` |
86
- | "pay the team 100 a week", "a landlord's rent" | a schedule `does: "pay"` from a pool `to` "wallet:<address>" or "holder:<asset>" (anyone may run it when due: the page, a player, a sweep) |
87
- | "an entry fee", "pay per attempt" | `payments` with `amount` and a `split`. A payment has no gate, no opens/closes, no `"chosen:<item>"` share and no rising price: when the entry needs one, sell it as a box with one outcome that gives nothing (or an item), which has them all. Its split may name a `"chosen"` wallet (a referrer): `app.pay(key, { chosenRecipient })` |
88
- | "tips", "donations", "pay what you want", "any amount above 0.001" | a payment with `"any": true`: `amount` is the least; the page asks the amount and calls `app.pay(key, { amount })` |
89
- | "all proceeds to the school", "our DAO's vault gets the money", "the cut goes to the charity" | `"treasury": "wallet:<their address>"`: what every split leaves, and a lottery's cut, go there. Left out, the treasury is the owner's own wallet. The owner can change it later |
90
- | "several signers must approve spending", "a multisig" | loot isn't a multisig: the money goes to their multisig's vault (for example Squads): `"treasury": "wallet:<vault>"`, or a `wallet:` share; spending from it is approved in the multisig |
91
- | "60% to me, 40% to my co-designer", "10% to our charity", "split with my partners" | split shares `"to": "wallet:<their wallet address>"` (ask for each address). Going live opens each wallet's account for the currency. Use `account:` only for a token account that already exists |
92
- | "our partner gets 4%" (one fixed wallet) | a split share `"to": "wallet:<their address>"` |
93
- | "referral links", "each streamer gets 6% of what they bring", "tip the artist I pick" | a box's, shop item's, lottery's or payment's split share `"to": "chosen"`: the buyer names the wallet when they buy (the page passes it from the link: `app.buy(key, { listing, chosenRecipient: referrer })`, `app.pay(key, { chosenRecipient: referrer })`), never their own (a second wallet of theirs still works, so a referral share is at most a rebate: price it so); with none it goes to the treasury |
94
- | "a fee to the owner of the workshop I craft at", "the venue's owner gets a cut" | a split share `"to": "chosen:<item key>"`: the buyer names a collectible of that item, and whoever holds it is paid (`{ chosenRecipient: <its address> }`) |
95
- | "the holder of the deed gets 3%" | a split share `"to": "holder:<collectible address>"` |
96
- | "duel", "bet on a side", "prediction", "the winner takes the pot" | `contests` with `sides` and a rake in `split`; the authority decides, or a results server's key in `deciders` signs the result (anyone sends it) |
97
- | "1v1 wager", "both put in 10 and the winner takes it" | a contest with a wallet `gate` of the two players and a `stake` (every entry exactly that) |
98
- | "crowdfund", "all or nothing", "refund everyone if we miss the goal" | a contest with `sides: 1`, a `minPot` (the goal), and a `split` of 100% to `"wallet:<recipient>"`. Short of the goal when entries close, it is off by itself and every backer claims their pledge back (anyone can send those claims). Reached: the owner (or a decider) names side 0 and the whole pot goes to the recipient. A badge for each backer: a grant whose `list` is the backers |
99
- | "the game decides who cleared the level and pays them" | `decisions`: `does: "payout"` from a pool, with `when` in words |
100
- | "level up my pet", "write the score on the NFT" | a decision `does: "set"` (the authority rewrites attributes) |
101
- | "send caravans on trade runs, the game says how they went", "the event ends at 18:00" | `holds` (the item, `for` a time the player picks, or `until` a set time) and an `award` decision |
102
- | "quests that pay", "airdrop to these wallets", "one per person" | `grants`: a `prize` (an item, tokens, an NFT, or a roll); a `list` of wallets, or `vouchers` (a results server's public key: see `after-launch.md`); `limits`. A claim of a prize it can't give is refused and keeps its round |
103
- | "rewards by rank", "1st gets 400, 2nd 250", "points to tokens", "each winner a different amount" | one grant whose `prize` is a fixed amount of tokens (the most any claim takes: 1st place's) paid by `vouchers` or a list: each voucher (or list leaf) names that claim's own amount, up to the grant's (`signVoucher({ …, amount })`, `app.claimGrant(key, { voucher })`); none named pays the grant's amount. A `list` written in xsolana.json does the same: each entry `{ wallet, amount }` names its own amount |
104
- | "at most once a day each", "2 wins a day per wallet" | a grant's `limits.perWalletEach`: `{ "count": 1, "every": "1d" }` |
105
- | "promo codes on flyers", "a code pays once", "QR claim links" | a grant with `keys` (the public key of each code's keypair: the page derives the keypair from the typed code and signs a voucher for the player's own wallet) and `limits.perKey: 1` |
106
- | "holders of the ticket claim a free badge once" | an open grant: no `vouchers`, `keys` or `list`, a `gate` of the ticket, and `limits.perWallet: 1` (each ticket shown claims once too) |
107
- | "bring your own NFT", "any NFT of a partner collection can play", "holders of X get" | a `gate` (or a farm's, or odds') rule with `collection`: the collection's address; the project's notes about each NFT (a level) go in its Sheet |
108
- | "our own token, 10 million, we keep it", "a fixed supply", "a community token" | `source: "project"` with `supply`: going live mints all of it to the owner and turns minting off for good; pools are seeded from it |
109
- | "our token", "the coin we already launched", "this mint" | `source: "existing"` with `mint` and `decimals` (ask for the address). loot doesn't put a token on a market: for trading, launch it on a token launchpad or an exchange first |
110
- | "Token-2022", "a transfer fee" | `program: "token-2022"` and `powers.transferFeePercent` |
111
- | "tokens only, no NFTs" | `"collection": false`: no collection is made (no collectibles; normal items and tokens work) |
112
- | "mint into our existing collection", "two collections" | `collections`: Core collections the owner's wallet already controls (up to eight in all, with the project's own), each with the project added as an update delegate; an item's `collection` names the one it is minted into |
113
- | "a second game or community from the same wallet" | `projectNumber`: 1, 2, … (the first is 0); each number is a project of its own |
6
+ | They say | Write |
7
+ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
8
+ | "a shop", "sell hats for 0.05 SOL", "limited to 100" | `items` with `price` (in the project's `currency`) and `supply`: bought in one transaction (`buy`), the item handed over at once. `{serial}` in an item's `uri` gives each unit its own art |
9
+ | "free to claim", "a free starter item" | an item with `"free": true` (a gate or a supply keeps bots out), or a grant with limits |
10
+ | "costs more each time", "+3% per sale" | an item's or box's `rise` (`growthPercent`, or a `step`) |
11
+ | "price drops every minute until it sells", "Dutch auction" | `fall` (`step`, `every`, `floor` above 0): a Dutch auction is a falling price, not `auctions`. By default a sale holds the price where it was paid and restarts the countdown, so it falls only while nobody buys; `"clock": "shared"` keeps it falling on its own clock however often it sells (a Dutch drop of many units). Say which |
12
+ | "drops at 6pm Friday", "on sale for a week", "the sale ends Sunday" | an item's, box's or recipe's `opens` and `closes` (times: "publish+2d" or a date) |
13
+ | "one per wallet", "max 2 per wallet", "5 a day each" | an item's, box's or recipe's `perWallet` (in all) and `perWalletEach` (`{ "count": 5, "every": "1d" }`): the chain counts each wallet (a person with many wallets gets around it: say so) |
14
+ | "auction", "highest bid wins", "bids", "losing bids are returned", "1 of 1 to the top bidder" | `auctions`: a `lot` (`{ "item": key }` for a new one; `{ "asset": address }` for one the owner already holds, once live), `reserve`, `minRaise` (percent), `runs`, `extendBy` (anti-sniping), a `split` (the artist's or a collaborator's share) |
15
+ | "auction my NFT", "my 1-of-1" (and you can't tell whether they already hold it) | a new item lot: an item with `supply: 1` and `lot: { "item": key }`, minted to the winner. A collectible they hold can only go up once the economy is live (`E-AUCTION`): say so, and save the spec as `xsolana.json` all the same |
16
+ | "sealed bids", "nobody sees the bids", "second-price", "Vickrey" | loot cannot: say so, and offer an open auction (`auctions`) |
17
+ | "loot box", "gacha", "mystery box", "60/25/10/4/1" | `boxes` with `outcomes` of `gives: "item"` and a `chance` each |
18
+ | "pity", "guaranteed legendary by pull 90", "soft pity" | a soulbound charm each player holds, which the pull box rolls on (`rollsOn`, `then: "back"`): misses shift its `pity` +1, the rare shifts it by -1000000 (it stops at 0: a reset), the rare's `odds` add a percent a point. It never trades; a pity token minted on misses can be sold and never resets |
19
+ | "stop selling when the rare is gone", "keep selling what's left", "what happens when an item sells out" | a box's `whenOut`: `"stopSales"` (the default: it isn't sold while any outcome can't be given, so the odds stay as written) or `"drawLeft"` (it keeps selling, drawing among what is left, the rest's chances rising: a player can empty a common outcome to make a rare likelier). Tell the person the trade-off |
20
+ | "a booster of five", "a pack of 5 random cards", "three cards in one pack" | a box with `"slots": [{ "count": 5 }]`: each open draws five outcomes by their chances, all from one oracle answer, in one purchase and one wallet prompt. `"distinct": true` on the slot for five different outcomes ("a hand of different cards") |
21
+ | "a guaranteed rare", "one rare or better in every pack", "four commons and a rare" | slots that name outcomes by their place in `outcomes` (from 0): `[{ "count": 4, "share": 60 }, { "count": 1, "from": [8, 9, 10], "share": 40 }]` is four draws among all and one among the rares, the rare slot worth 40% of the price. Slots that name outcomes each need a `share`, 100 together: a draw that can't be made (its slot sold out) gives its slot's worth back, so a guaranteed rare keeps its value when the commons run out; ask what the rare is worth |
22
+ | "10-pull", "open ten at once", "buy 3 packs" | the same box: the page opens several in one purchase (`app.openBox(key, { listing, count: 10 })`), one wallet prompt and one oracle answer, each open at the price the one before it left (`maxPrice` is a pack's). Nothing to add to the spec |
23
+ | "a starter pack: a sword, a shield and some gold", "an item and coins together" | an outcome `{ "gives": "bundle", "prizes": [ … ] }` of any number (items, `pay` from a pool, `tokens` minted, `stash`, `nft`, `roll`). Random cards are slots, not a bundle. A box of one outcome with nothing random and no NFT it rolls on is bought in one step: no order, no oracle, nothing to settle |
24
+ | "random traits", "each one is different", "a random color and a power from 1 to 100", "generative" | a fresh item: its `drawn` traits, each from a table (`{ "attribute": "color", "table": "colors" }`, a row by weight among those in stock) or a range (`{ "attribute": "power", "min": 1, "max": 100 }`), and `tables`: `[{ "key": "colors", "kind": "labels", "rows": [{ "label": "red", "weight": 3 }, { "label": "gold", "weight": 1, "stock": 100 }] }]`. `{color}` and `{power}` in its `name` and `uri` take each unit's values, so the project hosts a file for every value (`telling-the-person.md`). Given only by a box's roll (a box of one outcome still rolls); never priced in the shop, granted or crafted |
25
+ | "a deck of thousands of cards", "a card pool with stock", "1 of 50 gold cards" | a fresh item that draws its card from a `numbers` table (`"drawn": [{ "attribute": "card", "table": "deck" }]`; each row gives its own number, from 0: the card's place in the project's own card list) with a `stock` on the scarce rows, and its `uri` naming the card (`https://…/cards/{card}.json`); a box's slots deal several a pack. A table is written once and sealed when it goes live, a batch of rows a transaction; its rent is the owner's and never comes back (about 0.00004 SOL a row of numbers, 0.0002 a row of labels) |
26
+ | "coin flip", "double or nothing", "plinko", "a prize" | a box whose outcomes `pay` from a pool; its `split` fills that pool. The box sells only while the pool holds every fixed prize for this purchase and those waiting for the oracle (seed it a few prizes deep), and a prize won is set aside for its winner at once |
27
+ | "jackpot", "a jackpot that grows", "a tenth of the jackpot" | a jackpot that grows is a lottery's pot (`"pot": 0`): its tickets fill it, what its places leave stays for the next round, and its places take percents of it. A box or any other prize gives a fixed amount (`gives: "pay"`, `amount`), all of it or none; a share of a pool as a prize is refused |
28
+ | "give away our partner's NFTs", "a 5% chance at an NFT from another collection", "random NFTs from our treasury", "NFTs we already own" | a `stashes` entry (`nfts`: their addresses, held by the owner's wallet at go-live; `collection` for Core NFTs of another collection; a Token Metadata NFT as `{ "mint", "programmable" }`) and a box outcome `{ "gives": "stash", "stash": "<key>" }`: a roll draws one by weight and takes it out of the stash at once. An NFT may carry a `weight` (1 when left out: one that weighs 10 is drawn ten times as often), so one stash can hold its own rarities; or one stash per tier, each an outcome with its chance. A compressed NFT goes in after go-live (`E-STASH`) |
29
+ | "a prize", "the winner gets", "reward" (known in advance) on a grant, a raffle, a tournament, a quest, a mission, a recipe | a prize line given in the same transaction: `{ "gives": "item", "item": key }`, `{ "gives": "tokens", "amount": n, "from": "mint", "currency": key }` or `"from": "pool:<key>"`, `{ "gives": "share", "pool": key, "percent": p }`, `{ "gives": "nft", "stash": key, "nft": "<address>" }` (an NFT a stash lists). One step for the player, no oracle |
30
+ | "a random prize" for a grant, a raffle, a tournament, a quest, a mission | the prize `{ "gives": "roll", "box": "<box>" }`: a box (`"active": false`) of whatever it may give (items, `pay`, `stash`), rolled for the winner. The winner or the project's keys ask the oracle for the roll and settle it; it holds no payment, so it is never refunded and never lost (an edit of the box leaves it to roll on the box as edited). Use it only for chance |
31
+ | "a free daily chest", "quest reward: a random item", "a promo code for a mystery box" | a grant with `"prize": { "gives": "roll", "box": "<box>" }` (its funds pay the roll's order, back to the winner at settle); a known reward is a direct `prize` instead |
32
+ | "raffle my NFT", "the winner gets the 1-of-1", "first place wins an NFT", "refund everyone if not enough tickets sell" | a lottery with `"raffle": "<address>"` (or `{ "nft", "collection" }` for an NFT of another collection) and no `every`: the NFT waits in the lottery's own stash, which nothing but the draw can empty, and the winner pulls it after the draw. `minSold`: a round that sold fewer is cancelled at its draw and refunded. The ticket money goes where its `split` says (`"treasury"`, the seller's), and `"rest": "treasury"` sends the round's leftover there too: a raffle keeps no pot. A seller raffling their own NFT is the owner of a project of their own (`app.raffle`) |
33
+ | "sell my shield for 3 SOL", "list it at a price", "rent out my mech for a few days", "scholarships" | a listing, made from the app's page at run time, not in xsolana.json: `app.listForSale(asset, { price })`, `app.listForRent(asset, { price, lasts, maxPeriods })`; a buyer `app.buyListing(asset, { listing })`, a renter `app.rentListing(asset, { periods, listing })` (the `listing` the page showed); the app reads who rents it with `app.rentedTo(asset)` (one account read). On-chain gates don't read rentals |
34
+ | "presale", "fair launch", "10% of the supply split by what each put in", "allocation by contribution" | a contest with one side, a `minPot`, a split of 100% to the treasury and a token `prize` with `"split": true`: set aside whole when it is decided (or the decision is refused), each backer's part by what they put in; short of `minPot`, everyone is refunded |
35
+ | "vesting", "cliff then monthly", "locked team tokens", "nobody can take it back", "unlock schedule" | a schedule `{ "does": "pay", "sealed": true, "currency": key, "to": "wallet:<address>", "amount": n, "start": "publish+60d", "every": "30d", "maxRuns": 6, "fund": total, "fundFrom": "mint" }`: its own vault, never edited or stopped by anyone, the last run pays the rest. An ordinary schedule from a pool is only the owner's promise (`W-SCHEDULE-PAY`) |
36
+ | "leave early for a 10% fee", "paper hands penalty", "compound" | a farm of tokens: a tier's `exitFee` (percent) with the farm's `exitFeeTo` (`"burn"`, `"pool:<key>"`, or `"rewards"`: its own rewards vault releases it to those who stay, over the farm's longest lock; a farm whose rewards are its staked token); leaving early keeps what the seat was credited. Compounding is `app.compound(farm)` when the rewards are the staked token, the lock unchanged. A farm of collectibles leaves early by a recipe that lowers its `lockAttribute`. There is no unbonding wait and no harvest tax: say so |
37
+ | "monthly payouts from a list", "each wallet gets a different amount", "points to tokens by list" | a grant of tokens whose `list` entries are `{ "wallet", "amount", "round" }`: each claims its amount once; the same wallet under another `round` claims again |
38
+ | "the tournament winner gets a trophy NFT", "a random NFT for every correct pick" | a contest's `prize`, given to each winning entry when it is claimed, besides its part of the pot: one NFT goes to the first entry claimed (`W-CONTEST-PRIZE`; a contest whose sides are the players has one winner), a roll gives each winner a random one |
39
+ | "auction a mystery box", "bid on a mystery lot" | an auction `lot: { "roll": "<box>" }`: the winner gets a roll of the box; the price goes to the treasury. Say what the box can give, with its odds (`W-AUCTION`) |
40
+ | "the expedition brings back a chest", "missions drop random loot" | a farm tier's `prize`: `{ "gives": "roll", "box": "<box>" }`, rolled when the mission is completed (its lock stayed, its upkeep never ran dry, not forfeited) while the NFT goes home; a known reward (tokens, an item, an NFT) is given as it leaves |
41
+ | "only pass holders can buy it", "members only" | a `gate` of the item alone: `{ "item": "pass" }` |
42
+ | "only holders can open it", "level 5 and up" | a `gate`: `{ "item": "pass", "attribute": "level", "atLeast": 5 }` |
43
+ | "only these players", "just the two of them", "the tournament's entrants" | a `gate` of wallets: `{ "wallets": ["<address>", …] }` (a long list is a `list` gate instead, below) |
44
+ | "stake to enter", "staking tiers for a token sale", "only people who lock 100 of our token can buy" | a farm of a token that nothing funds when staking pays nothing (no `seed`, no share or schedule into `"farm:<key>"`; `W-FARM-DRY` says so), and a `gate` `{ "staked": { "farm": "<farm of a token>", "atLeast": 100 } }`: the wallet's stake in that farm, counted only while its tier's lock runs and from the second after it is made (one stake can't move to another wallet and count again; a tier with no lock never passes, `W-STAKED`). The app passes the seat and the farm |
45
+ | "allowlist", "whitelist of 5,000 wallets", "a snapshot of our holders", "early supporters only" | a `gate` `{ "list": ["<address>", …] }` of any size: the program keeps only its root, the page passes each player's proof (`W-LIST`); a new list is a new root, published again |
46
+ | "rarer pulls for higher levels", "a charm stat improves the odds" | a box's `odds`: `{ "outcome": 3, "when": { "attribute": "charm" }, "add": { "perPointPercent": 10, "upTo": 9 } }` |
47
+ | "stake to earn", "x% house edge to stakers", "fees go to holders" | a `split` share `"to": "farm:<key>"` into a `farms` entry whose `rewards` is that token (its own rewards vault, shared by weight); "1,000 a day to stakers" is a schedule `does: "issue"` `"to": "farm:<key>"`, or a `rate` with `"shared": true` (`perDay` the whole farm's, `halveEvery` to halve it) |
48
+ | "lock for longer, earn more", "30-day lock pays 3x" | a farm's `tiers`: `[{ "lock": "1d", "multiplier": 1 }, { "lock": "30d", "multiplier": 3 }]` |
49
+ | "stake your NFT", "rarer heroes earn more" | a farm with `"stakes": "collectibles"`, `boosts` on an attribute, `maxBoostPercent` |
50
+ | "earn 12 a day per NFT", "daily emissions per NFT", "a fixed rate per NFT" | a farm's `rate`: `{ "perDay": 12, "minted": true }` (the reward a token the project mints), the same for each NFT whoever else stakes; without `minted` it pays from its rewards vault while it lasts. "Only for 30 days per stake" is a tier's `earnsFor`: `"30d"` |
51
+ | "XP", "levels up while staked", "level 5 after a week", "evolve at level 10" | a farm's `growth`: `{ "perDay": 100, "levels": [100, 300, 600] }` writes `xp` and `level` on the NFT for time staked, with nobody's signature; a boost per `level`, a gate, box `odds` or a recipe's `needs` read it (evolve: a recipe that burns the hero and makes the next form, `needs` level 10). `"streak": true` resets it when the NFT leaves |
52
+ | "a mission can fail", "the hero comes back hurt", "the hero can be lost / captured" | the tier's `roll`: a box (`"active": false`, `rollsOn` the mission's NFTs) whose outcomes say what happens: an item or a prize on success, `"shifts": [{ "attribute": "affection", "by": -2 }]` when hurt, `"then": "burn"` (or `"treasury"`, `{ "giveTo": "<wallet>" }`) when lost; `odds` by level make success likelier. The farm then needs freeze or escrow custody for a loss to bite (a watched NFT sold first is simply not rolled) |
53
+ | "enchant: 60% +1 level, 30% -1 level, 10% it breaks", "upgrade can fail" | a box with `rollsOn` (the NFT the player brings) and outcomes with `shifts` and `then`; one that can burn or give the NFT away holds it until the roll settles, so say so |
54
+ | "risky forge", "merge with a chance of failure" | a recipe output `{ "gives": "roll", "box": "<box>", "on": 0 }` (the box `"active": false`): on a kept collectible, or with no `on`, a roll whose outcomes are items or prizes |
55
+ | "merging costs a level", "lose a stat" | a recipe's `raises` with a negative `by`: `{ "place": 0, "key": "level", "by": -1 }` (never below 0) |
56
+ | "missions", "expeditions", "send heroes on a 3-day quest for loot", "random rewards" | a farm tier with a `lock` and a `prize`: `{ "gives": "tokens", "amount": 2, "from": "mint", "currency": "<ticket>" }` (grown by boosts); a box priced in that ticket gives the random loot, its `odds` raised by level or trait. A mission needs a lock (`E-FARM`) |
57
+ | "1 level a day", "+2 affection a day" | a farm's `growth` without `levels`: the points are the attribute itself, `[{ "attribute": "level", "perDay": 1 }, { "attribute": "affection", "perDay": 2 }]` |
58
+ | "a mission open for one week", "a season that ends" | a farm's `runs`: `"7d"` from when it goes live; it takes no stake at its end or after it, nor one whose lock would run past it. A sharing farm keeps what it has not released by the end in its vault (the authority may take back what is free): fund it at least its release span (its longest lock, a day at least) before the end. A mission that pays no tokens has `"rate": { "perDay": 0, "minted": true }`. Released later: add the farm to the spec and publish again |
59
+ | "level 3 to join", "needs level 100 and 6 affection" | a farm's `gate`, one rule or a list the NFT must all pass: `[{ "collection": "<address>", "attribute": "level", "atLeast": 100 }, { …, "attribute": "affection", "atLeast": 6 }]` |
60
+ | "fuel", "food", "upkeep", "drills need fuel to dig", "runs out", "license for 30 days" | a farm of collectibles' `upkeepAttribute` (`"fuel_until"`): a staked NFT earns and grows only until that time, and it runs out by itself; a recipe sells it: `in` the NFT (`use`) and a `pay` line, `raises`: `[{ "place": 0, "key": "fuel_until", "by": "1d", "upTo": "3d", "fromNow": true }]` (a dry tank fills from now; at most 3 days ahead). A recipe reaches an NFT staked in escrow; the play page refreshes its seat in the same transaction. A mission's prize needs the fuel to last its lock. Each new end time costs whoever writes it 44 bytes of rent, not refunded |
61
+ | "leave early for a fee", "early unstake penalty", "break the lock", "recall" | a farm of collectibles' `lockAttribute` (`"free_at"`: the lock is written on the NFT as the time it ends) and a recipe that lowers it for a price: `raises`: `[{ "place": 0, "key": "free_at", "by": "-1d", "fromNow": true }]` with a `pay` line burned, or paid into the farm's rewards pool (released to the seats there over the farm's longest lock, a day at least). Leaving early takes no mission prize. A farm of tokens uses its tier's `exitFee` instead |
62
+ | "jail", "cooldown after a raid", "arrested for 24 hours" | a roll's outcome `"shifts": [{ "attribute": "jailed", "by": "1d", "upTo": "1d", "fromNow": true }]`, and the mission's gate `{ "item": "<hero>", "attribute": "jailed", "time": "passed" }`; on a staked NFT with `lockAttribute`, a shift on that key keeps it in longer |
63
+ | "set bonus", "collect all four elements", "a full set earns more", "bonus for a complete set staked" | a set bound into a badge: a recipe whose `in` takes each piece `{ "takes": "staked", "item": "<piece>", "farm": "<its farm>" }` (the farm it is staked in, escrowed or frozen) and whose `out` is a badge item, with `raises` on each piece `{ "key": "bound", "by": 1, "upTo": 1 }` (one badge a set) and `{ "key": "free_at", "by": "20000d" }` (that farm has `"lockAttribute": "free_at"`, so they stay staked while the badge exists; a raise on a staked piece must move its farm's lock, or the craft is refused); the badge earns in a farm of its own. Unbinding: `in` the badge burned and the pieces `staked` in the same farm, `needs` each `bound` at least 1, `raises` `bound` by -1 and `free_at` by "-20000d", and no `pay` line (one waits while the platform suspends the project, keeping the pieces locked: charge when binding). As many pieces as one transaction carries (`xsolana_check` sizes it). The `elements-set` example |
64
+ | "only with fuel left", "pass still valid" | a rule's `"time": "ahead"` (a time still to come) or `"passed"`; a farm's gate reads a time as the NFT comes in |
65
+ | "the NFT stays in the guild while it goes on the mission", "can still sell it" | `"custody": "watch"` on the mission (and on any farm whose NFTs must stay free); the guild itself may freeze |
66
+ | "my pNFTs", "compressed NFTs", "any NFT" | nothing to add: every route takes Core, Token Metadata (legacy, pNFT) and compressed NFTs |
67
+ | "a badge for everyone who comes", "airdrop an NFT to 20,000 wallets", "POAPs", "mint cNFTs" | the prize `{ "gives": "item", "item": "<badge>", "compressed": true }` (a grant's, a place's, a contest's, a mission's or a recipe's): a compressed NFT of the collectible, no rent each (a Core one is about 0.003 SOL), minted into a tree the setup opens (`compressedTree: { "holds": n }`, 16,384 by default, about 0.23 SOL once). Write it before going live: the setup's collection takes compressed NFTs only if it is made with them in the spec. Not soulbound. The program can mint one as a box's prize at the settle, but the spec's box outcomes don't take `compressed` yet (`xsolana_check` refuses it): give a box's cards as Core collectibles |
68
+ | "blue background gets 10%", "a trait in the metadata" | a boost on the attribute (`{ "when": { "collection": "<address>", "attribute": "Background", "equals": "Blue" }, "add": { "addPercent": 10 } }`) and the spec's `traits` (the root of `loot.traitsTree` of the collection's off-chain metadata): each NFT's traits are proven into its Sheet once |
69
+ | "pay to level up", "train", "upgrade a stat for 100 shards", "max level 10" | a recipe: `in` the collectible kept (`use`) and a `pay` line, and `raises`: `[{ "place": 0, "key": "level", "by": 1, "upTo": 10 }]`. A farm's growth never takes a bought level back down |
70
+ | "upgrade an NFT of another collection in our game", "stats for NFTs from another collection" | a recipe line `{ "takes": "show", "collection": "<address>" }` and `raises` on it: the stats live in the project's Sheet of that NFT (or on the NFT, when its Attributes plugin names the project); the authority imports stats that only exist off the chain with a `set` decision |
71
+ | "earns its power a day", "rate by rarity or stat" | a farm `rate` and a boost `perPointPercent` on the stat: `{ "when": { "attribute": "power" }, "add": { "perPointPercent": 100, "upTo": 100 } }` and `maxBoostPercent` up to 10,000 (x101) |
72
+ | "level-up rewards", "claim a badge at level 15" | an open grant gated by `{ "attribute": "level", "atLeast": 15 }`, `perWallet` and the NFT counted once (show the badge, claim once) |
73
+ | "weekly lottery", "three places", "one grand prize and seven smaller" | `lotteries` with `every: "7d"`, `places: [60, 25, 15]`, and where the places are paid from, which has no default: `"pot": 0`, a pot of its own kept from round to round (`currency`, the project's by default; `seed` fills it at go-live; only its winners are paid from it, so the owner can't drain it by mistake; gifts and what earlier rounds left count), or `"rest": "<destination>"`, each round's own ticket money alone, what the places leave going there |
74
+ | "the pot rolls over", "unclaimed prizes carry to next week", "sponsors can add to the prize" | `"pot": 0`: what a round's places leave stays in the pot, and the next round always starts with it; anything can add to it (`"lottery:<key>"` in a split or a schedule, `seed`) |
75
+ | "each draw pays out only its own tickets", "the rest goes to charity", "nothing carries over" | `"rest": "wallet:<charity>"` (or `"treasury"`, `"burn"`): each round pays its places from its own ticket money, which nothing from outside can change, and sends what they leave there. Never a pool or `"chosen"`; such a lottery takes no `seed` and nothing can pay into it |
76
+ | "a raffle", "one draw at the end of the month" | a lottery with no `every` and a `firstDraw`: it draws once and ends |
77
+ | "x% of tickets go to me" | the lottery's `cutPercent` (a share to the treasury, paid after a successful draw) |
78
+ | "20% of every ticket to the school fund", "the referrer gets 8% of the tickets they bring" | the lottery's `split`: `{ "to": "wallet:<fund>", "percent": 20 }`, `{ "to": "chosen", "percent": 8 }` (the buyer fixes the referrer when they buy; shares are paid after a successful draw) |
79
+ | "max 10 tickets each" | the lottery's `perWallet` (tickets one wallet holds in a round) |
80
+ | "fed by 10% of every flip" | a 10% share in the box's `split` to `"lottery:<key>"`, a lottery with `"pot": 0` |
81
+ | "burn X for Y", "merge two into one", "craft", "breed", "upgrade" | `recipes`: `in` (`burn`, `use`, `renew`, `pay`, `trade`, `staked`, `show`) and `out` (`item`, `tokens`, `share`, `nft`, `roll`) |
82
+ | "trade in any NFT of ours for 10 gold", "swap an NFT of another collection for a badge", "we keep what's traded in and give it away again" | a recipe line `{ "takes": "trade", "collection": "<address>" (left out: the project's own), "then": "burn" \| { "stash": "<key>" } \| { "giveTo": "<wallet>" } }`: the NFT is the price. Into a stash, it becomes another player's prize (a box outcome `{ "gives": "stash" }` resells it) |
83
+ | "a trade-in box: give an NFT, roll for a new one" | a box with `rollsOn` (which NFTs it takes) and `"then": { "stash": "<key>" }` (or `"burn"`): every outcome keeps the NFT paid, and its outcomes give the prizes |
84
+ | "tickets cost an NFT", "sacrifice an NFT for a raffle ticket" | a lottery's `ticketNft`: `{ "rule": { "item": key } or { "collection": address }, "then": "burn" or { "stash": key } }`, with no `price`, `split` or `ticketCurrency`; with `"pot": 0` the pot is its `seed` and what others send it, plus its places' prizes. An NFT spent on a ticket is never refunded (`W-NFT-PRICE`) |
85
+ | "no-loss lottery", "prize savings", "you never lose your ticket", "the sponsor pays the prize" | a lottery with `"noLoss": true` and `"pot": 0`: every ticket (tokens or an NFT) goes back to its buyer once the round is drawn, and the winners take only the pot: its `seed`, a sponsor's payments, shares of other sales (`"lottery:<key>"`). No `split`, `cutPercent` or `ticketCurrency`. Say that tickets are not weighted by time: someone may buy most of them right before the draw and risk only the fees |
86
+ | "stake an NFT on the duel", "winner takes the loser's NFT" | a contest's `stakeNft` (a rule): each entry stakes one NFT; the winners share the losers' NFTs, each picking its share (`losersEach` caps it: 1 for "winner takes one"), and the opener gets what the winners leave. Losers send nothing |
87
+ | "places 2 to 101 each win 1%", "100 runners-up each get a badge" | a lottery place with `times`: `{ "percent": 1, "times": 100 }` is a hundred places alike (not for a named NFT or a roll; at most 64 runs) |
88
+ | "raffle 200 NFTs", "each winner gets a random NFT from the collection" | a lottery that draws once, its `nfts` (put in its own stash at go-live, a group a call) and a place `{ "prize": { "gives": "raffled" }, "times": 200 }`: each winner draws one at random |
89
+ | "bid with NFTs", "the most NFTs wins" | an auction's `bidNfts` (a rule): a bid is Core or Token Metadata NFTs (never compressed), the most wins, `reserve` counts NFTs |
90
+ | "pay with an NFT to enter", "burn an NFT for an attempt" | not a spec part: the page calls `loot.payNftIx` (burned, or given to a wallet) and the app admits by the receipt, as for a payment |
91
+ | "points", "tickets", "shards", "attempts", "sparks" | a token the project mints: a `tokens` entry `source: "project"`, given by a recipe's `out` (`from: "mint"`) or a schedule |
92
+ | "a pass that claims daily", "cooldown" | an item with `life.cooldown`; a recipe that `use`s it and pays `tokens` from a pool |
93
+ | "ready after 7 days", "rots in 3 days", "6 uses" | an item's `life`: `readyAfter`, `lasts`, `uses`. Only recipes read `life`: the kit writes it into each recipe that takes the item, which then refuses one not ready, expired or used up; gates, odds and boosts never do |
94
+ | "monthly membership", "renew", "pass valid until", "expires unless renewed", "a week's access" | a pass item with a time attribute (`paid_until`), a renew recipe (`in`: the pass `use`d and a `pay` line; `raises`: `[{ "place": 0, "key": "paid_until", "by": "30d", "fromNow": true, "upTo": "60d" }]`), and gates that read `{ "item": "pass", "attribute": "paid_until", "time": "ahead" }`. Nothing charges a wallet by itself: the member renews, the page reminds them |
95
+ | "every day mint 5,000 into the pool, halving every 90 days" | a `schedules` entry `does: "issue"`, `every: "1d"`, `halveEvery: 90` |
96
+ | "burn half a percent of the pool daily", "move 5% weekly" | a schedule `does: "burn"` or `does: "move"` with `percent` |
97
+ | "pay the team 100 a week", "a landlord's rent" | a schedule `does: "pay"` from a pool `to` "wallet:<address>" or "holder:<asset>" (the project's keys, or a wallet it pays, run it when due: the page signed by one of them, or their sweep) |
98
+ | "an entry fee", "pay per attempt" | `payments` with `amount` and a `split`. A payment has no gate, no opens/closes, no `"chosen:<item>"` share and no rising price: when the entry needs one, sell it as a box with one outcome that gives nothing (or an item), which has them all. Its split may name a `"chosen"` wallet (a referrer): `app.pay(key, { chosenRecipient })` |
99
+ | "tips", "donations", "pay what you want", "any amount above 0.001" | a payment with `"any": true`: `amount` is the least; the page asks the amount and calls `app.pay(key, { amount })` |
100
+ | "all proceeds to the school", "our DAO's vault gets the money", "the cut goes to the charity" | `"treasury": "wallet:<their address>"`: what every split leaves, and a lottery's cut, go there. Left out, the treasury is the owner's own wallet. The owner can change it later |
101
+ | "several signers must approve spending", "a multisig" | loot isn't a multisig: the money goes to their multisig's vault (for example Squads): `"treasury": "wallet:<vault>"`, or a `wallet:` share; spending from it is approved in the multisig |
102
+ | "60% to me, 40% to my co-designer", "10% to our charity", "split with my partners" | split shares `"to": "wallet:<their wallet address>"` (ask for each address). Going live opens each wallet's account for the currency. Use `account:` only for a token account that already exists |
103
+ | "our partner gets 4%" (one fixed wallet) | a split share `"to": "wallet:<their address>"` |
104
+ | "referral links", "each streamer gets 6% of what they bring", "tip the artist I pick" | a box's, shop item's, lottery's or payment's split share `"to": "chosen"`: the buyer names the wallet when they buy (the page passes it from the link: `app.buy(key, { listing, chosenRecipient: referrer })`, `app.pay(key, { chosenRecipient: referrer })`), never their own (a second wallet of theirs still works, so a referral share is at most a rebate: price it so); with none it goes to the treasury |
105
+ | "a fee to the owner of the workshop I craft at", "the venue's owner gets a cut" | a split share `"to": "chosen:<item key>"`: the buyer names a collectible of that item, and whoever holds it is paid (`{ chosenRecipient: <its address> }`) |
106
+ | "the holder of the deed gets 3%" | a split share `"to": "holder:<collectible address>"` |
107
+ | "duel", "bet on a side", "prediction", "the winner takes the pot" | `contests` with `sides` and a rake in `split`; the authority decides, or a results server's key in `deciders` signs the result (anyone sends it) |
108
+ | "1v1 wager", "both put in 10 and the winner takes it" | a contest with a wallet `gate` of the two players and a `stake` (every entry exactly that) |
109
+ | "crowdfund", "all or nothing", "refund everyone if we miss the goal" | a contest with `sides: 1`, a `minPot` (the goal), and a `split` of 100% to `"wallet:<recipient>"`. Short of the goal when entries close, it is off by itself and every backer claims their pledge back (anyone can send those claims). Reached: the owner (or a decider) names side 0 and the whole pot goes to the recipient. A badge for each backer: a grant whose `list` is the backers |
110
+ | "the game decides who cleared the level and pays them" | `decisions`: `does: "payout"` from a pool, with `when` in words |
111
+ | "level up my pet", "write the score on the NFT" | a decision `does: "set"` (the authority rewrites attributes) |
112
+ | "send caravans on trade runs, the game says how they went", "the event ends at 18:00" | `holds` (the item, `for` a time the player picks, or `until` a set time) and an `award` decision |
113
+ | "quests that pay", "airdrop to these wallets", "one per person" | `grants`: a `prize` (an item, tokens, an NFT, or a roll); a `list` of wallets, or `vouchers` (a results server's public key: see `after-launch.md`); `limits`. A claim of a prize it can't give is refused and keeps its round |
114
+ | "rewards by rank", "1st gets 400, 2nd 250", "points to tokens", "each winner a different amount" | one grant whose `prize` is a fixed amount of tokens (the most any claim takes: 1st place's) paid by `vouchers` or a list: each voucher (or list leaf) names that claim's own amount, up to the grant's (`signVoucher({ …, amount })`, `app.claimGrant(key, { voucher })`); none named pays the grant's amount. A `list` written in xsolana.json does the same: each entry `{ wallet, amount }` names its own amount |
115
+ | "at most once a day each", "2 wins a day per wallet" | a grant's `limits.perWalletEach`: `{ "count": 1, "every": "1d" }` |
116
+ | "promo codes on flyers", "a code pays once", "QR claim links" | a grant with `keys` (`codeAddresses(makeCodes(n))`: 80-bit codes, their public keys only) and `limits.perKey: 1`; the page signs a voucher from the typed code for the player's own wallet (`codeVoucher`) |
117
+ | "one per person", "no farming with alt wallets", "a daily reward bots can't drain" | `vouchers` from the game's server with `limits.perPerson` (it names a person hash from an outside sign-in) and `perDay`; or a `gate` on holding something the game sells; never an open grant of anything tradeable without one (`after-launch.md`, "A wallet is not a person") |
118
+ | "holders of the ticket claim a free badge once" | an open grant: no `vouchers`, `keys` or `list`, a `gate` of the ticket, and `limits.perWallet: 1` (each ticket shown claims once too) |
119
+ | "bring your own NFT", "any NFT of a partner collection can play", "holders of X get" | a `gate` (or a farm's, or odds') rule with `collection`: the collection's address; the project's notes about each NFT (a level) go in its Sheet |
120
+ | "our own token, 10 million, we keep it", "a fixed supply", "a community token" | `source: "project"` with `supply`: going live mints all of it to the owner and turns minting off for good; pools are seeded from it |
121
+ | "our token", "the coin we already launched", "this mint" | `source: "existing"` with `mint` and `decimals` (ask for the address). loot doesn't put a token on a market: for trading, launch it on a token launchpad or an exchange first |
122
+ | "Token-2022", "a transfer fee" | `program: "token-2022"` and `powers.transferFeePercent` |
123
+ | "tokens only, no NFTs" | `"collection": false`: no collection is made (no collectibles; normal items and tokens work) |
124
+ | "mint into our existing collection", "two collections" | `collections`: Core collections the owner's wallet already controls, each with the project added as an update delegate; an item's `collection` names the one it is minted into |
125
+ | "a second game or community from the same wallet" | `projectNumber`: 1, 2, … (the first is 0); each number is a project of its own |
114
126
 
115
127
  Ask, in your first reply, for the numbers that define the product if the person hasn't given them: prices, supply, odds
116
128
  or multipliers, and how much SOL they can put in (their budget sets every seed). Don't wait for the answers to build:
@@ -123,11 +135,10 @@ When loot can't do exactly what they asked, follow SKILL.md rule 9: `xsolana_exa
123
135
  words>"}` returns the limit and its nearest versions. Offer them with their costs, build the one they pick, and when
124
136
  they push back offer the next one. Don't stop at "it doesn't meet your requirement".
125
137
 
126
-
127
138
  ## Worked example
128
139
 
129
- The person: *"A fishing game: a cast costs 0.02 SOL. Most casts catch nothing, some land a Perch to keep, and 1 cast
130
- in 50 hooks the Golden Carp, worth 0.5 SOL. 5% of every cast goes to a monthly prize draw."*
140
+ The person: _"A fishing game: a cast costs 0.02 SOL. Most casts catch nothing, some land a Perch to keep, and 1 cast
141
+ in 50 hooks the Golden Carp, worth 0.5 SOL. 5% of every cast goes to a monthly prize draw."_
131
142
 
132
143
  - **Pays**: a cast (a box). **Gets**: nothing (68%), a Perch collectible (30%), or 0.5 SOL from a pool (2%).
133
144
  **Money goes**: 5% to the draw's pot; they didn't say where the rest goes, so the draft sends 55% to the pond that
@@ -145,16 +156,32 @@ in 50 hooks the Golden Carp, worth 0.5 SOL. 5% of every cast goes to a monthly p
145
156
  "tokens": [{ "key": "sol", "source": "sol" }],
146
157
  "items": [{ "key": "perch", "name": "Perch", "uri": "https://example.com/perch.json", "active": false }],
147
158
  "pools": [{ "key": "pond", "currency": "sol", "seed": 2 }],
148
- "boxes": [{
149
- "key": "cast", "name": "Cast", "price": 0.02,
150
- "split": [{ "to": "pool:pond", "percent": 55 }, { "to": "lottery:monthly", "percent": 5 }],
151
- "outcomes": [
152
- { "gives": "nothing", "chance": 68 },
153
- { "gives": "item", "item": "perch", "chance": 30 },
154
- { "gives": "pay", "pool": "pond", "amount": 0.5, "chance": 2 }
155
- ]
156
- }],
157
- "lotteries": [{ "key": "monthly", "name": "Monthly draw", "price": 0.01, "places": [100], "every": "30d", "firstDraw": "publish+30d" }],
159
+ "boxes": [
160
+ {
161
+ "key": "cast",
162
+ "name": "Cast",
163
+ "price": 0.02,
164
+ "split": [
165
+ { "to": "pool:pond", "percent": 55 },
166
+ { "to": "lottery:monthly", "percent": 5 }
167
+ ],
168
+ "outcomes": [
169
+ { "gives": "nothing", "chance": 68 },
170
+ { "gives": "item", "item": "perch", "chance": 30 },
171
+ { "gives": "pay", "pool": "pond", "amount": 0.5, "chance": 2 }
172
+ ]
173
+ }
174
+ ],
175
+ "lotteries": [
176
+ {
177
+ "key": "monthly",
178
+ "name": "Monthly draw",
179
+ "price": 0.01,
180
+ "places": [100],
181
+ "every": "30d",
182
+ "firstDraw": "publish+30d"
183
+ }
184
+ ],
158
185
  "scenario": { "days": 30, "perDay": { "box:cast": 500, "lottery:monthly": 20 } }
159
186
  }
160
187
  ```