Skip to content

Store and Rewards ​

Sell items in your game or app for your own token, give items away as rewards, and unlock content for the people who hold your token. Buyers pay you directly on chain; Chain Daddy keeps the catalog, the item ledger and a signed receipt for every purchase.

There are three ways to sell:

  • Your own store, in your game. Your game shows its own catalog in its own UI and talks to the store API from its server with a store key. This is the main path, and the rest of this page is built around it.
  • An app on your token page. Your app asks the page to run the purchase with one action; the page handles the wallet. See Selling from an app on your token page.
  • The Item showcase on your token page. A ready-made widget for the same flow, for creators without a game. See The Item showcase.

Every action is an API call, a chaindaddy iap … command and an MCP tool, and every write can be retried safely.

Sell from your own game ​

0. Set up once ​

bash
npm install -g @chaindaddy/cli@beta
export CHAINDADDY_API_KEY=cd_live_…            # your account key (see Authentication)

# Open the store for your token page (chain key + registration id)
chaindaddy iap store open --chain base --crown 8 --json
#   → {"ok":true,"idempotencyKey":"…","store":{"id":"3f0c…","feeBps":0,"live":true,…}}

# Add an item and put it on sale (see Items below for the JSON)
chaindaddy iap sku create 3f0c… --file car-red.json
chaindaddy iap sku update 3f0c… car.red --status active

# A key for your game server: create orders, read and spend items
chaindaddy iap key create 3f0c… --name game-server \
  --scope orders:read orders:write entitlements:read entitlements:write
#   prints the cd_iap_… secret ONCE: put it in your server's secrets

1. Your server creates the order ​

When a player taps Buy, your server asks for an order for that player's wallet. appAccountToken is your own id for the player; it comes back on the order, the receipt and every event, so you can match a payment to an account.

bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/orders \
  -H "Authorization: Bearer $CHAINDADDY_IAP_KEY" \
  -H "Idempotency-Key: cart-5521" \
  -H "Content-Type: application/json" \
  -d '{"storeId":"3f0c…","sku":"car.red","quantity":1,"wallet":"0xabc…","appAccountToken":"player-42"}'

Same thing from the CLI: chaindaddy iap order create 3f0c… --sku car.red --wallet 0xabc… --app-account-token player-42. MCP: iap_create_order.

The order locks the price, the payees and a deadline about 20 minutes out (5 minutes for an item priced in dollars and paid in your token or the chain's coin), and says how to pay:

json
{
  "id": "0x9e1f…",
  "storeId": "3f0c…",
  "sku": "car.red",
  "quantity": 1,
  "wallet": "0xabc…",
  "appAccountToken": "player-42",
  "currency": "0xToken…",
  "currencyKind": "token",
  "currencySymbol": "FLUXGP",
  "currencyDecimals": 18,
  "amount": "5000000000000000000000",
  "status": "pending",
  "expiresAt": "2026-10-02T18:24:11Z",
  "payment": {
    "kind": "evm",
    "chainId": "eip155:8453",
    "checkout": "0xCheckout…",
    "order": {
      "orderId": "0x9e1f…",
      "token": "0xToken…",
      "payees": ["0xPayout…"],
      "amounts": ["5000000000000000000000"],
      "deadline": 1791051851
    },
    "total": "5000000000000000000000",
    "permit": { "supported": true, "name": "FLUX GP", "version": "1" },
    "permitTypedData": { "domain": { "…": "…" }, "types": { "…": "…" }, "primaryType": "Permit", "message": { "…": "…" } },
    "calls": {
      "approve": { "to": "0xToken…", "data": "0x095ea7b3…", "value": "0" },
      "pay": { "to": "0xCheckout…", "data": "0x…", "value": "0" }
    }
  }
}

Pass payment to the game client. Use one Idempotency-Key per purchase attempt (your cart id, say): a retry returns the same order instead of a second one.

2. The player pays from their wallet ​

The client has two ways to pay, and never needs to know an amount:

  • calls: raw transactions to send in order: approve, the ERC-20 approval, then pay, the checkout payment. When the item is paid in the chain's coin there is no approve, and pay.value is the total: the coin travels with the payment.
  • permitTypedData: present only when the currency supports permits (permit.supported; never for the chain's coin). The player signs it, and one transaction calls payWithPermit on the checkout. One signature and one transaction, no separate approval.

With viem:

typescript
import { createPublicClient, createWalletClient, custom, http, parseAbi, parseSignature } from 'viem';
import { base } from 'viem/chains';

const checkoutAbi = parseAbi([
  'function payWithPermit((bytes32 orderId,address token,address[] payees,uint256[] amounts,uint256 deadline) o, uint256 permitDeadline, uint8 v, bytes32 r, bytes32 s)',
]);

const publicClient = createPublicClient({ chain: base, transport: http() });
const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum) });
const [account] = await wallet.requestAddresses();

async function pay(payment): Promise<`0x${string}`> {
  if (payment.permitTypedData) {
    const td = payment.permitTypedData;
    const sig = await wallet.signTypedData({
      account, domain: td.domain, types: td.types, primaryType: td.primaryType, message: td.message,
    });
    const { r, s, v, yParity } = parseSignature(sig);
    const o = payment.order;
    return wallet.writeContract({
      account,
      address: payment.checkout,
      abi: checkoutAbi,
      functionName: 'payWithPermit',
      args: [
        { orderId: o.orderId, token: o.token, payees: o.payees, amounts: o.amounts.map(BigInt), deadline: BigInt(o.deadline) },
        BigInt(td.message.deadline),
        Number(v ?? BigInt(yParity + 27)),
        r,
        s,
      ],
    });
  }
  const { approve, pay: checkoutPay } = payment.calls;
  if (approve) { // absent when the item is paid in the chain's coin
    const hash = await wallet.sendTransaction({ account, to: approve.to, data: approve.data });
    await publicClient.waitForTransactionReceipt({ hash }); // the approval must land before the payment
  }
  // value is the total when paying in the chain's coin, 0 otherwise
  return wallet.sendTransaction({ account, to: checkoutPay.to, data: checkoutPay.data, value: BigInt(checkoutPay.value) });
}

Solana. payment is {"kind":"solana", "transaction", "reference", "memo", …}: transaction is a base64 unsigned transaction, built by the API, that pays you (and the fee, if any) directly, in the token, SOL or USDC the item is paid in. The player's wallet signs and sends it; the resulting signature takes the place of the transaction hash below.

3. Hand the transaction to your server ​

The client gives the hash to your server, which submits it:

bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/orders/0x9e1f…/submit \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: cart-5521-submit" \
  -d '{"txHash":"0x5b7c…"}'          # Solana: {"signature":"…"}

Submitting is optional. Chain Daddy watches the checkout on every chain and moves the order to paid within a couple of seconds of the payment landing on chain, whether or not anyone submits it, so an order still settles if your client crashes after paying. Submit when you already have the hash: it checks that one transaction immediately.

4. Wait for confirmed ​

Poll the order with the store key until it is confirmed, or subscribe to the iap.order.confirmed webhook:

bash
curl https://api.chaindaddy.io/api/v2/iap/orders/0x9e1f… \
  -H "Authorization: Bearer $CHAINDADDY_IAP_KEY"
#   → {"status":"confirmed","confirmations":5,"requiredConfirmations":5,"receipt":"eyJhbGciOiJFZERTQSIs…",…}

paid means the payment is on chain and gathering confirmations; do not hand over the item yet. Polling also moves it along: each read of a paid order re-checks its payment on chain, so an order you poll every second or two reads confirmed as soon as it is deep enough. See How a purchase settles for how long that is on each chain.

5. Verify the receipt ​

A confirmed order carries a signed receipt. Check it offline against the public keys, or online; see Verifying receipts.

6. Read and spend items on your server ​

The item ledger is the source of truth for what a player owns:

bash
chaindaddy iap entitlements 3f0c… --wallet 0xabc… --json                                # GET …/entitlements?wallet=
chaindaddy iap consume 3f0c… --wallet 0xabc… --sku boost --qty 1 --idempotency-key race-7731-boost-2

Tools for agents ​

The MCP server exposes the same calls as iap_* tools. It reads your account key from CHAINDADDY_API_KEY and a store key from CHAINDADDY_IAP_KEY:

json
{
  "mcpServers": {
    "chaindaddy": {
      "command": "npx",
      "args": ["@chaindaddy/mcp"],
      "env": {
        "CHAINDADDY_API_KEY": "cd_live_…",
        "CHAINDADDY_IAP_KEY": "cd_iap_…"
      }
    }
  }
}
MCP toolCLIHTTP
iap_open_storeiap store openPOST /api/v2/iap/stores
iap_list_storesiap store listGET /api/v2/iap/stores
iap_list_skusiap sku listGET /api/v2/iap/stores/{id}/skus
iap_create_skuiap sku createPOST /api/v2/iap/stores/{id}/skus
iap_update_skuiap sku update / archivePATCH / DELETE /api/v2/iap/stores/{id}/skus/{sku}
iap_create_orderiap order createPOST /api/v2/iap/orders
iap_list_ordersiap order listGET /api/v2/iap/stores/{id}/orders
iap_get_orderiap order showGET /api/v2/iap/stores/{id}/orders/{orderId}
iap_refund_orderiap order refundPOST /api/v2/iap/stores/{id}/orders/{orderId}/refund
iap_get_entitlementsiap entitlementsGET /api/v2/iap/stores/{id}/entitlements
iap_grant / iap_consume / iap_revokeiap grant / consume / revokePOST /api/v2/iap/stores/{id}/{grants,consume,revoke}
iap_create_dropiap drop createPOST /api/v2/iap/stores/{id}/drops
iap_set_drop_statusiap drop statusPOST /api/v2/iap/stores/{id}/drops/{dropId}/status
iap_add_drop_recipientsiap drop recipientsPOST /api/v2/iap/stores/{id}/drops/{dropId}/recipients
iap_awardiap drop awardPOST /api/v2/iap/stores/{id}/drops/{dropId}/award
iap_create_store_keyiap key createPOST /api/v2/iap/stores/{id}/keys
iap_list_eventsiap eventsGET /api/v2/iap/stores/{id}/events

The full request and response schemas are in the OpenAPI spec under the Creator Store tag.

Who can open a store ​

A store needs a Developer plan, or membership in the Developer Beta Program in good standing. Without one, every creator and server route answers 403 with code IAP_PLAN_REQUIRED, and the token page's Rewards tab shows what it would unlock. GET /api/v2/iap/eligibility tells a signed-in wallet where it stands.

You open a store as a wallet that manages the token page; changing the payout address takes the owner. The store stays live while the wallet that opened it keeps its plan. If that lapses, the store stops taking new orders and store keys stop working, but buyers can always read the items and receipts they already have.

Where you manage it ​

WhatWhere
Items, orders, players, store keys, events, settings and purchase notificationsDeveloper portal → Store: chaindaddy.io/_developer/store. Pick a store, then /_developer/store/<storeId>/skus (or orders, players, keys, events, settings)
Rewards: claim links, pushes and game-awarded dropsYour token's manager: Community → Rewards, at https://chaindaddy.io/_/<SYMBOL>/rewards
Showing items on your token pageAdd the Item showcase widget in the token page editor
Everything, from an agent or a game serverThe API below, chaindaddy iap …, or the iap_* MCP tools

A Developer-plan customer reaches the Store section without joining the developer program; the program's agreement is only needed to publish apps.

Authentication ​

Store routes take one credential, in Authorization: Bearer …. Unlike the rest of the API, they do not also need an X-API-Key header:

CredentialUse it for
Your account: a cd_live_ API key or a wallet sessionCreator and admin work, including opening stores and managing store keys
A store key, cd_iap_…Your game server. Works on one store, limited to the scopes you gave it. Cannot open stores, list your stores or manage keys. Send only this key
NonePublic reads (a store's catalog, a drop), GET /api/v2/iap/jwks, receipt verification and …/orders/{id}/submit

The CLI reads the store key from --key or CHAINDADDY_IAP_KEY; the MCP server from CHAINDADDY_IAP_KEY.

Open a store ​

bash
chaindaddy iap store open --chain base --crown 8
bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/stores \
  -H "Authorization: Bearer $CHAINDADDY_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" -H "Content-Type: application/json" \
  -d '{"chainKey":"base","crownId":8}'

One store per token page, so opening it again returns the same store. Items are paid in the token itself unless you choose otherwise for an item (What the buyer pays in). Sales pay out to payoutAddress, which starts as the owner's wallet:

bash
chaindaddy iap store update 3f0c… --payout 0xYourTreasury   # owner only
chaindaddy iap store update 3f0c… --pause                      # stop selling; --resume to restart

EVM chains and Solana are both supported. GET /api/v2/iap/config lists the chains the store runs on, how many confirmations each needs, and the currencies besides your token that each chain offers.

Items (SKUs) ​

Each item has a sku key, unique in your store: lowercase letters, digits, ., _ and -, up to 64 characters, starting with a letter or digit. Prices are base-unit strings of the item's currency, which is your token unless you choose another (for an 18-decimal token, 5000 tokens is "5000000000000000000000"). The CLI's --price-tokens 5000 does that conversion for you. We recommend pricing in dollars and taking payment in your token: a steady price for the buyer, and every sale is demand for your token.

TypeA purchase givesGood for
consumablequantity added to a balance; your game spends it with consumeCoins, lives, boosts
non_consumableOwned, at most one per walletSkins, cars, tracks
timedAccess until an expiry, extended by durationSeconds × quantity from now or from the current expiry, whichever is laterSeason passes. Renewing is another purchase; nothing charges a wallet automatically
unlockNever sold. Held for as long as its rule holds, checked liveHolder perks

New SKUs start as draft. Set status to active to list them, and archive them to take them down (chaindaddy iap sku archive <store> <sku>); archiving never takes an item away from anyone who has it.

The examples below are from FLUX GP, a racing game.

A car skin (non_consumable) ​

json
{
  "sku": "car.red",
  "type": "non_consumable",
  "name": "Red Racer",
  "description": "Gloss red livery for your starter car.",
  "imageUrl": "https://fluxgp.example/items/car-red.png",
  "price": "5000000000000000000000",
  "metadata": { "carId": "starter", "livery": "red" },
  "status": "active"
}

metadata is yours: any JSON your game needs to render or apply the item. It comes back on every SKU read.

A boost pack (consumable bundle) ​

A bundle grants other items instead of itself. Here, buying one boost.pack10 adds 10 to the player's boost balance. boost has no price, so it cannot be bought on its own; it is reward-only.

json
[
  { "sku": "boost", "type": "consumable", "name": "Nitro boost", "status": "active" },
  {
    "sku": "boost.pack10",
    "type": "consumable",
    "name": "Boost pack ×10",
    "price": "1000000000000000000000",
    "grants": [{ "sku": "boost", "quantity": 10 }],
    "status": "active"
  }
]

When a player fires a boost, the game server spends one:

bash
chaindaddy iap consume 3f0c… --wallet 0xabc… --sku boost --qty 1 --idempotency-key race-7731-boost-2

If the balance is too low the call answers 409 INSUFFICIENT_QUANTITY and nothing changes.

A season pass (timed) ​

json
{
  "sku": "pass.s1",
  "type": "timed",
  "name": "Season 1 pass",
  "price": "20000000000000000000000",
  "durationSeconds": 7776000,
  "saleStartsAt": "2026-10-01T00:00:00Z",
  "saleEndsAt": "2026-12-31T00:00:00Z",
  "status": "active"
}

90 days per purchase. A player who buys twice gets 180 days.

A holders-only track (unlock) ​

Held by any wallet with at least 1,000 tokens, for as long as it keeps them. Nobody buys it; it appears in that wallet's items with "source": "unlock".

json
{
  "sku": "track.neon",
  "type": "unlock",
  "name": "Neon Circuit",
  "rule": { "hold": { "min": "1000000000000000000000" } },
  "status": "active"
}

Price in dollars ​

Your token's price moves; the price of a coin pack in your game probably should not. Set priceUsdMicros instead of price and the item always costs about the same in dollars, whatever the token trades at. The unit is a millionth of a dollar, so $5 is 5000000.

This is the setup we recommend: priced in dollars, paid in your token. The buyer sees a steady price, and every purchase is someone getting and spending your token. It needs nothing extra: an item with no currency is paid in your token.

json
{
  "sku": "coins.500",
  "type": "consumable",
  "name": "500 coins",
  "priceUsdMicros": 5000000,
  "minPrice": "1000000000000000000000",
  "maxPrice": "500000000000000000000000",
  "status": "active"
}

From the CLI: chaindaddy iap sku create 3f0c… --sku coins.500 --type consumable --name "500 coins" --price-usd 5 --min-price-tokens 1000 --max-price-tokens 500000 --status active. MCP: priceUsdMicros, minPrice and maxPrice on iap_create_sku / iap_update_sku.

An item has either a fixed price (price) or a dollar price (priceUsdMicros), never both. When you update an item, setting one clears the other.

What the buyer pays. Nothing on chain knows about dollars. Each time an order is created, we convert the dollar price to an exact amount of the item's currency at that moment and lock it on the order, the same way a fixed price is locked:

  • The rate is live. We read your token's price, and the chain's coin's, from DexScreener every 15 seconds, and a quote uses the average of the last 2 minutes of readings. An average means one odd trade cannot move what a buyer pays; a short window means a fast-moving token is still quoted at what it trades at now.
  • The amount is rounded up to 4 significant figures, so a wallet shows a clean number like 140,100 rather than 140,056.27. That is at most about 0.1% above the exact conversion.
  • minPrice and maxPrice (optional, base units per item) cap the conversion. Use them so a crash in your token's price cannot make an item cost a fortune in tokens, and a spike cannot make it almost free.
  • The platform fee (see The fee) is taken from the converted amount, exactly as from a fixed price.
  • An item paid in USDC needs no rate at all: $5 is exactly 5 USDC.

The order's amount is what the buyer sees in the wallet and approves: an exact amount, fixed for the order's life. An order converted to your token or the chain's coin can be paid for 5 minutes, long enough for a wallet round trip and short enough that nobody holds a quote through a big move; other orders get about 20. If the price moves after that, the next order gets a new amount; an order already created never changes. A dollar-priced order also carries priceUsdMicros and usdPerToken, the dollar price of one whole unit of the currency that the quote used ("1" for USDC), and so does its receipt.

When there is no price. We would rather not sell than sell at a wrong price. Creating an order for a dollar-priced item paid in your token or the chain's coin answers 503 PRICE_UNAVAILABLE when:

  • the newest reading is more than 45 seconds old (the price feed has stopped),
  • there are fewer than 3 readings in the last 2 minutes (a new store's price takes about 30 seconds to warm up),
  • the newest reading is more than 15% away from the 2-minute average (the price is moving too fast to quote fairly), or
  • DexScreener has no price for the token at all (it follows no pool that trades it).

This usually clears within a minute. Tell the player to try again shortly. A fixed price, and a dollar price paid in USDC, never depend on any of this. On test networks, which no market prices, the rate comes from our own price records instead.

Your store's price field (on GET /api/v2/iap/stores/{id} and on the public store) shows what a dollar-priced item paid in your token would convert at right now: {"usdPerToken": "0.0000357", "source": "feed", "asOf": "…"}, or {"unavailable": "the price is moving too fast"} when an order would be refused. chaindaddy iap store show prints it.

In the public catalog (GET /api/v2/iap/public/stores/…) a dollar-priced item carries "priceIsEstimate": true and a price that is an estimate for one item at the current rate, for display ("$5.00 ≈ 140,100 FLUXGP"). It is null when no price is available right now. An item paid in USDC is exact, so its price is not an estimate. The locked amount on the order is the one that counts.

What the buyer pays in ​

Items are paid in your token by default, and we recommend keeping it that way: the point of a token page is that people buy your token. When an item suits another currency better, set its currency:

currencyThe buyer pays inprice, minPrice, maxPrice are
token (default)Your tokenBase units of your token
nativeThe chain's coin: ETH, BNB, POL, SOL…Base units of the coin (wei, lamports)
usdcThe chain's dollar stable: USDC (USDG on Robinhood Chain; Circle USDC on Solana)Base units of the stable (check its decimals: BNB Chain's is 18)

GET /api/v2/iap/config lists each chain's currencies, each with its address, symbol and decimals. A chain without a dollar stable (Gnosis) refuses usdc.

json
{
  "sku": "coins.500",
  "type": "consumable",
  "name": "500 coins",
  "currency": "usdc",
  "priceUsdMicros": 5000000,
  "status": "active"
}

From the CLI: chaindaddy iap sku create 3f0c… --sku coins.500 --type consumable --name "500 coins" --currency usdc --price-usd 5 --status active. MCP: currency on iap_create_sku / iap_update_sku.

  • A dollar price in USDC is exact: no rate, no rounding, no PRICE_UNAVAILABLE. It takes no minPrice or maxPrice.
  • A dollar price in the chain's coin converts at the coin's live price, with the same guards as your token.
  • Changing an item's currency changes what its base units mean. An update that changes currency on an item with a fixed price must set price (or priceUsdMicros) in the same call, and it clears minPrice and maxPrice. A dollar price means the same in any currency and carries over.
  • An unlock is never sold, so it has no currency.
  • The platform fee is taken in the currency paid.

Every SKU read carries currency, currencyAddress, currencySymbol and currencyDecimals. An order carries currencyKind, currencySymbol and currencyDecimals beside currency (the address), and the receipt's currency is what the buyer actually paid in.

Paying in the chain's coin. On EVM, payment.calls has no approve, payment.calls.pay.value is the total, and permit is null. On Solana, payment.transaction has the same shape as for a token, made of plain SOL transfers.

It is still the buyer paying you. Choosing the coin or USDC changes nothing about custody: the buyer's own transaction pays you (and the fee, if any) directly, exactly as with your token. Chain Daddy never holds the payment. A payment in another currency than the order's, such as your token sent against a USDC order, does not pay the order: it is recorded as an order.mismatch event and grants nothing (see What you are responsible for).

Limits on a sale ​

FieldEffect
maxSupplyTotal that can ever be sold. Stock is held while an order waits for payment and released if it expires
maxPerWalletMost one wallet may own (forced to 1 for non_consumable)
saleStartsAt, saleEndsAtThe sale window
ruleOn a SKU for sale: who may buy it. On an unlock: who holds it
sortOrderOrder in the catalog

Rules ​

One small language decides who may buy an item, who holds an unlock and who may claim a drop.

json
{
  "all": [
    { "hold": { "min": "1000000000000000000000" } },
    { "hold": { "min": "5", "token": "0x…", "chainId": "eip155:8453" } },
    { "owns": { "sku": "car.red", "min": 1 } },
    { "any": [{ "owns": { "sku": "pass.s1" } }, { "not": { "owns": { "sku": "banned" } } }] }
  ]
}
NodeTrue when
all / anyEvery / at least one child is true
notIts child is false
holdThe wallet holds at least min base units of your token, or of token on chainId when given. Read live
ownsThe wallet owns at least min (default 1) of a SKU in your store

A rule may nest 4 levels deep and have 32 nodes; anything larger is rejected when you save it.

Rewards: drops ​

A drop gives items away. Nobody pays, no tokens move and nothing costs gas.

ModeHow items arrive
claimYou share a link; eligible wallets claim from it
pushYou upload a list of wallets; activating the drop delivers to all of them
awardYour game server awards a wallet, e.g. for finishing a race
json
{
  "name": "Podium finish",
  "mode": "award",
  "items": [{ "sku": "boost", "quantity": 3 }],
  "perWalletLimit": 5,
  "maxClaims": 10000,
  "startsAt": "2026-10-01T00:00:00Z",
  "endsAt": "2026-12-31T00:00:00Z"
}
bash
chaindaddy iap drop create 3f0c… --file podium.json --json          # starts as draft
chaindaddy iap drop status 3f0c… <dropId> active
chaindaddy iap drop award 3f0c… <dropId> --wallet 0xabc… --idempotency-key race-7731-podium

audience takes a rule (holders of at least X, owners of a SKU), and allowlistOnly: true limits a drop to the wallets you add with chaindaddy iap drop recipients <store> <drop> --csv wallets.csv. perWalletLimit, maxClaims and the window are enforced on every claim and award, so a bug or a replay in your game cannot give out more than you set.

The Item showcase on your token page ​

No game needed: add the Item showcase widget from Add Widget on your token page, and visitors can buy your items right there. Each item shows its picture, name and price (with the dollar price when you priced it in dollars) and a Buy button that opens the page's purchase sheet. Items that can't be bought right now say why: sold out, holders only, or not on sale.

What a visitor already holds is marked on the card: Owned for a one-off item, and Buy again with the amount held for items that stack. Under your items, a signed-in visitor sees everything they hold from your store, including live unlocks, with amounts and expiry dates. A visitor who isn't signed in sees a link to sign in.

Open the widget's settings (the gear) to change:

SettingDefaultWhat it does
TitleItemsThe heading above your items. Leave it blank for none.
Items to showemptyThe items to feature, in order. Empty shows every item that has a price. On the widget itself, Choose items lets you tick them from your store and put them in order.
Show the viewer's itemsonThe "Your items" list under your items.
Columns2 per row2 or 3 item cards per row.

The catalog and prices always come from your store: add, price and archive items in the developer portal (Items in your store). Only active items can be shown, and an archived item drops out of the widget by itself. Visitors see nothing until your store has something to show; you see a hint with a link to add items.

Selling from an app on your token page ​

The convenient path when your game runs as an app on your token page: no server, no payment code. The app never touches the player's wallet or the amounts; it asks the page to run the purchase:

  1. Declare the iap:purchase permission in your manifest. The install screen shows it as "Sell items from this token's store".
  2. Dispatch the action from your widget (see Actions):
typescript
const res = await onAction({
  type: 'iap-purchase',
  sku: 'car.red',
  quantity: 1,
  appAccountToken: player.id, // optional: your own id for this player, echoed on the order and receipt
});
// res.data: { status: 'confirmed' | 'paid' | 'cancelled' | 'failed', orderId?, receipt? }

The page opens its own purchase sheet, the player approves in their wallet, and the action resolves. paid means the payment is on chain and still gathering confirmations; the item arrives when it reaches confirmed.

To show the catalog and the player's items, read with api-call:

typescript
await onAction({ type: 'api-call', method: 'GET', endpoint: '/api/v2/iap/public/stores?chainKey=base&crownId=8' });
await onAction({ type: 'api-call', method: 'GET', endpoint: `/api/v2/iap/me/entitlements?storeId=${storeId}` });

Apps may read /api/v2/iap/public/…, /api/v2/iap/me/… and /api/v2/iap/orders/…, with the viewer's own session.

How a purchase settles ​

pending ──▶ paid ──▶ confirmed ──▶ refunded
   │
   └──▶ expired
StatusMeaning
pendingThe price, quantity, wallet and payees are locked for about 20 minutes (5 for a dollar price converted to your token or the coin) while the buyer pays
paidThe payment is on chain and gathering confirmations
confirmedEnough confirmations: the items are in the wallet and a signed receipt exists
expiredThe window passed with no payment. Held stock is released
refundedYou refunded the buyer and recorded it. The items and the receipt are revoked

The number of confirmations each chain needs is in GET /api/v2/iap/config; Solana orders confirm when the payment is finalized. A payment that disappears in a chain reorganisation before it is confirmed sends the order back to pending, and it can still be paid before its deadline.

An order reads paid within a couple of seconds of its payment landing on chain. How long it then takes to read confirmed, at most:

ChainConfirmationsPayment landed → confirmed
Base5~10 seconds
Arbitrum One20~7 seconds
BNB Chain15~13 seconds
Solanafinalized~15 seconds
Ethereum12~2¼ minutes
Polygon64~2¼ minutes

These follow the chain's own block times, so a congested or quiet chain can take a little longer.

Stray payments never change an order. The checkout records a payment against the whole order (its id, currency, payees, amounts and deadline), not the order id alone. So a payment that names an order's id with another currency, payee or amount cannot block the real one: the buyer still pays as normal, and the stray payment is recorded as an order.mismatch event and grants nothing. The same order can only be paid once, but a client that changes the deadline can pay it a second time; that is recorded as order.duplicate_payment and grants nothing more. Neither happens with the payment block used as given.

Verifying receipts on a game server ​

Every confirmed order gets a receipt: a compact JWS signed with Ed25519 (alg: EdDSA). Its payload:

json
{
  "iss": "https://chaindaddy.io",
  "typ": "iap-receipt+v1",
  "receiptId": "…",
  "orderId": "0x…",
  "storeId": "3f0c…",
  "chainId": "eip155:8453",
  "crownId": 8,
  "sku": "car.red",
  "skuType": "non_consumable",
  "quantity": 1,
  "wallet": "0xabc…",
  "appAccountToken": "player-42",
  "currency": { "address": "0x…", "symbol": "FLUXGP", "decimals": 18 },
  "amount": "5000000000000000000000",
  "feeAmount": "0",
  "payment": { "txHash": "0x…", "logIndex": 3, "blockNumber": 51900000, "payer": "0xabc…" },
  "purchasedAt": "2026-10-02T18:04:11Z",
  "confirmedAt": "2026-10-02T18:04:33Z",
  "expiresAt": null,
  "iat": 1791050673
}

currency is what the buyer paid in: your token, the chain's coin (the zero address on EVM, 11111111111111111111111111111111 on Solana) or the dollar stable. amount and feeAmount are base units of it.

A receipt for a dollar-priced item also carries priceUsdMicros (per item) and usdPerToken (the dollar price of one whole unit of currency the order was quoted at, a decimal string; "1" for USDC). Neither field appears on a fixed-price receipt, so parse them as optional.

Offline, against the public keys at GET /api/v2/iap/jwks (current and previous, so rotation does not break you):

typescript
import { createRemoteJWKSet, jwtVerify } from 'jose';

const JWKS = createRemoteJWKSet(new URL('https://api.chaindaddy.io/api/v2/iap/jwks'));

export async function verifyReceipt(jws: string, storeId: string) {
  const { payload } = await jwtVerify(jws, JWKS, {
    issuer: 'https://chaindaddy.io',
    algorithms: ['EdDSA'],
  });
  if (payload.typ !== 'iap-receipt+v1') throw new Error('not a store receipt');
  if (payload.storeId !== storeId) throw new Error('receipt is for another store');
  return payload; // grant by payload.sku, payload.quantity, payload.wallet
}

Online, to also learn whether it has since been refunded:

bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/receipts/verify \
  -H "Content-Type: application/json" \
  -d '{"receipt":"eyJhbGciOiJFZERTQSIs…"}'

It returns the decoded receipt and its current status, valid or revoked. An offline check proves a receipt was issued; only the online check (or the item ledger) knows about a later refund. For most games the simplest source of truth is the ledger itself: GET …/entitlements?wallet=.

Store keys ​

A store key is how a game server talks to your store. It works on one store and only for the scopes you give it. Give each server the least it needs.

ScopeAllows
catalog:writeCreate and change SKUs
orders:readRead orders
orders:writeCreate orders for a wallet
entitlements:readRead what wallets own
entitlements:writeGrant, consume and revoke items
drops:writeCreate and change drops
rewards:awardAward a wallet from an award drop
events:readRead the event feed
bash
chaindaddy iap key create 3f0c… --name game-server --scope entitlements:read entitlements:write
chaindaddy iap key list 3f0c…
chaindaddy iap key revoke 3f0c… <keyId>

The secret (cd_iap_…) is shown once, when the key is created. Store it in your secrets manager; lost keys cannot be recovered, only revoked and replaced. Only your account credentials can create, list or revoke keys. A revoked key stops working within a minute.

Rate limits ​

Calls made with a store key are limited per key, not per IP address, so several games behind one address, or on one host, each get their own budget:

BudgetLimitCounted per
Each store key2,400 requests a minutekey
All store keys calling from one IP address9,600 requests a minuteIP address
Unknown or revoked keys60 requests a minuteIP address

Every response carries X-RateLimit-Limit and X-RateLimit-Remaining for the budget it drew on. Past a limit the API answers 429 with a Retry-After header in seconds; wait that long, then retry. A busy server that needs more than one key's budget can use a key per game or per server.

Retries are safe ​

Every POST takes an Idempotency-Key header. Send the same key again and you get the first result back instead of a second grant, sale or award. The CLI generates one when you do not pass --idempotency-key, and prints it with --json so an agent can retry with it. Build keys from something stable in your game (race-7731-podium), not a fresh random value per attempt.

Events and webhooks ​

Everything that changes in your store is written to an append-only event feed in the same moment:

EventWhen
order.createdAn order was quoted
order.paidIts payment is on chain, not yet confirmed
order.confirmedConfirmed: items granted, receipt issued
order.expiredIts window passed unpaid
order.revertedA paid order's payment left the chain (a reorg); it is pending again
order.refundedYou recorded a refund
order.mismatchA payment named the order but not its quote; the order is unchanged and nothing is granted
order.duplicate_paymentThe order was paid a second time (a client changed its deadline); nothing more is granted
entitlement.grantedItems were added to a wallet
entitlement.consumedItems were spent
entitlement.revokedItems were taken back
drop.claimedA wallet claimed a drop
drop.awardedYour server awarded a wallet
bash
chaindaddy iap events 3f0c… --after 1200 --json
chaindaddy iap events 3f0c… --follow --json     # one event per line, as they happen

GET /api/v2/iap/stores/{id}/events?after= returns {entries, last}; pass last back as after to continue. The same events are pushed as webhooks named iap.<type> (iap.order.confirmed, …): to your store's own webhook (below), and to any iap.* webhook the wallet that opened the store has set up.

Purchase notifications ​

Give your store a webhook and your game server hears about every sale as it confirms, with no polling. Any store can have one: you need the store's own plan (a Developer plan or developer program membership), not the Developer plan the general webhooks need.

Set it in Developer portal → Store → Settings → Purchase notifications, or:

bash
chaindaddy iap webhook set 3f0c… --url https://game.example.com/hooks/chaindaddy
#   prints the whsec_… signing secret ONCE: put it in your server's secrets
chaindaddy iap webhook test 3f0c…      # sends a signed iap.test event
chaindaddy iap webhook show 3f0c…      # the webhook and its last 20 deliveries
chaindaddy iap webhook rotate 3f0c…    # a new secret; the old one stops working at once
chaindaddy iap webhook delete 3f0c…

Over HTTP it is PUT /api/v2/iap/stores/{id}/webhook {url, events?} (201 with the secret on create, 200 without it on a change), GET, DELETE, POST …/webhook/test and POST …/webhook/rotate-secret. Setting, rotating and removing need your account credentials; a store key with events:read can read the webhook and send a test. The URL must be https:// and reachable from the internet. events narrows what it receives; by default it gets every type below.

Webhook eventWhen
iap.order.createdAn order was quoted
iap.order.paidIts payment is on chain, not yet confirmed
iap.order.confirmedConfirmed: items granted, receipt issued. Deliver on this one
iap.order.expiredIts window passed unpaid
iap.order.revertedA paid order's payment left the chain (a reorg); it is pending again
iap.order.refundedYou recorded a refund; its items and receipt were revoked
iap.order.mismatchA payment named the order but not its quote
iap.order.duplicate_paymentThe order was paid a second time
iap.entitlement.granted / .consumed / .revokedA wallet's items changed
iap.drop.claimed / iap.drop.awardedA drop was claimed or awarded

A confirmed purchase arrives like this:

json
{
  "id": "iap_evt_1207",
  "type": "iap.order.confirmed",
  "created_at": "2026-09-27T09:14:03Z",
  "data": {
    "eventId": 1207,
    "type": "order.confirmed",
    "storeId": "3f0c…",
    "chainId": "eip155:8453",
    "crownId": 8,
    "wallet": "0x5a1c…",
    "orderId": "0x9e1f…",
    "payload": {
      "sku": "boost_pack",
      "quantity": 1,
      "status": "confirmed",
      "appAccountToken": "player-7731",
      "amount": "50000000000000000000",
      "txHash": "0x4b7e…",
      "payer": "0x5a1c…",
      "receiptId": "0d6f…",
      "receipt": "eyJhbGciOiJFZERTQSIs…"
    }
  }
}

appAccountToken is whatever you passed when you created the order, so you know which player to credit. receipt is the signed receipt; verify it as in Verifying receipts and deliver. entitlement.* events carry the sku, the delta and the wallet's quantity after the change. id is the same on every retry of one event, so use it to ignore a repeat.

Every delivery is signed. X-Webhook-Signature is the hex HMAC-SHA256 of the raw request body, keyed with your whsec_… secret. Check it against the raw bytes before you parse them:

js
import crypto from 'node:crypto';

// app.post('/hooks/chaindaddy', express.raw({ type: 'application/json' }), handler)
function verifyChainDaddy(rawBody, signature, secret) {
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
  return signature?.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

if (!verifyChainDaddy(req.body, req.get('X-Webhook-Signature'), process.env.CHAINDADDY_WEBHOOK_SECRET)) {
  return res.status(401).end();
}
const event = JSON.parse(req.body);

The other headers are X-Webhook-Event (the type), X-Webhook-Delivery (the id) and X-Webhook-Timestamp. Answer with a 2xx within 5 seconds. A failed delivery is retried with backoff, starting at 1 minute. After 3 failed deliveries in a row the webhook is turned off; saving its URL again turns it back on. chaindaddy iap webhook show lists what was delivered and what your endpoint answered.

If you miss one, reconcile from the feed. Keep the highest data.eventId you have processed and read GET /api/v2/iap/stores/{id}/events?after=<eventId> on start-up and on a timer; its payload is the same object the webhook carries. The webhook makes you fast, the feed makes you complete.

The fee ​

Your tokenFee
Launched in a Chain Daddy launchpad pool0%. We already earn from that pool's trading fees
Every other token: made with the Chain Daddy token creator, launched elsewhere, or existing2.5%

The fee is taken at checkout in whatever the buyer pays in (your token, the chain's coin or USDC), out of the listed price. The buyer pays the price you set; you receive the price minus the fee, and the fee goes to Chain Daddy in the same transaction. The store's feeBps field is the rate that applies to it.

Non-custodial ​

A sale never passes through a Chain Daddy account. On EVM the checkout contract moves the buyer's payment, in your token, the chain's coin or USDC, straight to your payout address (and the fee, if any, to Chain Daddy) in one transaction. It has no owner, no admin and no balance, so there is nothing in it for anyone to take. On Solana the buyer's own transfer pays you directly. An order counts as paid only when that payment is on chain at the required depth.

What you are responsible for ​

You are the merchant. Chain Daddy records what happened on chain and keeps the item ledger; the rest is yours:

  • Prices and what an item does in your game.

  • Delivery. Read the ledger or verify receipts, and honour what they say.

  • Refunds. A refund is your own transfer back to the buyer from your wallet. Then record it, which revokes the items and the receipt:

    bash
    chaindaddy iap order refund 3f0c… 0x9e1f… --tx 0xRefundTxHash --note "double purchase"
  • Stray and duplicate payments. An order.mismatch event means someone paid an order's id but not its quote (a different amount, payee or currency); the payment went wherever that payment sent them, and if they reached you, settle it with the payer yourself. An order.duplicate_payment event means the buyer paid the same order twice; the second payment reached you, so refund it.

Items are not money. They live off chain, cannot be transferred between wallets and can never be exchanged for tokens or cash. Call them items, skins, passes or whatever your game calls them, never a balance of money.

  • Actions: dispatching actions from a widget
  • App Manifest: permissions your app declares
  • Webhooks: signing, retries and subscriptions
  • CLI: installing and configuring chaindaddy