diff --git a/cattown/SKILL.md b/cattown/SKILL.md index 2788fee3..fcf5398b 100644 --- a/cattown/SKILL.md +++ b/cattown/SKILL.md @@ -1,6 +1,6 @@ --- name: cattown -description: Interact with Cat Town — a Farcaster-native game world on Base. Covers KIBBLE staking (stake, claim, unlock, unstake, leaderboard, deposit history); live world state (season, weather, time of day, weekend flag); fishing drops filtered by world state; Isabella's weekend fishing competition with live prize-pool math; Paulie's weekly fish raffle (free-ticket claim, tier-based prize pool, odds, last winners); the daily 3-item boutique with KIBBLE→USD conversion; gacha spins (async VRF pay-then-receive, 100/day cap, seasonal pools); item valuation plus batch selling via the V2 vendor (5% tax); and KIBBLE tokenomics (% burned, % staked, live APY). Use when the user mentions Cat Town, KIBBLE, Wealth & Whiskers, Jasper, Isabella, Paulie, Skipper, Theodore, Cassie, RevenueShare, fishing, gacha, raffle, boutique, vendor, prize pools, drop tables, or any read/write on the Cat Town contracts. +description: Interact with Cat Town — a Farcaster-native game world on Base. Covers KIBBLE staking (stake, claim, unlock, unstake, leaderboard, deposit history); live world state (season, weather, time of day, weekend flag); fishing drops filtered by world state; Isabella's weekend fishing competition with live prize-pool math; Paulie's weekly fish raffle (free-ticket claim, tier-based prize pool, odds, last winners); the daily 3-item boutique — read rotation, KIBBLE→USD conversion, and **buy items** (most priced in KIBBLE, collab items in other ERC-20s like DOTA); gacha spins (async VRF pay-then-receive, 100/day cap, seasonal pools); item valuation plus batch selling via the V2 vendor (5% tax); and KIBBLE tokenomics (% burned, % staked, live APY). Also recognises requests about incantations / vouchers / the Mystic Study and redirects them to the in-game claim page (not currently agent-claimable). Use when the user mentions Cat Town, KIBBLE, Wealth & Whiskers, Jasper, Isabella, Paulie, Skipper, Theodore, Cassie, RevenueShare, fishing, gacha, raffle, boutique, vendor, prize pools, drop tables, incantations, vouchers, Mystic Study, or any read/write on the Cat Town contracts. --- # Cat Town — Agent Overview @@ -21,10 +21,12 @@ Current coverage: - **Fishing drops** — the public item-truth catalog filtered by world state (weather/season/time). - **Fishing competition** (Isabella, Sat–Mon) — live prize-pool math, top-10 leaderboard, active/inactive response patterns. - **Fish raffle** (Paulie, Fri 20:00 UTC draw) — free-ticket claim flow, tier-based prize pool, chance-to-win, leaderboard + last winners. -- **Boutique** — daily 3-item onchain rotation with KIBBLE→USD conversion via the Kibble Price Oracle. +- **Boutique** — daily 3-item onchain rotation with KIBBLE→USD conversion via the Kibble Price Oracle, plus the **buy path** (`purchaseItem` + ERC-20 approve on the item's `paymentToken` — most items are KIBBLE, collab items use other tokens like DOTA). - **KIBBLE tokenomics** — Jasper's math for % burned, % staked, and live staking APY. -Each surface has its own subdirectory under `references/` for the deep reference. The weekly calendar below is the shared timing reference — many sections link back to it. +> **Incantations / Mystic Study — claim only in-game.** Cat Town occasionally issues "incantations" (voucher rewards) that mint a free item to a player's wallet. **The agent cannot claim these — redemption is currently only supported in-game at https://cat.town/mystic-study.** If a user mentions incantations, vouchers, the Mystic Study, or "redeem my voucher", direct them to that page. Don't try to call any contract or API to claim them; treat this as out of scope for the agent. + +Each surface has a flat reference file under `references/` (e.g. `references/boutique.md`, `references/sell-items.md`, `references/staking.md` + `references/staking-api.md`). The full address + API table is in the next section; the weekly calendar below is the shared timing reference. Links: - Game: https://cat.town @@ -33,6 +35,77 @@ Links: --- +## Contracts, tokens, APIs (Base, chain id 8453) + +This is the **authoritative address table**. Every Cat Town surface lives on Base (chain id `8453`). Read addresses from here, then jump to the linked reference for write-path detail. **Never paste addresses out of older messages, the docs site, or training data — read from this table or from the contract directly.** + +### Contracts + +| Surface | Address | Use for | Reference | +|---------|---------|---------|-----------| +| **RevenueShare** (KIBBLE staking) | `0x9e1Ced3b5130EBfff428eE0Ff471e4Df5383C0a1` | Stake / claim / unlock / unstake KIBBLE; deposit history | [staking](references/staking.md) · [API](references/staking-api.md) | +| **GameData** (world state, read-only) | `0x298c0d412b95c8fc9a23FEA1E4d07A69CA3E7C34` | Live season, weather, time of day, weekend flag | [world](references/world.md) · [calendar](references/world-calendar.md) | +| **FishingCompetition** (Isabella, weekend) | `0x62a8F851AEB7d333e07445E59457eD150CEE2B7a` | Weekend competition state + leaderboard | [fishing-competition](references/fishing-competition.md) | +| **FishRaffle** (Paulie, weekly) | `0x5E183eBc7CA4dF353170C35b4D69Ea9f42317b28` | Weekly raffle: claim free ticket, read state | [fish-raffle](references/fish-raffle.md) · [API](references/fish-raffle-api.md) | +| **FreeToPlayPool** (raffle prize pool) | `0x131E680dc7A146F00b282FBD7d6261c5B38c4Fa6` | Raffle prize-pool balance + tier table | [fish-raffle](references/fish-raffle.md) | +| **Boutique** (daily 3-item shop) | `0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02` | Read rotation + buy items (per-item paymentToken) | [boutique](references/boutique.md) | +| **GachaMachine** (capsule pulls) | `0xAD0ee945B4Eba7FB8eB7540370672E97eB951F1a` | Async-VRF capsule pulls; 100/day cap per wallet | [gacha](references/gacha.md) · [API](references/gacha-api.md) | +| **SellItems** (a.k.a. **Supermarket** / "V2 vendor") | `0x49936db5Dcbc906D682CFa2dcfAb0788e3ee5808` | Sell V2-minted Treasures/Collectibles for KIBBLE (5% fee) | [sell-items](references/sell-items.md) | +| **V2 Minter** (ERC-1155, all Cat Town items) | `0x7b65ec82cB4600Bc1dCc5124a15594976f19eA14` | The 1155 contract that holds **every Cat Town item** — gacha pulls, fishing drops, boutique mints. For any Cat Town sell, this is the `nftContract` arg. **Even items themed after partner projects (e.g. "Songbirdz Owl") live here, not on the partner's NFT contract.** | [sell-items](references/sell-items.md) | +| **Kibble Price Oracle** | `0xE97B7ab01837A4CbF8C332181A2048EEE4033FB7` | KIBBLE→USD, ETH→USD, KIBBLE→ETH. ⚠️ KIBBLE-only — does **not** price DOTA / collab tokens. | [boutique § paymentToken→USD](references/boutique.md) | +| Legacy V1 staking (deprecated) | `0xc3398Ae89bAE27620Ad4A9216165c80EE654eE96` | Do not send new stakes here. | [staking](references/staking.md) | + +### Tokens + +| Symbol | Address | Decimals | Notes | +|--------|---------|----------|-------| +| **KIBBLE** | `0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb` | 18 | Game's primary currency. ⚠️ `RevenueShare.stake/unstake` take **integer KIBBLE**, not wei — see CRITICAL section below. The token's `approve()` is still standard ERC-20 wei. | +| **DOTA** ("Defense of the Agents") | `0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07` | 18 | Used by "Friends of Cat Town" collab boutique items (e.g. Rat Skull Charm). | +| **BARON** | `0x89CD293538C2390992CDFb3520cFb136748CD9B9` | 18 | Frontend prices it via the KIBBLE/BARON Uniswap V2 pool. | +| **USDC** (Base) | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | 6 | Dollar-pegged; `usd = price / 10^6`. | +| **cbBTC** | `0xcbB7C0000aB88B473b1f5aFd9ef808440eed33Bf` | 8 | Coinbase BTC. Frontend prices via Chainlink BTC/USD. | +| **cbETH** | `0x2Ae3F1Ec7F1F5012CFEab0185bfc7aa3cf0DEc22` | 18 | Coinbase ETH. Frontend prices via Chainlink. | +| **WETH** (Base) | `0x4200000000000000000000000000000000000006` | 18 | Wrapped ETH; surfaces in DEX pairs. | + +### Public APIs (`https://api.cat.town`, no auth) + +| Endpoint | Use | Reference | +|----------|-----|-----------| +| `GET /v2/items/master?limit=1000` | Full catalog: fishing, gacha, boutique items + traits + sellValue. Filter by `source` and `dropConditions`. | [fishing-drops](references/fishing-drops.md) | +| `GET /v2/inventory//paginated?hasSellValue=true&sortBy=kibble&sortOrder=desc` | Sellable items in a wallet. `hasSellValue=true` filters out unsellable types. | [sell-items](references/sell-items.md) | +| `GET /v2/items/capsule/` | Gacha NFTs the wallet currently holds. Polling target after a spin. Returns 500 on cold wallets — treat as empty. | [gacha-api](references/gacha-api.md) | +| `GET /v2/revenue/staking/leaderboard` | Ranked stakers + pool share. | [staking-api](references/staking-api.md) | +| `GET /v2/revenue/deposits/` | Per-wallet historical fishing/gacha revenue deposits. | [staking-api](references/staking-api.md) | +| `GET /v1/fishing/competition/leaderboard` | Live + most-recent-completed weekend competition. | [fishing-competition](references/fishing-competition.md) | +| `GET /v1/tickets/leaderboard` | Current raffle round buyers. | [fish-raffle-api](references/fish-raffle-api.md) | +| `GET /v1/tickets/winners` | Most recent completed raffle draw. | [fish-raffle-api](references/fish-raffle-api.md) | + +### External price source (non-KIBBLE collab tokens) + +| Endpoint | Use | +|----------|-----| +| `GET https://api.dexscreener.com/latest/dex/tokens/` | USD price for DOTA / partner tokens. Filter `chainId=base`, sort `liquidity.usd` DESC, use top pool's `priceUsd`. No auth. | + +--- + +## Approvals — what to approve, where, in what unit + +Wrong-token / wrong-spender / wrong-unit approvals are the #1 reason write txs revert. **Always read the current allowance / approval state first; only submit an approval tx if it's insufficient.** + +| Action | Approve which token? | Approval call | Spender (= contract you're calling) | Read to check first | +|--------|----------------------|---------------|-------------------------------------|---------------------| +| Stake / unstake KIBBLE | KIBBLE | `KIBBLE.approve(revenueShare, N × 10^18)` (wei) | RevenueShare `0x9e1C...0a1` | `KIBBLE.allowance(user, revenueShare)` | +| Buy boutique item | **`item.paymentToken`** (per-item! KIBBLE / DOTA / etc.) in **that token's** wei | `paymentToken.approve(boutique, price)` | Boutique `0xf984...a02` | `paymentToken.allowance(user, boutique)` | +| Spin gacha | KIBBLE | `KIBBLE.approve(gacha, N × 10^18)` | GachaMachine `0xAD0e...F1a` | `KIBBLE.allowance(user, gacha)` | +| Sell items to vendor | **V2 Minter** (1155) via `setApprovalForAll` (one-time per wallet) | `V2Minter.setApprovalForAll(supermarket, true)` | SellItems / Supermarket `0x4993...808` | `V2Minter.isApprovedForAll(user, supermarket)` | +| Claim raffle free ticket | nothing | — | FishRaffle `0x5E18...b28` | — | +| Claim staking rewards | nothing | — | RevenueShare | — | +| Unlock / relock staking | nothing | — | RevenueShare | — | + +**The approval target is almost never the same contract as the action target.** If you reflexively approve KIBBLE for a sell-to-vendor, or approve KIBBLE for a DOTA-priced boutique item, the tx reverts on the internal `transferFrom`. The "spender" column is who pulls tokens during the action; that's who needs the allowance. + +--- + ## Weekly Calendar (all times UTC) Cat Town runs on a fixed weekly cadence. Use these timings when setting user expectations ("your next fishing drop is Monday") or scheduling follow-ups. @@ -46,7 +119,7 @@ Cat Town runs on a fixed weekly cadence. Use these timings when setting user exp | Friday | **Fish raffle draw** | 20:00 | Paulie | No | | Sat–Sun | **Weekly fishing competition** | Sat morning → Sun night | Isabella | Indirect* | -*During the weekend fishing competition (Sat–Sun), 10% of every fish identification feeds the KIBBLE stakers pool. Weekday fishing (Skipper) does **not** feed stakers. This is why weekend activity sizes the following Monday's fishing-revenue deposit. See [references/world/calendar.md](references/world/calendar.md) for the full revenue split. +*During the weekend fishing competition (Sat–Sun), 10% of every fish identification feeds the KIBBLE stakers pool. Weekday fishing (Skipper) does **not** feed stakers. This is why weekend activity sizes the following Monday's fishing-revenue deposit. See [references/world-calendar.md](references/world-calendar.md) for the full revenue split. Deposits are triggered by the Cat Town backend calling `depositRevenue(amount, source)` on RevenueShare, with `source` in `"fishing"` or `"gacha"`. Watch the `RevenueDeposited(string source, uint256 depositTimestamp, uint256 depositAmount, uint256 newAccRewardPerShare)` event to know the exact moment a drop lands. @@ -91,7 +164,7 @@ The second form reverts with `ERC20: transfer amount exceeds balance` because th - **RevenueShare**: `0x9e1Ced3b5130EBfff428eE0Ff471e4Df5383C0a1` - **KIBBLE token (ERC-20, 18 decimals)**: `0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb` -Base Sepolia addresses and the full ABI surface are in [references/staking/contract.md](references/staking/contract.md). User-facing overview: https://docs.cat.town/economy/staking. +Base Sepolia addresses and the full ABI surface are in [references/staking.md](references/staking.md). User-facing overview: https://docs.cat.town/economy/staking. ### Core flows @@ -173,7 +246,7 @@ KIBBLE-denominated reads return **whole KIBBLE** (not wei). See the Amount units | `LOCK_PERIOD()` | seconds | Unlock wait duration | | `accRewardPerShare()` | accumulator × 1e18 | Global reward accumulator | -Full function-by-function reference: [references/staking/contract.md](references/staking/contract.md). +Full function-by-function reference: [references/staking.md](references/staking.md). ### KIBBLE circulating supply — always subtract the burn address @@ -202,7 +275,7 @@ Two public JSON endpoints on `https://api.cat.town`, **no auth required**. Use t - `GET /v2/revenue/staking/leaderboard` — ranked stakers with stake amount and pool-share %. - `GET /v2/revenue/deposits/{address}` — one user's historical `fishing` / `gacha` deposits, per-tx amounts, and the share that landed for that user. -Full shapes, field meanings, and example responses: [references/staking/api.md](references/staking/api.md). +Full shapes, field meanings, and example responses: [references/staking-api.md](references/staking-api.md). --- @@ -220,9 +293,9 @@ The one call you usually want is **`getGameState()`** → `(season, timeOfDay, i World state drives fishing and gacha drop tables — different fish appear in different weather/seasons. Fishing drop tables are documented in the **Fishing drops** section below; gacha pools are planned for a future revision. -Full function table, selectors, raw calldata, live sample response, and historical-lookup fns (`getSeasonForDate`, `getWeatherForDate`): [references/world/contract.md](references/world/contract.md). +Full function table, selectors, raw calldata, live sample response, and historical-lookup fns (`getSeasonForDate`, `getWeatherForDate`): [references/world.md](references/world.md). -For the fixed weekly cadence (fishing/gacha revenue deposits, Paulie's raffle, Isabella's weekend competition), see [references/world/calendar.md](references/world/calendar.md). +For the fixed weekly cadence (fishing/gacha revenue deposits, Paulie's raffle, Isabella's weekend competition), see [references/world-calendar.md](references/world-calendar.md). --- @@ -294,7 +367,7 @@ Per-species fish weight ranges are **not** returned by `/v2/items/master`. If a For quick programmatic answers, lean on rarity + `sellValue`. For "what's the biggest {species}", point the user at those docs pages. -Full recipe, complete weather→drops table, and live-sweep counts: [references/fishing/drops.md](references/fishing/drops.md). Player-facing context: https://docs.cat.town/fishing/start-fishing, https://docs.cat.town/fishing/hot-streaks, https://docs.cat.town/fishing/upgrades. +Full recipe, complete weather→drops table, and live-sweep counts: [references/fishing-drops.md](references/fishing-drops.md). Player-facing context: https://docs.cat.town/fishing/start-fishing, https://docs.cat.town/fishing/hot-streaks, https://docs.cat.town/fishing/upgrades. --- @@ -357,7 +430,7 @@ Pull the API response once, then pick 3–5 of these to feature (keep it convers > > Want the full top 10? -Full ABI surface, per-rank payout worked example at current oracle rate, and the complete leaderboard response shape: [references/fishing/competition.md](references/fishing/competition.md). Player-facing overview: https://docs.cat.town/fishing/weekly-competition. +Full ABI surface, per-rank payout worked example at current oracle rate, and the complete leaderboard response shape: [references/fishing-competition.md](references/fishing-competition.md). Player-facing overview: https://docs.cat.town/fishing/weekly-competition. --- @@ -370,13 +443,44 @@ The boutique is a fully onchain daily shop. Every day at **00:00 UTC** the Bouti - **Boutique**: `0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02` - **Kibble Price Oracle** (for USD conversion): `0xE97B7ab01837A4CbF8C332181A2048EEE4033FB7` +### ⚠️ Buy path: each item carries its own `paymentToken` — approve THAT token + +Most boutique items are priced in **KIBBLE**, but partnership / collab items use other ERC-20s. Live example: today's **Rat Skull Charm** ("Friends of Cat Town" collab) is priced in **DOTA** ("Defense of the Agents", `0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07`), not KIBBLE. The contract pulls the price from `msg.sender` in **whichever token the item specifies** via `ShopItemView.paymentToken`. Read it per item — never assume KIBBLE. + +If you reflexively approve KIBBLE for a DOTA-priced item, the buy reverts on the internal `transferFrom` because the contract is pulling DOTA, not KIBBLE. + ### Primary read — `getTodaysRotationDetails()` -Single call returns today's 3 items as `ShopItemView[]`. Each item carries `price` (in KIBBLE **wei**, divide by `10^18`), `stockRemaining`, `maxSupply`, `isPurchasableNow`, and a `traitNames`/`traitValues` parallel pair that encodes Name, Rarity, Slot, Image. Parse those into a dict to render. +Single call returns today's 3 items as `ShopItemView[]`. Each item carries `price` (in **`paymentToken` wei** at that token's decimals), `paymentToken` address, `stockRemaining`, `maxSupply`, `isPurchasableNow`, and a `traitNames`/`traitValues` parallel pair that encodes Name, Rarity, Slot, Collection, Image. Parse those into a dict to render. **Never assume `price` is in KIBBLE wei — read `paymentToken` and use that token's decimals.** + +### ⚠️ paymentToken → USD: the Kibble Price Oracle ONLY works for KIBBLE-priced items + +This is the same trap as the approval one — reflexively reaching for `getKibbleUsdPrice()` produces wildly wrong USD for non-KIBBLE items. Real failure mode caught in production: 1.5M DOTA × KIBBLE-rate (~$0.00095) = ~$1,420 quoted, when the actual DOTA market price puts it at **~$2.17**. Off by ~700×. + +Branch on `paymentToken` first: + +``` +if paymentToken == KIBBLE (0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb): + usd = (price * getKibbleUsdPrice()) / 10^36 # KIBBLE oracle is valid here +elif paymentToken == USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913): + usd = price / 10^6 # dollar-pegged +else: # DOTA, BARON, cbBTC, future collabs + usd = (price / 10^decimals) * dexscreener_usd_price(paymentToken, chain="base") + # if DEX lookup fails or low confidence: skip the USD readout entirely +``` + +For non-KIBBLE collab tokens, hit Dexscreener (no auth): + +``` +GET https://api.dexscreener.com/latest/dex/tokens/ + → filter pairs where chainId == "base" + → sort by liquidity.usd DESC + → use pairs[0].priceUsd +``` -### KIBBLE → USD conversion (the game UI doesn't do this — we should) +**Sanity check before quoting USD:** rare boutique items typically land $5–$15, epic $15–$40, legendary $40–$100+. If your computed USD is orders of magnitude off-band (a Rare item at $1,000+, a Legendary at $0.01), the price source is wrong — almost always the Kibble oracle was applied to a non-KIBBLE amount. Re-check `paymentToken` and re-route. If you can't price a token confidently, **omit the USD readout** rather than quote a wrong one — users trust whatever number you show. -The in-game boutique shows KIBBLE prices only. To give users a USD readout, read the Kibble Price Oracle: +#### KIBBLE oracle (only for KIBBLE-priced items) - `getKibbleUsdPrice()` → `uint256` USD per 1 KIBBLE, scaled by **`10^18`** (not 1e8 — **don't confuse with `getEthUsdPrice()` which is `10^8` Chainlink style**). - Formula: `usd_value = (price_wei * rawKibbleUsdPrice) / 10^36` @@ -384,21 +488,23 @@ The in-game boutique shows KIBBLE prices only. To give users a USD readout, read ### Response pattern — "what's in the boutique today?" -1. Parallel reads: `getTodaysRotationDetails()` + `getKibbleUsdPrice()`. -2. For each of the 3 items: parse the trait arrays (Name/Rarity/Slot), compute KIBBLE and USD price, check stock. -3. Sort big-ticket first — **rarity DESC** (Legendary → Common), then **KIBBLE price DESC**, then name ASC. +1. Parallel reads: `getTodaysRotationDetails()` + `getKibbleUsdPrice()`. For any item whose `paymentToken` is not KIBBLE / USDC, also fetch its DEX price via Dexscreener. +2. For each of the 3 items: parse the trait arrays (Name/Rarity/Slot/Collection), compute token amount (using `paymentToken` decimals) and USD via the branch above, check stock. +3. Sort big-ticket first — **rarity DESC** (Legendary → Common), then **USD price DESC** (cross-token comparable; don't sort by raw token amount, since 1M of one token can be worth less than 100 of another), then name ASC. 4. Flag `stockRemaining == 0` as "Sold Out"; otherwise format as `"{stockRemaining} of {maxSupply} remaining"` — **stockRemaining first, maxSupply second**. Sanity check: if your first number is larger than the second, you've swapped them — reread the struct fields. `stockRemaining` can never exceed `maxSupply`. 5. Open the reply with the current season; close with the matching `docs.cat.town/boutique/…-fashion` link for fuller context. The collection name (e.g. `"Spring Fashion"`) is on the item itself as the **`Collection`** trait — surface it at the top of the reply so the user knows which collection is currently rotating. -Example reply (real data from today's rotation) — note the **"N of M remaining"** phrasing: +Example reply (live rotation as of writing — mixed-currency, with the **"N of M remaining"** phrasing and per-token USD): -> **Boutique today — Spring Fashion collection:** +> **Boutique today — mixed Spring Fashion and Friends of Cat Town collab:** +> +> 1. **Striking Baseball Cap** — Legendary Hat (Spring Fashion) — **50,000 KIBBLE (~$47)** — 1 of 1 remaining +> 2. **Cherry Neckerchief** — Epic Neck (Spring Fashion) — **25,000 KIBBLE (~$24)** — 1 of 1 remaining +> 3. **Rat Skull Charm** — Rare Neck (Friends of Cat Town collab) — **1,500,000 DOTA (~$2)** — 64 of 100 remaining > -> 1. **White Longsleeve** — Rare Body — **12,500 KIBBLE (~$11.86)** — 1 of 1 remaining -> 2. **Royal Blue Varsity** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 2 of 2 remaining -> 3. **Classic Academic Blouse** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 1 of 2 remaining +> Note the third item is priced in **DOTA** (`0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07`), not KIBBLE. To buy it, approve DOTA → Boutique, not KIBBLE. > > Browse the other seasonal collections: > - Spring: https://docs.cat.town/boutique/spring-fashion @@ -409,9 +515,61 @@ Example reply (real data from today's rotation) — note the **"N of M remaining Include all four season links in every response — a user interested in the current collection will often want to peek at others. -Full ABI surface, trait schema (real keys: `Item Name`, `Rarity`, `Item Type`, `Source`, `Slot`, `Sprite`, `imageUrl`, `Collection`, etc.), preview future rotations, and the complete oracle math: [references/boutique/contract.md](references/boutique/contract.md). +### Buying an item + +Single-item purchase per tx — the contract has no batch buy. Mint is **synchronous** (unlike gacha's async VRF mint): the same tx pulls payment to the treasury and mints the NFT to the buyer. + +``` +Boutique.purchaseItem(uint256 itemId) external nonReentrant whenNotPaused returns (uint256 mintedTokenId) +``` + +`itemId` is the **plain rotation id** from `getTodaysRotation()` (live as of writing: 208, 173, 196 — these change daily, always re-read). **Not** wei-scaled, **not** payable — do not send `msg.value`. + +Preconditions, in order: + +1. **`paused()` == false** — modifier is `whenNotPaused`; the tx reverts otherwise. One read protects against a town-wide pause. +2. **`canPurchaseItem(itemId)` → `(bool, string reason)`** — view-only preflight. If false, surface `reason` verbatim. The exact strings the contract returns: `"Item does not exist"`, `"Item is not active"`, `"Item is out of stock"`, `"Item not available yet"`, `"Item no longer available"`, `"Item not available this season"`, `"Item not in today's rotation"`. (Note: `canPurchaseItem` does **not** check `paused()` — that's why step 1 is separate.) +3. **`paymentToken.balanceOf(user) >= price`** — `price` is in `paymentToken`'s native unit. KIBBLE / DOTA / BARON = 18 decimals; USDC = 6; cbBTC = 8. If short, offer a swap *into* `paymentToken` via `trails` or `symbiosis`. cat.town's UI sends DOTA buyers straight to Uniswap (`https://app.uniswap.org/swap?…outputCurrency=0x5f09821cbb61e09d2a83124ae0b56aaa3ae85b07`). +4. **`paymentToken.approve(boutique, price)`** if `allowance(user, boutique) < price`. **`paymentToken` is the per-item one — read it from `ShopItemView`. NEVER assume KIBBLE.** The spender is the Boutique address even though the tokens flow through to a `treasury`. + +Then `purchaseItem(itemId)` mints the NFT to `msg.sender` and emits `ItemPurchased(buyer, itemId, mintedTokenId, paymentToken, price)`. `mintedTokenId` is the V2-minter token id the user now owns. + +If `purchaseItem` reverts, you'll see a Solidity **custom error** (4-byte selector, no message) — `ItemNotFound()`, `ItemNotActive()`, `ItemOutOfStock()`, `ItemNotAvailableYet()`, `ItemNoLongerAvailable()`, `ItemNotAvailableThisSeason()`, `ItemNotInDailyRotation()`, or `EnforcedPause()`. Friendly strings only come from `canPurchaseItem` — that's why the preflight matters. + +#### Never compute the price — read it + +`ShopItemView.price` is the literal token amount in `paymentToken` wei. Read it from the contract; do **not** infer it from doc snippets, the docs site, or prior conversation. Prices change (admin-updated) and currencies vary per item. The only correct flow is: read `getShopItem(itemId)` (or pull the item out of `getTodaysRotationDetails()`), use `paymentToken` and `price` exactly as returned, divide `price` by the right decimals for display. + +#### Example — Rat Skull Charm (DOTA collab, live as of writing) + +``` +itemId = 208 +paymentToken = 0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07 // DOTA ("Defense of the Agents"), 18 decimals +price = 1,499,999,999,999,999,974,834,176 wei = 1,500,000 DOTA + (read live from ShopItemView.price — do not hardcode) + +0. paused() == false +1. canPurchaseItem(208) // expect (true, "") +2. DOTA.balanceOf(user) >= price // else swap +3. DOTA.approve(boutique, price) // if allowance < price +4. Boutique.purchaseItem(208) // returns mintedTokenId +``` + +#### Example — KIBBLE-priced item (the common case) + +Live as of writing: **Striking Baseball Cap** (`itemId = 173`, Legendary Hat) at **50,000 KIBBLE**, stock 1 of 1; **Cherry Neckerchief** (`196`, Epic Neck) at **25,000 KIBBLE**, stock 1 of 1. Same recipe as DOTA, but step 3 is `KIBBLE.approve(boutique, price)` against the KIBBLE token (`0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb`). + +#### Bankr execution + +Natural-language path handles approval + buy in one shot: + +```bash +bankr agent prompt "Buy the Rat Skull Charm from the Cat Town boutique" +``` + +Or submit calldata directly — see [references/boutique.md](references/boutique.md) for raw calldata recipes per token. -**Purchase flow is out of scope for this revision** — this skill currently reads the boutique only. +Full ABI surface, trait schema (real keys: `Item Name`, `Rarity`, `Item Type`, `Source`, `Slot`, `Sprite`, `imageUrl`, `Collection`, etc.), per-token approval recipes, revert catalogue, and preview future rotations: [references/boutique.md](references/boutique.md). --- @@ -505,7 +663,7 @@ Example reply (live state): > > You haven't claimed your free ticket this week — want me to grab it? -Full ABI surface, write paths, tier math, live-worked chance calcs: [references/fish-raffle/contract.md](references/fish-raffle/contract.md). API response shapes: [references/fish-raffle/api.md](references/fish-raffle/api.md). Player-facing overview: https://docs.cat.town/fishing/fish-raffle. +Full ABI surface, write paths, tier math, live-worked chance calcs: [references/fish-raffle.md](references/fish-raffle.md). API response shapes: [references/fish-raffle-api.md](references/fish-raffle-api.md). Player-facing overview: https://docs.cat.town/fishing/fish-raffle. **Paid tickets (buying with caught fish) are out of scope for this revision** — free claim + reads only. @@ -580,9 +738,31 @@ Because the pay tx and the mint tx are decoupled, the frontend correlates them b If the user spins 10 times, you must wait for 10 items with `id > latestId`. Partial results are fine to preview, but be explicit about how many are still pending. Don't assume pull-1's result has a smaller id than pull-2's — VRF callbacks can interleave. -### Response patterns +### Response pattern — ALWAYS poll and report in the same message + +**Default: poll, then reply.** Do not return "ask me again in 30 seconds" as a normal response. The agent has a working HTTP tool and a sleep — use them. The pay tx is async at the contract level, but the user-facing experience must not be: cat.town's own UI polls every 1 s and shows the result inline, and so should we. Telling the user to re-prompt is a regression. + +The flow inside one user turn: + +``` +1. Cache latestId before any tx: + latestId = max(item.id for item in GET /v2/items/capsule/) + # endpoint returns 500 for cold wallets — treat as latestId = 0 +2. Submit the pay tx(s). Wait for each one's receipt (Base confirms in ~2 s). +3. Loop, sleeping 2 s between attempts: + newItems = [i for i in GET /v2/items/capsule/ if i.id > latestId] + if len(newItems) >= N: break # all results landed +4. Cap the loop at ~60 s of total polling (≈30 attempts). +5. Report inline: rarity + name + collection + KIBBLE/$ value, plus "X pulls left today." +``` + +VRF on Base typically lands in 5–15 s per pull. Polling cost is one cheap GET every couple seconds — keep going until you have all N or you hit the 60 s cap. + +**Only fall back to "ask me again" if the 60 s budget genuinely expires** without all N items landing. That fallback should be specific: + +> 4 of 5 spins minted (results below). The 5th is still pending — VRF backed up. Ask me "what else did I get?" in ~30 seconds and I'll grab it. -**Can Bankr poll for results?** If yes, use the loop above and report when all N have landed. If not, submit the pay tx(s), return immediately with "Spin submitted — ask me again in ~30 seconds to see what dropped" and let the user re-prompt. When they come back, pull `/v2/items/capsule/` and show items with `id > latestId` (where `latestId` was cached in the original turn). +Don't preempt this fallback. Don't issue it as the *first* response after submitting the tx — that's the regression we're fixing. ### Always quote the item's value + offer a sell @@ -590,9 +770,7 @@ When reporting a gacha result, look up the item's `sellValue` (in US cents) from Format: `" " (, ) worth ~ KIBBLE (~$)`. Example for Fern: `"Common Fern (Collectible, Plant Minis) worth ~105 KIBBLE (~$0.10)"`. -### Example replies - -**Polling path (Bankr can wait):** +### Example reply (the only one — same message, polled to completion) > Spinning once… paid ~527 KIBBLE. Waiting on VRF… > @@ -600,9 +778,17 @@ Format: `" " (, ) worth ~ KIBBLE (~$)`. Ex > > Want me to sell it for you? After the 5% vendor fee, you'd get ~100 KIBBLE. -**Non-polling path (no async support):** +For an N-pull batch, lead with the headline (highest-rarity drop) and list the rest: -> Submitted 5 pulls (~2,635 KIBBLE total). VRF needs a few seconds to mint each one. Ask me "what did I get?" in ~30 seconds and I'll check — I can also sell the results right away if you want. +> Spun 5x (~2,635 KIBBLE). All 5 results landed: +> +> 🎉 **Epic Diamond** (Treasure, Spring Treasures) — ~10,500 KIBBLE (~$10.00) — your headline pull +> - Common Fern — ~105 KIBBLE +> - Common Pebble — ~50 KIBBLE +> - Uncommon Acorn — ~210 KIBBLE +> - Common Twig — ~50 KIBBLE +> +> 95 pulls left today. Want me to sell the four commons/uncommon and keep the Diamond? You'd net ~395 KIBBLE after the 5% fee. ### Reads cheat-sheet @@ -615,34 +801,52 @@ Format: `" " (, ) worth ~ KIBBLE (~$)`. Ex | `GET /v2/items/capsule/` | Result polling target | | `GET /v2/items/master?limit=1000` | Full catalog; filter `source=Gacha` | -Full contract signatures, VRF event names, oracle math, and the capsule API quirks (500 for cold wallets, etc.): [references/gacha/contract.md](references/gacha/contract.md), [references/gacha/api.md](references/gacha/api.md). Player-facing overview + pool archive: https://docs.cat.town/shops/gacha, https://docs.cat.town/items/gacha/archive. +Full contract signatures, VRF event names, oracle math, and the capsule API quirks (500 for cold wallets, etc.): [references/gacha.md](references/gacha.md), [references/gacha-api.md](references/gacha-api.md). Player-facing overview + pool archive: https://docs.cat.town/shops/gacha, https://docs.cat.town/items/gacha/archive. --- ## Selling items (vendor, V2 minter only) -Players sell **Treasures** and **Collectibles** (including gacha pulls) to the **SellItems** contract at `0x49936db5Dcbc906D682CFa2dcfAb0788e3ee5808` for KIBBLE, minus a **5% merchant fee**. +Players sell **Treasures** and **Collectibles** (including gacha pulls) to the **SellItems** contract for KIBBLE, minus a **5% merchant fee**. SellItems = Supermarket = "V2 vendor" — three names, one contract. -This skill revision supports **only items minted by the V2 minter** (`0x7b65ec82cB4600Bc1dCc5124a15594976f19eA14`). Legacy V1-minted items must be filtered out in the preflight. +### Addresses (Base, chain id 8453) -### Value math +- **SellItems / Supermarket** (the contract you call): `0x49936db5Dcbc906D682CFa2dcfAb0788e3ee5808` +- **V2 Minter (ERC-1155, all Cat Town items)** (the `nftContract` argument you pass, and the contract you `setApprovalForAll` on): `0x7b65ec82cB4600Bc1dCc5124a15594976f19eA14` +- **KIBBLE** (the token you receive): `0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb` -Each sellable item has a `sellValue` in the public item catalog — **US cents**, not KIBBLE, not wei: +Reference: [references/sell-items.md](references/sell-items.md). Player-facing overview: https://docs.cat.town/shops/sell-items. -``` -GET https://api.cat.town/v2/items/master?limit=1000 - → items[].sellValue (cents, e.g. 10 = $0.10) -``` +### What's sellable here — and what's not -Convert to KIBBLE for display via the Kibble Price Oracle: +This skill revision supports **only items minted by the V2 minter**. Legacy V1-minted items, **and ALL third-party NFTs (Songbirdz, Pudgy, etc.) regardless of theme name**, are not sellable here — the contract enforces a `supportedNFTContracts` whitelist. Pass any other address and the call reverts with `NFTContractNotSupported()`. -``` -usd = sellValue / 100 -kibble_value = usd / (rawKibbleUsdPrice / 10^18) -payout_after_tax = kibble_value * 0.95 // 5% vendor fee -``` +⚠️ **A "Songbirdz Owl" or "Songbirdz Cardinal" in a user's wallet is a Cat Town gacha collectible (collab-themed), minted by the V2 minter — NOT the external Songbirdz NFT contract.** Items with `source == "Capsule Machine"` or `source == "Fishing"` in the catalog are V2-minted Cat Town items, sellable here. If you find yourself looking up the actual Songbirdz/Pudgy/etc. contract address, you've routed wrong — the V2 minter is always the right `nftContract` argument for Cat Town items. -A freshly minted NFT (e.g. a gacha pull) also carries a `Sell Value (KIBBLE)` trait with the pre-computed KIBBLE amount. Prefer the trait when available; fall back to the catalog formula. +### ⚠️ All sell reverts come back as bare 4-byte custom errors, NOT strings + +If you see "unknown error" / a bare `0x...` selector in a sell trace, decode it against this table: + +| Selector / error | Meaning | Fix | +|--------------------------------|-------------------------------------------------------------------------|----------------------------------------------------------------| +| `NFTContractNotSupported()` | `nftContracts[i]` isn't on the supermarket whitelist | Use the V2 minter address. Don't pass external NFT contracts. | +| `InsufficientNFTBalance()` | `V2Minter.balanceOf(user, tokenId) < amount` | Wrong tokenId, wrong wallet, or item already transferred away. | +| `InsufficientKibbleBalance()` | Vendor is out of KIBBLE | Wait for ops to top up; tell the user. | +| `KibbleTransferFailed()` | KIBBLE.transfer(seller, …) returned false | Re-try; surface to user if persists. | +| `InputArrayLengthMismatch()` | `nftContracts.length != tokenIds.length != amounts.length` | Encoding bug — fix the call. | +| `ERC1155MissingApprovalForAll` | `setApprovalForAll(supermarket, true)` was never called | Submit the approval, then retry the sell. | + +**Always do the read-side preflight FIRST** (next section). Most "unknown error" tickets are one of these — readable in advance. + +### Preflight (read-only — run BEFORE constructing the sell tx) + +In order: + +1. **Approval** — `V2Minter.isApprovedForAll(user, supermarket)`. If `false`, the very first tx must be `V2Minter.setApprovalForAll(supermarket, true)`. One-time per wallet — once true, all future sells are single-tx. +2. **V2 filter** — only include items whose source `nftContract` is the V2 minter. Skip V1 / external NFTs (they revert `NFTContractNotSupported`); tell the user how many were skipped. +3. **Ownership** — `V2Minter.balanceOf(user, tokenId) >= amount` for each item. +4. **Vendor liquidity** — `KIBBLE.balanceOf(supermarket)` must exceed total payout. Otherwise the call reverts `InsufficientKibbleBalance` (a.k.a. "vendor is out of KIBBLE"). +5. **Tax rate** — `taxRateInBps()` (currently 500 = 5%); use this to size the user-facing "you'll net X KIBBLE" estimate. ### Write flow @@ -650,20 +854,32 @@ Single function, batched up to **25 items per call**: ``` SellItems.sellMultipleNFTsToContract( - address[] nftContracts, // V2 minter address repeated, one per item - uint256[] tokenIds, // token ids to sell + address[] nftContracts, // V2 minter (0x7b65...ea14) repeated, one per item + uint256[] tokenIds, // V2 token ids to sell uint256[] amounts // 1 per item (ERC-1155) ) ``` -Preflight: +Emits `NFTSold(seller, nftContract, tokenId, amount, kibblePaid)` per item, and `KibbleTransferred(to, amount)` for the aggregate payout. + +### Value math -1. **Approval** — check `V2Minter.isApprovedForAll(user, sellContract)`. If false, submit `setApprovalForAll(sellContract, true)` first. One-time per wallet. -2. **V2 filter** — only include items whose source nftContract is the V2 minter. Skip V1, tell the user how many were skipped. -3. **Ownership** — `V2Minter.balanceOf(user, tokenId) >= 1` for each item. -4. **Vendor liquidity** — `KIBBLE.balanceOf(sellContract)` must exceed total payout; otherwise reverts `KibbleTransferFailed` ("vendor is out of KIBBLE"). +Each sellable item has a `sellValue` in the public item catalog — **US cents**, not KIBBLE, not wei: -Tax rate is read from `taxRateInBps()` (currently 500 = 5%, rounded from chain on the frontend). +``` +GET https://api.cat.town/v2/items/master?limit=1000 + → items[].sellValue (cents, e.g. 10 = $0.10) +``` + +Convert to KIBBLE for display via the Kibble Price Oracle: + +``` +usd = sellValue / 100 +kibble_value = usd / (rawKibbleUsdPrice / 10^18) +payout_after_tax = kibble_value * 0.95 // 5% vendor fee, or use taxRateInBps()/10000 +``` + +A freshly minted NFT (e.g. a gacha pull) also carries a `Sell Value (KIBBLE)` trait with the pre-computed KIBBLE amount. Prefer the trait when available; fall back to the catalog formula. ### Inventory API — "what can I sell?" @@ -690,7 +906,7 @@ Or, for a batch: > You've got 12 V2-minter items worth selling, totaling ~3,420 KIBBLE after the 5% fee. (Skipping 2 legacy items.) Want me to sell all 12, or cherry-pick? -Full ABI surface, approval detail, inventory-API query params, revert catalogue, and the batch recipe: [references/sell-items/contract.md](references/sell-items/contract.md). Player-facing overview: https://docs.cat.town/shops/sell-items. +Full ABI surface, approval detail, inventory-API query params, revert catalogue, and the batch recipe: [references/sell-items.md](references/sell-items.md). Player-facing overview: https://docs.cat.town/shops/sell-items. --- @@ -732,7 +948,7 @@ Derived dynamically from **baronbot** (`0x8Ff7AcCCf73c515c1f62Fc7b64A63F17Ce9965 > **KIBBLE tokenomics (live):** ~66% of supply has been burned, ~24% of circulating is staked in Wealth & Whiskers, and staking currently pays ~30% APY. Want me to walk you through staking? The lock period is 14 days. -Full formulas, APY caps, and the live worked example: [references/kibble/tokenomics.md](references/kibble/tokenomics.md). Player-facing KIBBLE economy overview: https://docs.cat.town/economy/tokens/kibble, https://docs.cat.town/get-started/kibble-economy. +Full formulas, APY caps, and the live worked example: [references/kibble-tokenomics.md](references/kibble-tokenomics.md). Player-facing KIBBLE economy overview: https://docs.cat.town/economy/tokens/kibble, https://docs.cat.town/get-started/kibble-economy. --- diff --git a/cattown/docs.md b/cattown/docs.md index 69b3420b..ccdce604 100644 --- a/cattown/docs.md +++ b/cattown/docs.md @@ -70,14 +70,14 @@ Future revisions will add the boutique purchase flow, the paid-ticket (fish-burn - `isUnlocking(user)` + `unlockEndTime(user)` — exit status + ETA - `getTotalStaked()` / `getTotalActiveStaked()` — pool sizes -Full reference: [references/staking/contract.md](references/staking/contract.md). +Full reference: [references/staking.md](references/staking.md). ### Offchain API (public, unauthenticated) - `GET https://api.cat.town/v2/revenue/staking/leaderboard` — top stakers with rank + pool-share %. - `GET https://api.cat.town/v2/revenue/deposits/{address}` — one user's historical fishing/gacha deposits with per-tx share. -Response shapes + field meanings: [references/staking/api.md](references/staking/api.md). +Response shapes + field meanings: [references/staking-api.md](references/staking-api.md). --- @@ -91,9 +91,9 @@ Response shapes + field meanings: [references/staking/api.md](references/staking Enums: Season `0..3` (Spring/Summer/Autumn/Winter); TimeOfDay string (`"Morning"`, `"Daytime"`, `"Evening"`, `"Nighttime"`); Weather `0..6` (None/Sun/Rain/Wind/Storm/Snow/Heatwave). -World state drives fishing and gacha drop tables (different fish appear in different weather/seasons). Fishing drop tables are documented in [references/fishing/drops.md](references/fishing/drops.md); gacha pools are covered in the gacha section below. +World state drives fishing and gacha drop tables (different fish appear in different weather/seasons). Fishing drop tables are documented in [references/fishing-drops.md](references/fishing-drops.md); gacha pools are covered in the gacha section below. -Full function table, selectors, live sample: [references/world/contract.md](references/world/contract.md). +Full function table, selectors, live sample: [references/world.md](references/world.md). --- @@ -110,7 +110,7 @@ Each item carries optional `dropConditions` keyed by `events`, `seasons`, `times Weather changes most frequently (minutes-to-hours), so weather-exclusive drops are the most rotational — highest-value thing to surface. -Full recipe + live weather→drops table: [references/fishing/drops.md](references/fishing/drops.md). +Full recipe + live weather→drops table: [references/fishing-drops.md](references/fishing-drops.md). --- @@ -128,7 +128,7 @@ Seasonal doc pages (public): [shops/boutique](https://docs.cat.town/shops/boutiq **USD conversion:** `getKibbleUsdPrice()` returns USD-per-KIBBLE scaled by **`10^18`** (note: `getEthUsdPrice()` on the same contract uses `10^8`). Formula: `usd = (price_wei * rawKibbleUsdPrice) / 10^36`. Live at time of writing: ~$0.0009487 per KIBBLE. -Full reference: [references/boutique/contract.md](references/boutique/contract.md). +Full reference: [references/boutique.md](references/boutique.md). --- @@ -152,7 +152,7 @@ stakersRevenue = prizePool * 0.10 // flows to KIBBLE stakers via RevenueShar When active: lead with running time / weather / participants / prize pool / top 10. When inactive: compute next Saturday 00:00 UTC, offer a reminder, and offer to narrate the last completed competition (the API returns it when `isActive=false`). -Full reference: [references/fishing/competition.md](references/fishing/competition.md). +Full reference: [references/fishing-competition.md](references/fishing-competition.md). --- @@ -176,7 +176,7 @@ Prize pool is `poolBalance * tier.bps / 10000` where the tier is picked by the r Chance-to-win approximation: `min(1, 5 * userTickets / totalTickets)`. Live at time of writing: round 31, 2,855 tickets sold, 80-bps tier, ~47,742 KIBBLE pool → ~9,548 KIBBLE per winner. -Full reference: [references/fish-raffle/contract.md](references/fish-raffle/contract.md), [references/fish-raffle/api.md](references/fish-raffle/api.md). +Full reference: [references/fish-raffle.md](references/fish-raffle.md), [references/fish-raffle-api.md](references/fish-raffle-api.md). --- @@ -193,7 +193,7 @@ Full reference: [references/fish-raffle/contract.md](references/fish-raffle/cont The pay tx submits a VRF randomness request; the NFT mints in a separate tx seconds later. Agents must either poll the capsule API for new items (if Bankr supports async polling) or return "ask me again in ~30 s" and re-check later. For multi-pulls, wait until `count(newItems) >= N` before reporting. -Every pull is uniformly weighted against the current season's pool — no pity, no streaks. Full pattern + oracle math + 500-on-cold-wallet quirk: [references/gacha/contract.md](references/gacha/contract.md), [references/gacha/api.md](references/gacha/api.md). +Every pull is uniformly weighted against the current season's pool — no pity, no streaks. Full pattern + oracle math + 500-on-cold-wallet quirk: [references/gacha.md](references/gacha.md), [references/gacha-api.md](references/gacha-api.md). --- @@ -212,7 +212,7 @@ Sellable types: Treasure + Collectible only (Cosmetics/Fish/Equipment aren't sel Write call (single, batched): `sellMultipleNFTsToContract(address[] nftContracts, uint256[] tokenIds, uint256[] amounts)`. Requires `setApprovalForAll(sellContract, true)` on the V2 minter, once per wallet. -Full reference: [references/sell-items/contract.md](references/sell-items/contract.md). +Full reference: [references/sell-items.md](references/sell-items.md). --- @@ -227,7 +227,7 @@ Full reference: [references/sell-items/contract.md](references/sell-items/contra Mirrors Jasper's NPC answers in the Wealth & Whiskers Bank. The % burned uses total supply as the denominator; % staked uses circulating (total − burned). APY is dynamic and uncapped until 1000% APY / 50% monthly rate sanity limits. -Full reference: [references/kibble/tokenomics.md](references/kibble/tokenomics.md). +Full reference: [references/kibble-tokenomics.md](references/kibble-tokenomics.md). --- @@ -244,7 +244,7 @@ Cat Town runs on a fixed weekly UTC cycle. Only the **bold** rows directly affec | Friday | **Fish raffle draw** | 20:00 | Paulie | | Sat–Sun | **Weekly fishing competition** | Sat morning → Sun night | Isabella | -Full calendar with revenue-split details and NPC cheat-sheet: [references/world/calendar.md](references/world/calendar.md). +Full calendar with revenue-split details and NPC cheat-sheet: [references/world-calendar.md](references/world-calendar.md). --- diff --git a/cattown/references/boutique.md b/cattown/references/boutique.md new file mode 100644 index 00000000..34789809 --- /dev/null +++ b/cattown/references/boutique.md @@ -0,0 +1,356 @@ +# Cat Town Boutique — contract + KIBBLE oracle reference + +The boutique is a fully onchain daily shop on Base. Every day at **00:00 UTC** the contract surfaces **3 items** selected deterministically from the current season's pool. No offchain API is needed — items, prices, stock, rotation, **and the buy path** are all directly on the Boutique contract. + +This doc covers the **Boutique** contract (rotation, item state, **purchase flow**) and the **KIBBLE price oracle** (for USD conversion — the in-game UI shows native-token prices only). + +## ⚠️ CRITICAL: each item carries its own `paymentToken` — read it before approving + +Most items are priced in **KIBBLE**, but partnership / collab items use other ERC-20s — for example today's **Rat Skull Charm** (Friends of Cat Town collab) is priced in **DOTA** ("Defense of the Agents", `0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07`). The contract pulls the price from `msg.sender` in **whichever token the item specifies**, so you must approve the **right token**. + +If you reflexively `kibble.approve(boutique, …)` for a DOTA-priced item, the `purchaseItem` tx reverts on the internal `transferFrom` (insufficient DOTA allowance — KIBBLE allowance is irrelevant). The fix is to read `ShopItemView.paymentToken` per item and approve **that** token. + +Tokens currently surfaced by the cat.town frontend: **KIBBLE, DOTA, USDC, BARON, cbBTC**. Any ERC-20 the team configures will work; treat the address as authoritative, not the symbol. + +## Addresses (Base, chain 8453) + +| Contract | Address | +|---------------------|----------------------------------------------| +| Boutique | `0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02` | +| Kibble Price Oracle | `0xE97B7ab01837A4CbF8C332181A2048EEE4033FB7` | +| KIBBLE token | `0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb` | + +## Rotation model + +- Each day starts at **00:00 UTC**; `getCurrentDayNumber()` returns days since Unix epoch (`block.timestamp / 86400`). +- Rotation is deterministic from `(dayNumber, currentSeason)` — same day + same season = same 3 items. +- `itemsPerDay()` = **3** (constant). +- Season boundaries follow `GameData.getCurrentSeason()` (see [world.md](world.md)); each season has its own pool. +- The matching human-readable doc pages: + - Top-level shop: https://docs.cat.town/shops/boutique + - Spring: https://docs.cat.town/boutique/spring-fashion + - Summer: https://docs.cat.town/boutique/summer-fashion + - Autumn: https://docs.cat.town/boutique/autumn-fashion + - Winter: https://docs.cat.town/boutique/winter-fashion + +## Primary read — `getTodaysRotationDetails()` + +Returns today's 3 items as `ShopItemView[]` — full details in one call. Selector: `0x36362553`, no args. + +### `ShopItemView` fields + +| Field | Type | Notes | +|--------------------|-------------|-----------------------------------------------------------------------| +| `itemId` | `uint256` | Unique id for the shop item | +| `traitNames` | `string[]` | Parallel array of trait keys, e.g. `["Name","Rarity","Image","Slot","Shiny"]` | +| `traitValues` | `string[]` | Parallel array of values in the same order | +| `paymentToken` | `address` | Always the KIBBLE token | +| `price` | `uint256` | KIBBLE in **wei** (18 decimals) — divide by `10^18` for display | +| `stockRemaining` | `uint256` | Units still purchasable. `0` → sold out | +| `totalPurchased` | `uint256` | Units sold so far | +| `maxSupply` | `uint256` | Total ever available. `type(uint256).max` → uncapped | +| `startTime` | `uint64` | Unix seconds (0 = always available) | +| `endTime` | `uint64` | Unix seconds (0 = no end) | +| `availableSeasons` | `uint8` | Bitmask: `1=Spring`, `2=Summer`, `4=Autumn`, `8=Winter` | +| `isActive` | `bool` | Enabled by admin | +| `isPurchasableNow` | `bool` | Passes time + season gates | +| `isInTodaysRotation` | `bool` | In today's 3-item set | + +### Parsing the trait arrays + +`traitNames` and `traitValues` are parallel. Real trait keys on a live boutique item: + +| Trait key | Example value | Notes | +|---------------|------------------------------------------------------|------------------------------------------------------------| +| `Item Name` | `"White Longsleeve"` | Display name | +| `Rarity` | `"Rare"` | `Common` / `Uncommon` / `Rare` / `Epic` / `Legendary` | +| `Item Type` | `"Cosmetic"` | Almost always `Cosmetic` for boutique | +| `Source` | `"Boutique"` | Distinguishes from `Fishing`/`Gacha` in a joined view | +| `Slot` | `"Body"` | `Hat` / `Body` / `Eyewear` / `Companion` / etc. | +| `Sprite` | `"white-longsleeve"` | Internal asset id | +| `imageUrl` | `https://cdn.cat.town/nft/equipment/body/...` | Display image | +| **`Collection`** | `"Spring Fashion"` | **Collection label** — use this to tell the user which collection is currently rotating | +| `Flavor Text` | `"Clean and crisp like fresh spring linens."` | Optional color | +| `Sell Value` | `"0"` | Usually 0 for boutique (these aren't meant to be resold) | +| `coreId` | `"cmlz9n8f30008kz04flhruq6t"` | Internal database id | + +Boutique metadata is **onchain via the trait arrays** — don't cross-reference `/v2/items/master`. The `ShopItemView.traitNames`/`traitValues` are the source of truth. + +## Other useful reads + +| Function | Returns | Notes | +|---------------------------------------|-------------------------------|------------------------------------------------| +| `getTodaysRotation()` | `uint256[]` | Just today's 3 item ids (cheaper) | +| `getCurrentDayNumber()` | `uint256` | Days since Unix epoch | +| `getCurrentSeason()` | `uint8` | `0=Spring, 1=Summer, 2=Autumn, 3=Winter` | +| `getShopItem(itemId)` | `ShopItemView` | One item by id | +| `getAllShopItems()` | `ShopItemView[]` | Full catalog, active + inactive | +| `getItemsBySeason(season)` | `ShopItemView[]` | Season-specific pool | +| `previewRotationForDay(day, season)` | `uint256[]` | Future rotation preview (deterministic) | +| `getItemStock(itemId)` | `(max, purchased, remaining)` | Stock only | +| `dailyRotationEnabled()` | `bool` | Is daily rotation on (expected: true) | +| `itemsPerDay()` | `uint8` | Currently 3 | +| `defaultPaymentToken()` | `address` | KIBBLE | + +## paymentToken → USD conversion + +### ⚠️ The Kibble Price Oracle ONLY converts KIBBLE → USD. Branch on `paymentToken` first. + +The Kibble Price Oracle quotes **KIBBLE only**. Applying its rate to a non-KIBBLE amount silently returns nonsense — e.g. 1.5M DOTA × KIBBLE-rate gives ~$1,420, but the actual DOTA market value is ~$2 (off by ~700×). Always check `ShopItemView.paymentToken` against the KIBBLE address before reaching for the oracle: + +``` +if paymentToken == KIBBLE_ADDRESS: # 0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb + usd = (price * getKibbleUsdPrice()) / 10^36 # KIBBLE → USD via the oracle (below) +elif paymentToken == USDC_ADDRESS: + usd = price / 10^6 # USDC is dollar-pegged +else: + usd = price_via_dex(paymentToken, price) # see "Non-KIBBLE collab tokens" below — DOTA, BARON, cbBTC, etc. +``` + +If you can't get a reliable USD for a non-KIBBLE token, **quote the token amount only and skip the USD readout**. A wrong USD is worse than no USD — users will trust whatever number you show. + +### KIBBLE oracle reads (only valid for KIBBLE-priced items) + +| Function | Selector | Returns | Scale | +|-----------------------|--------------|--------------------------------|-------------| +| `getKibbleUsdPrice()` | `0x00cbfbce` | `uint256` USD per 1 KIBBLE | **× 10^18** | +| `getEthUsdPrice()` | `0xa0a8045e` | `uint256` USD per 1 ETH | × 10^8 (Chainlink) | +| `getKibbleEthPrice()` | `0x47bb71e5` | `uint256` ETH per 1 KIBBLE | × 10^18 | + +**Watch the scale mismatch:** `getKibbleUsdPrice()` is `10^18`, but `getEthUsdPrice()` is `10^8`. Easy to mix up — use the right divisor per call. + +#### Formula (KIBBLE-priced items only) + +``` +kibble_human = price / 10^18 # KIBBLE count +usd_per_kibble = rawKibbleUsdPrice / 10^18 # USD per 1 KIBBLE +usd_value = kibble_human * usd_per_kibble + = (price * rawKibbleUsdPrice) / 10^36 # BigInt-safe form +``` + +For integer cents: `usd_cents = (price * rawKibbleUsdPrice) / 10^34`. + +#### Live example (captured during writing) + +- `getKibbleUsdPrice()` = `948,723,424,083,878` → **$0.0009487 per KIBBLE** +- 1,000 KIBBLE ≈ $0.95 +- 10,000 KIBBLE ≈ $9.49 +- 100,000 KIBBLE ≈ $94.87 + +The oracle tracks KIBBLE's real market price; re-read at least every few minutes if you care about accuracy. + +### Non-KIBBLE collab tokens — pricing via DEX + +For paymentTokens the cat.town frontend doesn't price internally (DOTA, partnership/collab tokens), use a public DEX aggregator. Dexscreener's API works without auth and ranks pools by liquidity, which avoids the "stale low-liquidity pool" failure mode: + +``` +GET https://api.dexscreener.com/latest/dex/tokens/ + → response.pairs: array of pools across DEXes/chains + → filter: chainId == "base" + → sort: liquidity.usd DESC + → use: pairs[0].priceUsd # most-liquid Base pool, USD per 1 token + +usd_value = (price / 10^paymentTokenDecimals) * priceUsd +``` + +#### Live example — DOTA (`0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07`, 18 decimals) + +- Most liquid Base pool: Uniswap v4 DOTA/WETH (~$130k liquidity) +- `priceUsd` ≈ **$0.000001447 per DOTA** (re-read live; small caps move fast) +- 1,500,000 DOTA × $0.000001447 ≈ **$2.17** ← this is what the Rat Skull Charm actually costs in USD, NOT $1,400 + +#### Sanity check — does the USD make sense for the rarity? + +Boutique cosmetics typically price in this band: + +| Rarity | Typical USD range observed | +|------------|----------------------------| +| Common | $1–$3 | +| Uncommon | $3–$8 | +| Rare | $5–$15 | +| Epic | $15–$40 | +| Legendary | $40–$100+ | + +If you compute a USD for a Rare collab item and get something like $1,000+ or $0.0001, the price source is wrong — almost certainly the Kibble oracle was applied to a non-KIBBLE amount. Re-check `paymentToken` and re-route. Collab items can sit slightly below the band (partnership discounts), but never orders of magnitude off. + +### USDC — dollar-pegged, no oracle needed + +If `paymentToken == USDC` (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` on Base, 6 decimals): `usd = price / 10^6`. Skip the oracle entirely. + +## Response pattern — "what's in the boutique today?" + +1. Read in parallel: `getTodaysRotationDetails()` (single call, 3 items) and `getKibbleUsdPrice()`. If any item's `paymentToken` is not KIBBLE, also fan out to a DEX price source for those tokens (Dexscreener `/latest/dex/tokens/`). +2. For each `ShopItemView`: + - Parse `traitNames`/`traitValues` into a dict → pull `Name`, `Rarity`, `Slot`, `Collection`. + - `token_price = price / 10^paymentTokenDecimals` (KIBBLE/DOTA/BARON = 18; USDC = 6; cbBTC = 8). + - `usd_price` — branch on `paymentToken`: KIBBLE → oracle; USDC → `price / 10^6`; else DEX. Sanity-check against the rarity band (above). If you can't price it confidently, drop the USD and just show the token amount. + - Stock: if `stockRemaining == 0` → **"Sold Out"**; otherwise format as **`"{stockRemaining} of {maxSupply} remaining"`** — stockRemaining first, maxSupply second. The order matters: `stockRemaining` ≤ `maxSupply` always, so if the first number ever exceeds the second you've swapped them. Reread the struct fields if unsure. +3. Sort with the big-ticket order: **rarity DESC** (Legendary → Common), then **USD price DESC** (cross-token comparable), then name ASC. +4. Open the reply with the current season, and end with a link to the matching `docs.cat.town/boutique/...-fashion` page. + +### Example response (real data from today's rotation) + +> **Boutique today — Spring Fashion collection (Day 20566):** +> +> 1. **White Longsleeve** — Rare Body — **12,500 KIBBLE (~$11.86)** — 1 of 1 remaining +> 2. **Royal Blue Varsity** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 2 of 2 remaining +> 3. **Classic Academic Blouse** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 1 of 2 remaining +> +> Browse the other seasonal collections: +> - Spring: https://docs.cat.town/boutique/spring-fashion +> - Summer: https://docs.cat.town/boutique/summer-fashion +> - Autumn: https://docs.cat.town/boutique/autumn-fashion +> - Winter: https://docs.cat.town/boutique/winter-fashion +> - Overview: https://docs.cat.town/shops/boutique + +## Buying an item + +Single-item purchase per tx — no batch/multi-buy on the contract. Mint is **synchronous** (unlike gacha's async VRF mint): the same `purchaseItem` tx pulls payment, mints the NFT, and returns its token id. + +### Write path + +``` +Boutique.purchaseItem(uint256 itemId) external nonReentrant whenNotPaused returns (uint256 mintedTokenId) +``` + +- Selector: `0xd38ea5bf` +- `itemId` is the **plain rotation id** from `getTodaysRotation()` (e.g. 208, 173, 196 in today's rotation) — **not** scaled, **not** wei. Pass the integer as-is. +- State mutability: `nonpayable` — do **not** send `msg.value`. +- Modifiers: `nonReentrant` and `whenNotPaused` — if `paused()` returns true, all purchases revert. Read `paused()` first if you suspect downtime. +- Internally: pulls `price` of `paymentToken` from `msg.sender` to a contract-set `treasury` address (NOT held by the Boutique itself), then mints the NFT via the configured V2 minter, then emits `ItemPurchased`. There's no boutique fee — the full price flows to treasury. + +### Preconditions (run before submitting) + +1. **`canPurchaseItem(itemId)` → `(bool canPurchase, string reason)`** — view-only preflight. If `canPurchase == false`, surface `reason` verbatim. The exact strings the contract returns: + - `"Item does not exist"` + - `"Item is not active"` + - `"Item is out of stock"` + - `"Item not available yet"` (start-time gate) + - `"Item no longer available"` (end-time gate) + - `"Item not available this season"` + - `"Item not in today's rotation"` + + Note: `canPurchaseItem` does **not** check `paused()`. If the contract is paused but the item is otherwise fine, this returns `(true, "")` and the actual `purchaseItem` tx reverts. Read `paused()` separately when in doubt. +2. **`paymentToken.balanceOf(user) >= price`** — `price` is in `paymentToken`'s native unit (KIBBLE/DOTA/BARON are 18 decimals; USDC is 6 decimals; cbBTC is 8 decimals). Don't reflexively use 18. +3. **`paymentToken.allowance(user, boutique) >= price`** — if not, approve first. The spender is the Boutique address (`0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02`), even though tokens flow through to the treasury. + +### Reverts from `purchaseItem` are custom errors, NOT strings + +The friendly strings only come from `canPurchaseItem`. The actual `purchaseItem` reverts with Solidity custom errors (4-byte selectors, no message). If a tx reverts and you only see a hex selector in the trace, this table maps them: + +| Custom error | Same condition as `canPurchaseItem` reason | +|------------------------------------|--------------------------------------------| +| `ItemNotFound()` | "Item does not exist" | +| `ItemNotActive()` | "Item is not active" | +| `ItemOutOfStock()` | "Item is out of stock" | +| `ItemNotAvailableYet()` | "Item not available yet" | +| `ItemNoLongerAvailable()` | "Item no longer available" | +| `ItemNotAvailableThisSeason()` | "Item not available this season" | +| `ItemNotInDailyRotation()` | "Item not in today's rotation" | + +This is exactly why preflighting with `canPurchaseItem` is worth one cheap RPC — you get a string the user can read. + +### `ItemPurchased` event + +``` +event ItemPurchased( + address indexed buyer, + uint256 indexed itemId, + uint256 indexed mintedTokenId, + address paymentToken, + uint256 price +) +``` + +`mintedTokenId` is the V2-minter token id the user now owns — useful if you immediately want to surface a sell quote (see [sell-items.md](sell-items.md)) or render the item. + +### Never hardcode the price — read it from the contract + +`ShopItemView.price` is the literal token amount in `paymentToken`'s native wei. Read it via `getShopItem(itemId)` or `getTodaysRotationDetails()`. Don't infer from any doc, including this one; admin can update prices, the rotation rotates daily, and the currency varies per item. The recipes below use **today's live values** as illustrations — they will be wrong tomorrow. + +### Recipe — KIBBLE-priced item (the common case) + +Live as of writing — **Striking Baseball Cap** (`itemId = 173`, Legendary, **50,000 KIBBLE**, 1/1 stock). + +``` +price = ShopItemView.price for itemId 173 # = 50_000 * 10^18 today + +0. paused() == false # else everything reverts +1. canPurchaseItem(173) → must be (true, "") +2. KIBBLE.balanceOf(user) ≥ price +3. KIBBLE.allowance(user, boutique) ≥ price + - if not: KIBBLE.approve(boutique, price) # standard ERC-20 wei +4. Boutique.purchaseItem(173) # plain integer, no scaling +``` + +### Recipe — DOTA-priced item (collab / partnership) + +Live as of writing — **Rat Skull Charm** (`itemId = 208`, Rare, **1,500,000 DOTA**, ~64/100 stock remaining, "Friends of Cat Town" collection). + +``` +price = ShopItemView.price for itemId 208 # = 1_500_000 * 10^18 today (DOTA, 18 decimals) + +0. paused() == false +1. canPurchaseItem(208) → must be (true, "") +2. DOTA.balanceOf(user) ≥ price # else swap (see below) +3. DOTA.allowance(user, boutique) ≥ price + - if not: DOTA.approve(boutique, price) # DOTA = 0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07 +4. Boutique.purchaseItem(208) +``` + +If the user doesn't hold any DOTA, the cat.town UI sends them to Uniswap to swap in: + +``` +https://app.uniswap.org/swap?chain=mainnet&outputChain=base&inputCurrency=NATIVE&outputCurrency=0x5f09821cbb61e09d2a83124ae0b56aaa3ae85b07 +``` + +For collab tokens generally, prefer to swap *into* the required token from KIBBLE / ETH / USDC rather than asking users to source it themselves. Bankr's swap surface (via the `trails` or `symbiosis` skill) handles this. + +### Per-wallet purchase counts + +`getUserPurchaseCount(itemId, user)` returns how many times a wallet has bought a specific item. The contract increments this on every buy but does **not** enforce a per-user max — stock is enforced globally via `stockRemaining`. Useful only as a "you've already bought this" UX hint. + +### Common revert reasons + +- **`ERC20: transfer amount exceeds allowance`** (or `ERC20InsufficientAllowance`) — wrong token approved, or allowance too low. Reread `paymentToken` from `ShopItemView` and approve that exact address with the spender set to the Boutique. +- **`ERC20: transfer amount exceeds balance`** (or `ERC20InsufficientBalance`) — user doesn't hold enough of `paymentToken`. Offer a swap. +- **`EnforcedPause()`** — contract is paused. Read `paused()`; nothing actionable until ops un-pauses. +- **Custom errors** (`ItemNotFound()` etc., see table above) — these come through as bare 4-byte selectors. Always preflight with `canPurchaseItem` so you can show the user the matching string. + +### Bankr execution + +Natural-language prompt (handles approval + buy in one shot): + +```bash +bankr agent prompt "Buy the Rat Skull Charm from the Cat Town boutique" +``` + +Or encode calldata directly. **Do NOT copy a hex price from this doc** — read `ShopItemView.price` live and encode that exact value. Below is a worked example for today's Rat Skull Charm price (1,500,000 DOTA), purely to show the byte layout: + +```bash +# Today's price hex for Rat Skull Charm (read live; will change): +PRICE_HEX=0x13da329b6336470000000 # = 1_499_999_999_999_999_974_834_176 wei DOTA + +# 1) approve DOTA → boutique for that price +# (cast calldata "approve(address,uint256)" 0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02 $PRICE_HEX) +bankr wallet submit \ + --to 0x5F09821CBb61e09D2a83124Ae0B56aaa3ae85B07 \ + --data 0x095ea7b3000000000000000000000000f9843bf01ae7ef5203fc49c39e4868c7d0ca7a02000000000000000000000000000000000000000000013da329b6336470000000 \ + --chain base + +# 2) purchaseItem(208) +# (cast calldata "purchaseItem(uint256)" 208) +bankr wallet submit \ + --to 0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02 \ + --data 0xd38ea5bf00000000000000000000000000000000000000000000000000000000000000d0 \ + --chain base +``` + +For a KIBBLE-priced item, swap the approve target to the KIBBLE token (`0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb`) and re-encode against the live `price`. Always re-encode from a fresh contract read; never reuse stale hex. + +## Notes + +- **Caching:** `getTodaysRotationDetails()` is stable within a UTC day — cache freely. The oracle moves with market — 1–5 min cache is reasonable. +- **Future rotations:** `previewRotationForDay(day, season)` supports "what's in the boutique tomorrow?" queries without waiting. +- **Season mismatch:** if `GameData.getCurrentSeason()` disagrees with `Boutique.getCurrentSeason()`, trust Boutique's for rotation questions (they should match, but boutique may lag a block). +- **No batch buy.** Two purchases = two `purchaseItem` calls. Approve the cumulative amount once if both items share the same `paymentToken`. diff --git a/cattown/references/boutique/contract.md b/cattown/references/boutique/contract.md deleted file mode 100644 index 143bf26c..00000000 --- a/cattown/references/boutique/contract.md +++ /dev/null @@ -1,152 +0,0 @@ -# Cat Town Boutique — contract + KIBBLE oracle reference - -The boutique is a fully onchain daily shop on Base. Every day at **00:00 UTC** the contract surfaces **3 items** selected deterministically from the current season's pool. No offchain API is needed — items, prices, stock, and rotation are all readable directly from the Boutique contract. - -This doc covers the **Boutique** contract (rotation + item state) and the **KIBBLE price oracle** (for USD conversion — the in-game UI shows KIBBLE only). - -## Addresses (Base, chain 8453) - -| Contract | Address | -|---------------------|----------------------------------------------| -| Boutique | `0xf9843bF01ae7EF5203fc49C39E4868C7D0ca7a02` | -| Kibble Price Oracle | `0xE97B7ab01837A4CbF8C332181A2048EEE4033FB7` | -| KIBBLE token | `0x64cc19A52f4D631eF5BE07947CABA14aE00c52Eb` | - -## Rotation model - -- Each day starts at **00:00 UTC**; `getCurrentDayNumber()` returns days since Unix epoch (`block.timestamp / 86400`). -- Rotation is deterministic from `(dayNumber, currentSeason)` — same day + same season = same 3 items. -- `itemsPerDay()` = **3** (constant). -- Season boundaries follow `GameData.getCurrentSeason()` (see [../world/contract.md](../world/contract.md)); each season has its own pool. -- The matching human-readable doc pages: - - Top-level shop: https://docs.cat.town/shops/boutique - - Spring: https://docs.cat.town/boutique/spring-fashion - - Summer: https://docs.cat.town/boutique/summer-fashion - - Autumn: https://docs.cat.town/boutique/autumn-fashion - - Winter: https://docs.cat.town/boutique/winter-fashion - -## Primary read — `getTodaysRotationDetails()` - -Returns today's 3 items as `ShopItemView[]` — full details in one call. Selector: `0x36362553`, no args. - -### `ShopItemView` fields - -| Field | Type | Notes | -|--------------------|-------------|-----------------------------------------------------------------------| -| `itemId` | `uint256` | Unique id for the shop item | -| `traitNames` | `string[]` | Parallel array of trait keys, e.g. `["Name","Rarity","Image","Slot","Shiny"]` | -| `traitValues` | `string[]` | Parallel array of values in the same order | -| `paymentToken` | `address` | Always the KIBBLE token | -| `price` | `uint256` | KIBBLE in **wei** (18 decimals) — divide by `10^18` for display | -| `stockRemaining` | `uint256` | Units still purchasable. `0` → sold out | -| `totalPurchased` | `uint256` | Units sold so far | -| `maxSupply` | `uint256` | Total ever available. `type(uint256).max` → uncapped | -| `startTime` | `uint64` | Unix seconds (0 = always available) | -| `endTime` | `uint64` | Unix seconds (0 = no end) | -| `availableSeasons` | `uint8` | Bitmask: `1=Spring`, `2=Summer`, `4=Autumn`, `8=Winter` | -| `isActive` | `bool` | Enabled by admin | -| `isPurchasableNow` | `bool` | Passes time + season gates | -| `isInTodaysRotation` | `bool` | In today's 3-item set | - -### Parsing the trait arrays - -`traitNames` and `traitValues` are parallel. Real trait keys on a live boutique item: - -| Trait key | Example value | Notes | -|---------------|------------------------------------------------------|------------------------------------------------------------| -| `Item Name` | `"White Longsleeve"` | Display name | -| `Rarity` | `"Rare"` | `Common` / `Uncommon` / `Rare` / `Epic` / `Legendary` | -| `Item Type` | `"Cosmetic"` | Almost always `Cosmetic` for boutique | -| `Source` | `"Boutique"` | Distinguishes from `Fishing`/`Gacha` in a joined view | -| `Slot` | `"Body"` | `Hat` / `Body` / `Eyewear` / `Companion` / etc. | -| `Sprite` | `"white-longsleeve"` | Internal asset id | -| `imageUrl` | `https://cdn.cat.town/nft/equipment/body/...` | Display image | -| **`Collection`** | `"Spring Fashion"` | **Collection label** — use this to tell the user which collection is currently rotating | -| `Flavor Text` | `"Clean and crisp like fresh spring linens."` | Optional color | -| `Sell Value` | `"0"` | Usually 0 for boutique (these aren't meant to be resold) | -| `coreId` | `"cmlz9n8f30008kz04flhruq6t"` | Internal database id | - -Boutique metadata is **onchain via the trait arrays** — don't cross-reference `/v2/items/master`. The `ShopItemView.traitNames`/`traitValues` are the source of truth. - -## Other useful reads - -| Function | Returns | Notes | -|---------------------------------------|-------------------------------|------------------------------------------------| -| `getTodaysRotation()` | `uint256[]` | Just today's 3 item ids (cheaper) | -| `getCurrentDayNumber()` | `uint256` | Days since Unix epoch | -| `getCurrentSeason()` | `uint8` | `0=Spring, 1=Summer, 2=Autumn, 3=Winter` | -| `getShopItem(itemId)` | `ShopItemView` | One item by id | -| `getAllShopItems()` | `ShopItemView[]` | Full catalog, active + inactive | -| `getItemsBySeason(season)` | `ShopItemView[]` | Season-specific pool | -| `previewRotationForDay(day, season)` | `uint256[]` | Future rotation preview (deterministic) | -| `getItemStock(itemId)` | `(max, purchased, remaining)` | Stock only | -| `dailyRotationEnabled()` | `bool` | Is daily rotation on (expected: true) | -| `itemsPerDay()` | `uint8` | Currently 3 | -| `defaultPaymentToken()` | `address` | KIBBLE | - -## KIBBLE → USD conversion - -### Oracle reads - -| Function | Selector | Returns | Scale | -|-----------------------|--------------|--------------------------------|-------------| -| `getKibbleUsdPrice()` | `0x00cbfbce` | `uint256` USD per 1 KIBBLE | **× 10^18** | -| `getEthUsdPrice()` | `0xa0a8045e` | `uint256` USD per 1 ETH | × 10^8 (Chainlink) | -| `getKibbleEthPrice()` | `0x47bb71e5` | `uint256` ETH per 1 KIBBLE | × 10^18 | - -**Watch the scale mismatch:** `getKibbleUsdPrice()` is `10^18`, but `getEthUsdPrice()` is `10^8`. Easy to mix up — use the right divisor per call. - -### Formula - -Boutique `price` is in KIBBLE wei (18 decimals). Oracle returns USD × `10^18` per 1 KIBBLE: - -``` -kibble_human = price / 10^18 # KIBBLE count -usd_per_kibble = rawKibbleUsdPrice / 10^18 # USD per 1 KIBBLE -usd_value = kibble_human * usd_per_kibble - = (price * rawKibbleUsdPrice) / 10^36 # BigInt-safe form -``` - -For integer cents: `usd_cents = (price * rawKibbleUsdPrice) / 10^34`. - -### Live example (captured during writing) - -- `getKibbleUsdPrice()` = `948,723,424,083,878` → **$0.0009487 per KIBBLE** -- 1,000 KIBBLE ≈ $0.95 -- 10,000 KIBBLE ≈ $9.49 -- 100,000 KIBBLE ≈ $94.87 - -The oracle tracks KIBBLE's real market price; re-read at least every few minutes if you care about accuracy. - -## Response pattern — "what's in the boutique today?" - -1. Read in parallel: `getTodaysRotationDetails()` (single call, 3 items) and `getKibbleUsdPrice()`. -2. For each `ShopItemView`: - - Parse `traitNames`/`traitValues` into a dict → pull `Name`, `Rarity`, `Slot`. - - `kibble_price = price / 10^18` - - `usd_price = (price * rawKibbleUsdPrice) / 10^36` - - Stock: if `stockRemaining == 0` → **"Sold Out"**; otherwise format as **`"{stockRemaining} of {maxSupply} remaining"`** — stockRemaining first, maxSupply second. The order matters: `stockRemaining` ≤ `maxSupply` always, so if the first number ever exceeds the second you've swapped them. Reread the struct fields if unsure. -3. Sort with the big-ticket order: **rarity DESC** (Legendary → Common), then **KIBBLE price DESC**, then name ASC. -4. Open the reply with the current season, and end with a link to the matching `docs.cat.town/boutique/...-fashion` page. - -### Example response (real data from today's rotation) - -> **Boutique today — Spring Fashion collection (Day 20566):** -> -> 1. **White Longsleeve** — Rare Body — **12,500 KIBBLE (~$11.86)** — 1 of 1 remaining -> 2. **Royal Blue Varsity** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 2 of 2 remaining -> 3. **Classic Academic Blouse** — Uncommon Body — **6,000 KIBBLE (~$5.69)** — 1 of 2 remaining -> -> Browse the other seasonal collections: -> - Spring: https://docs.cat.town/boutique/spring-fashion -> - Summer: https://docs.cat.town/boutique/summer-fashion -> - Autumn: https://docs.cat.town/boutique/autumn-fashion -> - Winter: https://docs.cat.town/boutique/winter-fashion -> - Overview: https://docs.cat.town/shops/boutique - -## Notes - -- **Purchase flow is out of scope for this revision.** It involves `approve(boutique, price_wei)` on KIBBLE, then `purchaseItem(itemId)` which mints an NFT and returns `mintedTokenId`. A future skill update will add the write path (watch the integer-vs-wei convention on `purchaseItem`'s `itemId` — likely raw uint256 not scaled). -- **Caching:** `getTodaysRotationDetails()` is stable within a UTC day — cache freely. The oracle moves with market — 1–5 min cache is reasonable. -- **Future rotations:** `previewRotationForDay(day, season)` supports "what's in the boutique tomorrow?" queries without waiting. -- **Season mismatch:** if `GameData.getCurrentSeason()` disagrees with `Boutique.getCurrentSeason()`, trust Boutique's for rotation questions (they should match, but boutique may lag a block). diff --git a/cattown/references/fish-raffle/api.md b/cattown/references/fish-raffle-api.md similarity index 100% rename from cattown/references/fish-raffle/api.md rename to cattown/references/fish-raffle-api.md diff --git a/cattown/references/fish-raffle/contract.md b/cattown/references/fish-raffle.md similarity index 97% rename from cattown/references/fish-raffle/contract.md rename to cattown/references/fish-raffle.md index 2366ca81..854b8917 100644 --- a/cattown/references/fish-raffle/contract.md +++ b/cattown/references/fish-raffle.md @@ -105,7 +105,7 @@ prize_pool_wei = poolBalance * current_tier.bps / 10000 per_winner_wei = prize_pool_wei / winnersPerDraw // equal split — NOT ranked ``` -Divide by `10^18` for KIBBLE; multiply by `getKibbleUsdPrice() / 10^18` for USD (see [../boutique/contract.md](../boutique/contract.md) for the oracle formula). +Divide by `10^18` for KIBBLE; multiply by `getKibbleUsdPrice() / 10^18` for USD (see [boutique.md](boutique.md) for the oracle formula). ### Live worked example (at time of writing) @@ -138,7 +138,7 @@ For small `tickets / totalTickets` the approximation is accurate to within ~1– | rank 2 (`bitcoinbov`) | 364 | **~63.7%** | | single-free-ticket claimant | 1 | 5 × 1 / 2855 = **~0.175%** | -If a user asks "what's my chance?", call the `/v1/tickets/leaderboard` endpoint (see [./api.md](./api.md)), find their address, read `totalCount`, and compute against `totalTickets`. +If a user asks "what's my chance?", call the `/v1/tickets/leaderboard` endpoint (see [fish-raffle-api.md](fish-raffle-api.md)), find their address, read `totalCount`, and compute against `totalTickets`. ## Claiming winnings — `claimPrize()` diff --git a/cattown/references/fishing/competition.md b/cattown/references/fishing-competition.md similarity index 100% rename from cattown/references/fishing/competition.md rename to cattown/references/fishing-competition.md diff --git a/cattown/references/fishing/drops.md b/cattown/references/fishing-drops.md similarity index 99% rename from cattown/references/fishing/drops.md rename to cattown/references/fishing-drops.md index d64044fe..b8314b99 100644 --- a/cattown/references/fishing/drops.md +++ b/cattown/references/fishing-drops.md @@ -76,7 +76,7 @@ If an agent is answering "what's special about Storm weather?" — use this filt ## Recipe: "what can I catch in this weather?" -1. Read world state from GameData (see [../world/contract.md](../world/contract.md)): +1. Read world state from GameData (see [world.md](world.md)): ``` GameData.getGameState() → (season, timeOfDay, isWeekend, worldEvent, weather) ``` diff --git a/cattown/references/gacha/api.md b/cattown/references/gacha-api.md similarity index 97% rename from cattown/references/gacha/api.md rename to cattown/references/gacha-api.md index 6bd927cd..7d50bda7 100644 --- a/cattown/references/gacha/api.md +++ b/cattown/references/gacha-api.md @@ -68,4 +68,4 @@ Not gacha-specific, but useful: the general item catalog at `GET /v2/items/maste Use this when a user asks "what can I pull right now?" or "show me the rarest thing in the summer gacha." -Full shape in [../fishing/drops.md](../fishing/drops.md) (same endpoint, same shape). +Full shape in [fishing-drops.md](fishing-drops.md) (same endpoint, same shape). diff --git a/cattown/references/gacha/contract.md b/cattown/references/gacha.md similarity index 99% rename from cattown/references/gacha/contract.md rename to cattown/references/gacha.md index bf277aee..6a9b5ca5 100644 --- a/cattown/references/gacha/contract.md +++ b/cattown/references/gacha.md @@ -125,7 +125,7 @@ Public, no auth. Returns the user's capsule NFTs (not a global feed). Used as th Response shape (array of items): `[{ id, name, rarity, imageUrl, traitNames, traitValues, ... }, ... ]`. The `id` is the key field for the ordering trick. A 500 response can mean the address has never pulled; treat it as an empty array. -Full API shape notes: [./api.md](./api.md). +Full API shape notes: [gacha-api.md](gacha-api.md). ## Reads cheat-sheet diff --git a/cattown/references/kibble/tokenomics.md b/cattown/references/kibble-tokenomics.md similarity index 97% rename from cattown/references/kibble/tokenomics.md rename to cattown/references/kibble-tokenomics.md index 2f97aedb..cb879fb8 100644 --- a/cattown/references/kibble/tokenomics.md +++ b/cattown/references/kibble-tokenomics.md @@ -95,4 +95,4 @@ Offer a natural follow-up: "Want me to walk you through staking?" or "Want the c ## Other tokenomics Jasper surfaces (not yet wired here) - **Community Pot** — KIBBLE held for task-completion rewards. Jasper quotes it as a % of circulating. Not yet covered in this skill. -- **Price / market cap** — Jasper doesn't quote prices. For USD conversion use the Kibble Price Oracle (`getKibbleUsdPrice()`) — see [../boutique/contract.md](../boutique/contract.md) for the exact formula (1e18-scaled, not Chainlink 1e8). +- **Price / market cap** — Jasper doesn't quote prices. For USD conversion use the Kibble Price Oracle (`getKibbleUsdPrice()`) — see [boutique.md](boutique.md) for the exact formula (1e18-scaled, not Chainlink 1e8). diff --git a/cattown/references/sell-items/contract.md b/cattown/references/sell-items.md similarity index 100% rename from cattown/references/sell-items/contract.md rename to cattown/references/sell-items.md diff --git a/cattown/references/staking/api.md b/cattown/references/staking-api.md similarity index 98% rename from cattown/references/staking/api.md rename to cattown/references/staking-api.md index 45d3acfd..f8a8f99e 100644 --- a/cattown/references/staking/api.md +++ b/cattown/references/staking-api.md @@ -2,7 +2,7 @@ Two public JSON endpoints served from `https://api.cat.town`. **No authentication** — plain GET, no headers required. -Use these when you need leaderboard or historical-deposit data without paying RPC costs. For live position reads (current stake, pending rewards, unlock state), go onchain — see [contract.md](contract.md). +Use these when you need leaderboard or historical-deposit data without paying RPC costs. For live position reads (current stake, pending rewards, unlock state), go onchain — see [staking.md](staking.md). --- diff --git a/cattown/references/staking/contract.md b/cattown/references/staking.md similarity index 100% rename from cattown/references/staking/contract.md rename to cattown/references/staking.md diff --git a/cattown/references/world/calendar.md b/cattown/references/world-calendar.md similarity index 96% rename from cattown/references/world/calendar.md rename to cattown/references/world-calendar.md index 48e8e0fe..32f39091 100644 --- a/cattown/references/world/calendar.md +++ b/cattown/references/world-calendar.md @@ -56,7 +56,7 @@ Paulie's Fish Raffle runs weekly. - **5 winners per draw**, chosen via Chainlink VRF. Prize pool is split **equally** among the 5 — ranks are ordering only, not prize weighting. - Prize pool is a tier-based fraction of the `FreeToPlayPool` balance. 8 tiers keyed to `totalTickets` sold that round, paying 30 bps (0.3%) at 0 tickets up to 100 bps (1.0%) once sales cross 5,500. More tickets sold → bigger cut of the pool paid out. -Full contract + API reference, free-ticket claim flow, tier table, and chance-to-win math: [../fish-raffle/contract.md](../fish-raffle/contract.md), [../fish-raffle/api.md](../fish-raffle/api.md). +Full contract + API reference, free-ticket claim flow, tier table, and chance-to-win math: [fish-raffle.md](fish-raffle.md), [fish-raffle-api.md](fish-raffle-api.md). ## Weekly fishing competition (Sat–Sun) — Isabella @@ -80,7 +80,7 @@ Isabella hosts the weekend fishing competition. Only catches made during her win - The leaderboard is only meaningful Saturday–Monday; outside that window it reflects the prior week's results. -Full contract + API reference, prize-pool math (10% top-10 / 80% treasures / 10% stakers), and active/inactive response patterns: [../fishing/competition.md](../fishing/competition.md). +Full contract + API reference, prize-pool math (10% top-10 / 80% treasures / 10% stakers), and active/inactive response patterns: [fishing-competition.md](fishing-competition.md). --- diff --git a/cattown/references/world/contract.md b/cattown/references/world.md similarity index 100% rename from cattown/references/world/contract.md rename to cattown/references/world.md