# Store and Rewards

Sell items for **your own token**, give items away as rewards, and unlock perks for the people who hold your token. Buyers pay you directly on chain. Chain Daddy keeps the catalog, records what each wallet owns, and signs a receipt for every purchase.

You can sell three ways:

- **From your own app**, with your own UI and a server that talks to the store API. See [Sell from your own app](#sell-from-your-own-app).
- **From an app on your token page.** The app asks the page to run the purchase, and the page handles the wallet. See [Selling from an app on your token page](#selling-from-an-app-on-your-token-page).
- **With the Item showcase**, a ready-made app for your token page. No code needed. See [The Item showcase](#the-item-showcase-on-your-token-page).

Every action is an API call, a `chaindaddy iap …` command and an MCP tool, and every write can be retried safely. Build against your [test store](/developer/test-your-integration) first: the same API, with orders paid by one call and nothing on chain.

## Who can open a store {#who-can-open-a-store}

A store needs a **Developer, Partner or Enterprise plan**, or membership in the [Developer Beta Program](/developer/#developer-beta-program), which is open to any paid plan. Without one, store routes answer `403 IAP_PLAN_REQUIRED`. `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. If the wallet that opened the store loses its plan, the store stops taking orders and its store keys stop working until the plan is back. Buyers keep the items and receipts they already have.

## Fees {#the-fee}

| Your token | Fee on each sale |
|------------|-----|
| Launched in a Chain Daddy launchpad pool | **0%** |
| Any other token: made with the Chain Daddy token creator, launched elsewhere, or existing | **2.5%** |

The buyer pays the price you set. The fee comes out of it at checkout, in whatever the buyer pays in, and the rest goes to your payout address in the same transaction. A sale never passes through a Chain Daddy account: the checkout contract has no owner or admin and holds no balance. The store's `feeBps` field is the rate that applies to it.

## Where you manage it {#where-you-manage-it}

| What | Where |
|---|---|
| Items, orders, customers, store keys, events, settings and the store's webhook | **Developer portal → Store**: [chaindaddy.io/_developer/store](https://chaindaddy.io/_developer/store) |
| Rewards: claim links, pushes and drops your server awards | Your token's manager: **Community → Rewards** |
| Token prizes your server pays from a prize pool | Your token's manager: **Community → Airdrops**. See [Token Prizes](/developer/token-prizes) |
| Selling on your token page | Add the **Item showcase** app to the page |
| Everything, from your server or an agent | The API, `chaindaddy iap …`, or the `iap_*` [MCP tools](#tools-for-agents) |

## Sell from your own app {#sell-from-your-own-app}

### 1. Open the store and add an item {#open-a-store}

```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":250,"live":true,…}}

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

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

Over HTTP, opening a store is `POST /api/v2/iap/stores` with `{"chainKey":"base","crownId":8}`. A token page has one store, so opening it again returns the same one. EVM chains and Solana are both supported: `GET /api/v2/iap/config` lists the chains, the confirmations each needs, and the currencies each offers besides your token.

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
```

### 2. Your server creates the order {#create-the-order}

When a user taps **Buy**, your server creates an order for that user's wallet. `appAccountToken` is your own id for the user. 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":"badge.gold","quantity":1,"wallet":"0xabc…","appAccountToken":"user-42"}'
```

CLI: `chaindaddy iap order create 3f0c… --sku badge.gold --wallet 0xabc… --app-account-token user-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](#price-in-dollars) and paid in your token or the chain's coin), and says how to pay:

```json
{
  "id": "0x9e1f…",
  "storeId": "3f0c…",
  "sku": "badge.gold",
  "quantity": 1,
  "wallet": "0xabc…",
  "appAccountToken": "user-42",
  "ref": null,
  "currency": "0xToken…",
  "currencyKind": "token",
  "currencySymbol": "MYTOKEN",
  "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": "My Token", "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 your app client. Use one `Idempotency-Key` per purchase attempt (your cart id, say): a retry returns the same order instead of a second one.

### 3. The user pays from their wallet {#the-user-pays}

The client never needs to know an amount. It pays one of two ways:

- **`calls`**: send `approve` (the ERC-20 approval), then `pay`. An item [paid in the chain's coin](#what-the-buyer-pays-in) has no `approve`, and `pay.value` is the total.
- **`permitTypedData`**: present when the currency supports permits (`permit.supported`; never for the chain's coin). The user signs it, and one transaction calls `payWithPermit` on the checkout.

With [viem](https://viem.sh):

```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 that pays you (and the fee, if any) directly. The user's wallet signs and sends it, and its signature takes the place of the transaction hash below.

### 4. Wait for `confirmed` {#wait-for-confirmed}

Chain Daddy watches the checkout on every chain, so an order moves to `paid` within seconds of the payment landing, even if your client crashes after paying. If you already have the hash, submit it to check that transaction at once:

```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":"…"}
```

Then poll the order with the store key until it is `confirmed`, or subscribe to the `iap.order.confirmed` [webhook](#your-store-s-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: don't hand over the item yet. Each read of a `paid` order re-checks it on chain, so polling every second or two reads `confirmed` as soon as it is deep enough.

### 5. Deliver {#deliver}

A confirmed order carries a signed receipt: [verify it](#verifying-receipts-on-your-server) and deliver. Or read the item ledger, which is the source of truth for what a user owns, and spend from it on your server:

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

## Items {#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. A fixed `price` is a **base-unit string** of the item's currency (for an 18-decimal token, 5,000 tokens is `"5000000000000000000000"`); the CLI's `--price-tokens 5000` converts for you. We recommend [pricing in dollars](#price-in-dollars) and taking payment in your token.

| Type | A purchase gives | Good for |
|------|------------------|----------|
| `consumable` | `quantity` added to a balance; your server spends it with `consume` | Credits, boosts |
| `non_consumable` | Owned, at most one per wallet | Badges, themes, a feature bought once |
| `timed` | Access until an expiry, extended by `durationSeconds × quantity` from now or from the current expiry, whichever is later. Nothing renews automatically | Memberships, passes |
| `unlock` | Never sold. Held while its [`rule`](#rules) holds, checked live | Holder perks |

New items start as `draft`. Set `status` to `active` to list them. Archive one to take it down (`chaindaddy iap sku archive <store> <sku>`); that never takes it away from anyone who has it.

A badge:

```json
{
  "sku": "badge.gold",
  "type": "non_consumable",
  "name": "Gold badge",
  "description": "A gold badge beside your name.",
  "imageUrl": "https://app.example/items/badge-gold.png",
  "price": "5000000000000000000000",
  "metadata": { "badgeId": "gold" },
  "status": "active"
}
```

`metadata` is any JSON your app needs to render or apply the item. It comes back on every read.

A credit pack. A **bundle** grants other items instead of itself: buying `credits.100` adds 100 to the user's `credits` balance. `credits` has no price, so it can only be given as a reward. Spending more than a wallet has answers `409 INSUFFICIENT_QUANTITY` and changes nothing.

```json
[
  { "sku": "credits", "type": "consumable", "name": "Credit", "status": "active" },
  {
    "sku": "credits.100",
    "type": "consumable",
    "name": "100 credits",
    "price": "1000000000000000000000",
    "grants": [{ "sku": "credits", "quantity": 100 }],
    "status": "active"
  }
]
```

A 90-day membership, on sale for a window. Buying twice gives 180 days.

```json
{
  "sku": "member.90d",
  "type": "timed",
  "name": "90-day membership",
  "price": "20000000000000000000000",
  "durationSeconds": 7776000,
  "saleStartsAt": "2026-10-01T00:00:00Z",
  "saleEndsAt": "2026-12-31T00:00:00Z",
  "status": "active"
}
```

A holders-only perk, held by any wallet with at least 1,000 tokens for as long as it keeps them. It shows in that wallet's items with `"source": "unlock"`.

```json
{
  "sku": "access.early",
  "type": "unlock",
  "name": "Early access",
  "rule": { "hold": { "min": "1000000000000000000000" } },
  "status": "active"
}
```

Items live off chain. They can't be transferred between wallets or exchanged for tokens or cash, so don't describe them as a balance of money. Nothing you sell may have a random outcome: no loot boxes or mystery boxes.

### Price in dollars {#price-in-dollars}

Set `priceUsdMicros` instead of `price` and the item costs about the same in dollars whatever your token trades at. The unit is a millionth of a dollar, so $5 is `5000000`. An item with no `currency` is still paid in your token.

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

CLI: `--price-usd 5 --min-price-tokens 1000 --max-price-tokens 500000`. MCP: `priceUsdMicros`, `minPrice` and `maxPrice` on `iap_create_sku` / `iap_update_sku`. An item has either `price` or `priceUsdMicros`; setting one clears the other.

Each order converts the dollar price to an exact amount at that moment and locks it:

- The rate is your token's live market price (or the chain's coin's), averaged over the last 2 minutes so one odd trade can't move it.
- The amount is rounded **up** to 4 significant figures, so the wallet shows a clean number. That is at most about 0.1% above the exact conversion.
- `minPrice` and `maxPrice` (optional, base units per item) cap the conversion, so a crash or a spike in your token's price can't make an item cost a fortune or almost nothing.
- An order converted to your token or the coin can be paid for **5 minutes**. The next order gets a fresh amount.
- An item [paid in USDC](#what-the-buyer-pays-in) needs no rate: $5 is exactly 5 USDC.

When there is no reliable price right now (a new store's price takes about 30 seconds to warm up, or the price is moving too fast), creating an order answers `503 PRICE_UNAVAILABLE`. It usually clears within a minute, so ask the user to try again shortly. Fixed prices and USDC prices never depend on it.

A dollar-priced order and its receipt also carry `priceUsdMicros` and `usdPerToken`, the dollar price of one whole unit of the currency the quote used. The store's `price` field (`chaindaddy iap store show`) shows the current rate, `usdPerToken` and `usdPerNative`, or `unavailable` with the reason. In the public catalog a dollar-priced item has `"priceIsEstimate": true` and an estimated `price` for display ("$5.00 ≈ 140,100 MYTOKEN"); the amount locked on the order is the one that counts.

### What the buyer pays in {#what-the-buyer-pays-in}

Items are paid in your token by default. When an item suits another currency better, set its `currency`:

| `currency` | The buyer pays in | `price`, `minPrice`, `maxPrice` are |
|------------|-------------------|--------------------------------------|
| `token` (default) | Your token | Base units of your token |
| `native` | The chain's coin: ETH, BNB, POL, SOL… | Base units of the coin (wei, lamports) |
| `usdc` | The chain's dollar stablecoin: USDC (USDG on Robinhood Chain) | Base units of the stablecoin (check its `decimals`: BNB Chain's is 18) |

`GET /api/v2/iap/config` lists each chain's `currencies` with `address`, `symbol` and `decimals`. A chain without a dollar stablecoin (Gnosis) refuses `usdc`. CLI: `--currency usdc`. MCP: `currency` on `iap_create_sku` / `iap_update_sku`.

- An update that changes `currency` on a fixed-price item must set `price` (or `priceUsdMicros`) in the same call, and it clears `minPrice` and `maxPrice`. A dollar price carries over.
- In the chain's coin on EVM, `payment.calls` has no `approve`, `pay.value` is the total, and `permit` is `null`.
- The buyer's own transaction still pays you directly, whatever the currency. A payment in a different currency than the order's doesn't pay the order: it is recorded as an `order.mismatch` event and grants nothing.

### Revenue splits {#revenue-splits}

An item can pay other wallets a share of every sale: a collaborator, an artist, a charity. The buyer's one payment pays each of them directly, beside your payout.

```json
{
  "sku": "skin.neon",
  "type": "non_consumable",
  "name": "Neon skin",
  "priceUsdMicros": 3000000,
  "splits": [{ "address": "0x7777777777777777777777777777777777777777", "bps": 2000 }],
  "status": "active"
}
```

- `bps` is basis points (2000 = 20%) of **your share**, the price after the fee.
- Up to **2** splits, or **3** on a launchpad token (no fee). Together they stay under 100%, so your payout wallet always gets a part.
- Each split rounds down and your payout gets the rest. On a very small order a split that rounds to nothing is left off.
- Addresses are wallets on the store's chain, each once, and never your payout wallet. On Solana, an item paid in SOL can only split to a wallet that already holds some SOL.
- An order locks its splits, and its receipt lists what each wallet was paid. Changing an item's splits only affects later orders.

CLI: `--split <address>:<percent>`, repeated, and `--clear-splits`. MCP: `splits`.

### Limits on a sale {#limits-on-a-sale}

| Field | Effect |
|-------|--------|
| `maxSupply` | Total that can ever be sold. Stock is held while an order waits for payment and released if it expires |
| `maxPerWallet` | Most one wallet may own (always 1 for `non_consumable`) |
| `saleStartsAt`, `saleEndsAt` | The sale window |
| `rule` | Who may buy it (on an `unlock`: who holds it) |
| `sortOrder` | Order in the catalog |

## Rules {#rules}

A rule 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": "badge.gold", "min": 1 } },
    { "any": [{ "owns": { "sku": "member.90d" } }, { "not": { "owns": { "sku": "banned" } } }] }
  ]
}
```

| Node | True when |
|------|-----------|
| `all` / `any` | Every / at least one child is true |
| `not` | Its child is false |
| `hold` | The wallet holds at least `min` base units of your token, or of `token` on `chainId`. Read live |
| `owns` | The wallet owns at least `min` (default 1) of an item in your store |

A rule may nest 4 levels deep and have 32 nodes.

## Rewards: drops {#rewards-drops}

A drop gives items away. Nobody pays and no tokens move. To give away **your token** instead, use a [token prize pool](/developer/token-prizes).

| Mode | How items arrive |
|------|------------------|
| `claim` | You share a link; eligible wallets claim from it |
| `push` | You upload a list of wallets; activating the drop delivers to all of them |
| `award` | Your server awards a wallet (`POST …/drops/{dropId}/award`, scope `rewards:award`), for example when a user reaches a milestone |

```json
{
  "name": "Milestone reward",
  "mode": "award",
  "items": [{ "sku": "credits", "quantity": 30 }],
  "perWalletLimit": 5,
  "maxClaims": 10000,
  "startsAt": "2026-10-01T00:00:00Z",
  "endsAt": "2026-12-31T00:00:00Z"
}
```

```bash
chaindaddy iap drop create 3f0c… --file milestone.json --json          # starts as draft
chaindaddy iap drop status 3f0c… <dropId> active
chaindaddy iap drop award 3f0c… <dropId> --wallet 0xabc… --idempotency-key user-42-milestone-10
```

`audience` takes a [rule](#rules), 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 dates apply to every claim and award, so a bug or a replay can't give out more than you set.

## The Item showcase on your token page {#the-item-showcase-on-your-token-page}

No app of your own needed: add the **Item showcase** app to your token page, and visitors can buy your items there. Each item shows its picture, name and price, and a **Buy** button that opens the page's [purchase sheet](#what-the-buyer-sees). Items that can't be bought right now say why: sold out, holders only, or not on sale. A signed-in visitor also sees what they already hold from your store.

Open its settings (the gear) to set the title, choose and order the items to show (empty shows every item with a price), show or hide the visitor's own items, and pick 2 or 3 cards per row. Prices and items always come from your store, so add, price and archive them in the developer portal.

## Payment links {#sell-from-anywhere-payment-links}

A payment link opens one of your items, ready to buy, on your token page. Put it on your site, in a Discord post, in a menu or in a README:

```
https://chaindaddy.io/fluxgp/base?buy=boost.pack
https://chaindaddy.io/fluxgp/base?buy=credits.100&qty=3&ref=discord
```

| Param | Required | What it is |
|---|---|---|
| `buy` | yes | The item's `sku` |
| `qty` | no | 1 to 99, default 1 |
| `ref` | no | Your tag for where you shared it: 1 to 32 of `a-z`, `0-9`, `.`, `_`, `-` |

The link opens the purchase sheet. Nothing is charged until the buyer presses **Buy**, and the price and payees always come from your store, never from the link. An item that can't be bought right now opens the sheet with the reason.

Copy a link from the developer portal (**Items**, **Copy link** on an active item; **Copy embed** beside it gives the item's [card](/token/embeds) for your own site), or run `chaindaddy iap link <store> <sku> [--qty n] [--ref tag]`. Links always open your live store.

The order keeps `ref`: it shows in **Orders**, on the order, and in every `order.*` event. It is for your own attribution. To credit a user's account, key on the buyer's **wallet**: a link is a public URL, so it never carries `appAccountToken`.

## Selling from an app on your token page {#selling-from-an-app-on-your-token-page}

When your app runs on your token page, it needs no server and no payment code. It asks the page to run the purchase:

1. Declare `iap:purchase` in your [manifest](/developer/manifest#widget-permissions). The install screen shows it as "Sell items from this token's store".
2. Send the [`iap-purchase`](/developer/actions#iap-purchase) action:

```typescript
const res = await onAction({
  type: 'iap-purchase',
  sku: 'badge.gold',
  quantity: 1,
  appAccountToken: user.id, // optional: your own id for this user, echoed on the order and receipt
});
// res.data: { status: 'confirmed' | 'paid' | 'cancelled' | 'failed', orderId?, receipt? }
```

The page shows its purchase sheet, the user approves in their wallet, and the action resolves. `paid` means the payment is on chain and still confirming; the item arrives when it reaches `confirmed`.

Read the catalog and the user's items with `api-call`, with the viewer's own session. Apps may read `/api/v2/iap/public/…`, `/api/v2/iap/me/…` and `/api/v2/iap/orders/…`:

```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}` });
```

### Handing out a reward from the app {#handing-out-a-reward-from-the-app}

An app can claim a [claim-link drop](#rewards-drops) for the user in front of it, with their own session. It needs `iap:purchase`.

```typescript
const res = await onAction({ type: 'api-call', method: 'POST', endpoint: `/api/v2/iap/drops/${slug}/claim` });
// res.ok: true → res.data.granted lists the items; the store event store:entitlements follows
// res.ok: false → res.error says why (not in the audience, already claimed…); res.status is the HTTP status
```

The drop's audience, allowlist, per-wallet limit and dates apply just as on its claim page, so an app can't give out more than you set.

### Hearing when a purchase settles {#hearing-when-a-purchase-settles}

An app with `iap:purchase` can subscribe to store events instead of polling:

```typescript
const unsubscribe = props.subscribeStore?.((event) => {
  if (event.type === 'store:order') {
    // Only the app that asked for this purchase hears it.
    // event: { orderId, sku, status: 'confirmed' | 'failed', receipt? }
    if (event.status === 'confirmed') unlock(event.sku, event.receipt);
  }
  if (event.type === 'store:entitlements') {
    // The viewer's items in this store changed. Read them again.
    refreshInventory();
  }
});
// later: unsubscribe?.()
```

- `store:order` goes only to the app whose purchase closed as `paid`. The page follows that order for up to 45 minutes and sends `confirmed` (with the signed `receipt`) or `failed`.
- `store:entitlements` goes to every app on the page that holds `iap:purchase`, whenever a purchase on the page confirms.
- `subscribeStore` exists only for apps in their own frame (every app that isn't ours), so check for it before calling it.

### What the buyer sees {#what-the-buyer-sees}

The Item showcase, payment links and the `iap-purchase` action all open the token page's purchase sheet:

- **The price locks when the buyer taps Buy**, so reading the sheet doesn't use up the order's time.
- **One wallet prompt per purchase.** The first purchase in a token also asks once to let the checkout take that token; later purchases in it are a single prompt. Paying in the chain's coin needs no approval.
- **A purchase is never made twice by accident.** A double tap, a retry or a reopened sheet returns the same order. A cancelled prompt charges nothing.
- **The buyer can close the sheet once the payment is sent.** The item arrives when the order confirms.
- **A buyer who is short is offered what they lack.** For your token, the sheet opens the page's Buy panel sized to the shortfall. For USDC it offers a swap from the chain's coin, or a bridge from another EVM chain.

## How a purchase settles {#how-a-purchase-settles}

```
pending ──▶ paid ──▶ confirmed ──▶ refunded
   │
   └──▶ expired
```

| Status | Meaning |
|--------|---------|
| `pending` | Price, quantity, wallet and payees are locked while the buyer pays |
| `paid` | The payment is on chain and gathering confirmations |
| `confirmed` | The items are in the wallet and a signed receipt exists |
| `expired` | The deadline passed with no payment. Held stock is released |
| `refunded` | You refunded the buyer and recorded it. The items and the receipt are revoked |

`GET /api/v2/iap/config` gives the confirmations each chain needs; Solana orders confirm when the payment is finalized. On Base, Arbitrum, BNB Chain and Solana that takes seconds; on Ethereum and Polygon, about two minutes. A payment undone by a chain reorganisation before it confirms sends the order back to `pending`.

A payment that names an order's id but not its exact quote (another currency, payee or amount) can't pay or block that order: it is recorded as `order.mismatch` and grants nothing. Paying the same order twice is recorded as `order.duplicate_payment` and grants nothing more. Neither happens when the client uses the `payment` block as given.

## Verifying receipts on your server {#verifying-receipts-on-your-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": "badge.gold",
  "skuType": "non_consumable",
  "quantity": 1,
  "wallet": "0xabc…",
  "appAccountToken": "user-42",
  "currency": { "address": "0x…", "symbol": "MYTOKEN", "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,
  "livemode": true
}
```

- `currency` is what the buyer paid in: your token, the chain's coin (the zero address on EVM, `11111111111111111111111111111111` on Solana) or the stablecoin. `amount` and `feeAmount` are base units of it.
- A [dollar-priced](#price-in-dollars) receipt also carries `priceUsdMicros` and `usdPerToken`. Parse both as optional.
- `livemode` is `false` on a [test store's](/developer/test-your-integration) receipt. Older receipts don't carry it.

**Offline**, against the public keys at `GET /api/v2/iap/jwks` (current and previous, so key rotation doesn't 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 status, `valid` or `revoked`. Only the online check (or the item ledger, `GET …/entitlements?wallet=`) knows about a later refund.

## Authentication {#authentication}

Store routes take **one** credential, as `Authorization: Bearer …`. Your account key also works as `X-API-Key: cd_live_…`.

| Credential | Use it for |
|------------|-----------|
| Your account: a `cd_live_` API key or a wallet session | Opening stores, managing items, drops and store keys |
| A store key, `cd_iap_…` | Your server. Works on **one** store, limited to its scopes. Can't open stores, list your stores or manage keys. Send it alone |
| None | Public 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`, and the MCP server from `CHAINDADDY_IAP_KEY`.

## Store keys {#store-keys}

A store key is how your server talks to your store. Give each server only the scopes it needs.

| Scope | Allows |
|-------|--------|
| `catalog:write` | Create and change items |
| `orders:read` | Read orders |
| `orders:write` | Create orders for a wallet |
| `entitlements:read` | Read what wallets own |
| `entitlements:write` | Grant, consume and revoke items |
| `drops:write` | Create and change drops |
| `rewards:award` | Award a wallet from an `award` drop |
| `events:read` | Read the event feed |
| `messages:send` | [Message one of your customers](#messaging-one-user) |
| `airdrops:award` | Pay [token prizes](/developer/token-prizes) from a prize pool that pinned this key. Never given by default |

```bash
chaindaddy iap key create 3f0c… --name app-server --scope entitlements:read entitlements:write
chaindaddy iap key list 3f0c…
chaindaddy iap key revoke 3f0c… <keyId>
```

The secret (`cd_iap_…`) is shown **once**. Keep it in your secrets manager: a lost key can only be revoked and replaced. Only your account credentials can create, list or revoke keys. A revoked key stops working within a minute, and can't pay a token prize from the moment you revoke it.

### Rate limits {#rate-limits}

Store-key calls are limited per **key**, so several apps behind one IP address each get their own budget:

| Budget | Limit |
|--------|-------|
| Each store key | 2,400 requests a minute |
| All store keys from one IP address | 9,600 requests a minute |
| Unknown or revoked keys, per IP address | 60 requests a minute |
| Token prize awards | 60 a minute per key, within the key's budget |

Responses carry `X-RateLimit-Limit` and `X-RateLimit-Remaining`. Past a limit the API answers `429` with `Retry-After` in seconds.

## Retries are safe {#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. Build keys from something stable in your app (`user-42-milestone-10`), not a fresh random value per attempt. The CLI generates one when you don't pass `--idempotency-key`, and prints it with `--json`.

Token prizes are stricter, because they move your token: see [Token Prizes](/developer/token-prizes#retries).

## Events and webhooks {#events-and-webhooks}

Every change in your store is written to an event feed. The same events are sent as webhooks named `iap.<type>` (`iap.order.confirmed`, …).

| Event | When | `payload` beyond the basics |
|-------|------|------|
| `order.created` | An order was quoted | |
| `order.paid` | Its payment is on chain, not yet confirmed | |
| `order.confirmed` | Items granted, receipt issued. **Deliver on this one** | `receipt` |
| `order.expired` | Its deadline passed unpaid | |
| `order.reverted` | A paid order's payment was undone by a reorg; it is pending again | |
| `order.refunded` | You recorded a refund | |
| `order.mismatch` | A payment named the order but not its quote; nothing granted | |
| `order.duplicate_payment` | The order was paid a second time; nothing more granted | |
| `entitlement.granted` / `.consumed` / `.revoked` | A wallet's items changed | `sku`, `delta`, `quantity` after the change |
| `entitlement.expiring` | A timed item ends within 72 hours (only items held longer than 72 hours) | `sku`, `name`, `expiresAt` |
| `entitlement.expired` | A timed item has ended | `sku`, `name`, `expiredAt` |
| `drop.claimed` / `drop.awarded` | A wallet claimed a drop, or your server awarded one | |
| `drop.exhausted` | A drop reached its cap | `dropId`, `name` |
| `sku.sold_out` | A sale reached `maxSupply` | `sku`, `name`, `maxSupply` |
| `airdrop.award.submitted` / `.confirmed` / `.failed` | A [token prize](/developer/token-prizes#what-happens-next) was sent, paid, or not paid | `awardId`, `campaignId`, `idempotencyKey`, `amount`, `status`, `txHash`, `error` |
| `store.not_live` / `store.live` | The store stopped selling because the opener's plan lapsed, or started again | |

```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. Keep the highest `eventId` you have processed and read from it on start-up and on a timer: the webhook makes you fast, the feed makes you complete.

## Your store's webhook {#your-store-s-webhook}

Give your store a webhook and your server hears about every sale as it confirms. Any store can have one. Set it in **Developer portal → Store → Settings → Webhook** (all events, or the ones you pick), or:

```bash
chaindaddy iap webhook set 3f0c… --url https://app.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: `PUT /api/v2/iap/stores/{id}/webhook {url, events?}`, `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 public `https://`. A webhook with picked events gets new event types only once you add them.

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…",
    "livemode": true,
    "payload": {
      "sku": "credits.100",
      "quantity": 1,
      "status": "confirmed",
      "appAccountToken": "user-7731",
      "ref": null,
      "amount": "50000000000000000000",
      "txHash": "0x4b7e…",
      "payer": "0x5a1c…",
      "receiptId": "0d6f…",
      "receipt": "eyJhbGciOiJFZERTQSIs…"
    }
  }
}
```

`appAccountToken` is what you passed when you created the order. `ref` is the [payment link's](#sell-from-anywhere-payment-links) tag, or `null`. `id` is the same on every retry of one event, so use it to ignore repeats. `livemode` is `false` only for a [test store's](/developer/test-your-integration) events.

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

```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`, `X-Webhook-Delivery` (the `id`) and `X-Webhook-Timestamp`. Answer 2xx within 5 seconds. Failed deliveries are retried with backoff; after 3 failures in a row the webhook turns off, and saving its URL again turns it back on.

## Notifications {#notifications}

You and your buyers are told what matters in their notifications on chaindaddy.io, by push, by text and by email, each where the recipient has it on.

**You, as the store's owner,** hear about each new sale, payment problems (a mismatched, duplicate or undone payment), sold-out items, finished drops, your store stopping or starting to sell, and a webhook that turned off.

**Your buyers** hear when a purchase confirms, when you refund it, when they receive or lose an item, when a timed item ends within 72 hours, and about problems with their payment.

**Sale alerts.** Every sale is a push. You can also have sales texted, at most one text every 15 minutes, to a phone number you have verified. Set it up in **Sale alerts** in your store's **Settings**. On an iPhone, add Chain Daddy to your Home Screen first: Safari only delivers push to an installed web app.

Everyone manages these at [chaindaddy.io/_preferences](https://chaindaddy.io/_preferences): yours under **Your Store**, a buyer's under **Purchases**. Store notifications need no paid plan.

### Messaging one user {#messaging-one-user}

You can write to one customer yourself: a thank-you, a note that their order is ready, a reminder that their membership ends soon. It arrives in the app on chaindaddy.io and as a push, signed "From *your store* on $YOURTOKEN". It never goes by text or email.

From your server, to a wallet that bought from your store or was given an item by it, with a store key that has `messages:send`:

```bash
curl -X POST https://api.chaindaddy.io/api/v2/iap/stores/$STORE_ID/messages \
  -H "Authorization: Bearer $CD_IAP_KEY" -H "Content-Type: application/json" \
  -d '{"wallet":"0x…","key":"renewal-2026-10","title":"Your pass ends Friday","body":"Renew any time from the app."}'
# {"status":"sent"}   or "duplicate" (that key was used for this user before) or "muted"
```

From an app on your token page, to the signed-in user, with the [`notify`](/developer/actions#notify) action and the `notify:user` permission:

```typescript
const res = await onAction({ type: 'notify', key: `order-${orderId}`, title: 'Your order is in', body: 'Open the app to use it.' });
// res.data.status: 'sent' | 'duplicate' | 'muted'
```

- The title is one line of up to 60 characters; the body is plain text of up to 280.
- `key` is your id for the message (letters, digits, `.`, `_`, `:`, `-`). The same key to the same user sends nothing and answers `duplicate`, so retries are safe.
- At most 5 messages to one user in any 24 hours; past that the answer is 429.
- Users can mute your app or store from any of its notifications. A muted send answers `muted`.

## Refunds and stray payments {#what-you-are-responsible-for}

You are the merchant: you set prices, deliver what you sell, and handle refunds.

- **A refund** is your own transfer back to the buyer. Then record it, which revokes the items and the receipt:

  ```bash
  chaindaddy iap order refund 3f0c… 0x9e1f… --tx 0xRefundTxHash --note "double purchase"
  ```

- **`order.mismatch`**: someone paid an order's id but not its quote. If the payment reached you, settle it with the payer.
- **`order.duplicate_payment`**: the buyer paid the same order twice. The second payment reached you, so refund it.

## Tools for agents {#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 tool | CLI | HTTP |
|----------|-----|------|
| `iap_open_store` | `iap store open` | `POST /api/v2/iap/stores` |
| `iap_list_stores` | `iap store list` | `GET /api/v2/iap/stores` |
| `iap_list_skus` | `iap sku list` | `GET /api/v2/iap/stores/{id}/skus` |
| `iap_create_sku` | `iap sku create` | `POST /api/v2/iap/stores/{id}/skus` |
| `iap_update_sku` | `iap sku update` / `archive` | `PATCH` / `DELETE /api/v2/iap/stores/{id}/skus/{sku}` |
| `iap_create_order` | `iap order create` | `POST /api/v2/iap/orders` |
| `iap_list_orders` | `iap order list` | `GET /api/v2/iap/stores/{id}/orders` |
| `iap_get_order` | `iap order show` | `GET /api/v2/iap/stores/{id}/orders/{orderId}` |
| `iap_refund_order` | `iap order refund` | `POST /api/v2/iap/stores/{id}/orders/{orderId}/refund` |
| `iap_get_entitlements` | `iap entitlements` | `GET /api/v2/iap/stores/{id}/entitlements` |
| `iap_grant` / `iap_consume` / `iap_revoke` | `iap grant` / `consume` / `revoke` | `POST /api/v2/iap/stores/{id}/{grants,consume,revoke}` |
| `iap_create_drop` | `iap drop create` | `POST /api/v2/iap/stores/{id}/drops` |
| `iap_set_drop_status` | `iap drop status` | `POST /api/v2/iap/stores/{id}/drops/{dropId}/status` |
| `iap_add_drop_recipients` | `iap drop recipients` | `POST /api/v2/iap/stores/{id}/drops/{dropId}/recipients` |
| `iap_award` | `iap drop award` | `POST /api/v2/iap/stores/{id}/drops/{dropId}/award` |
| `iap_create_store_key` | `iap key create` | `POST /api/v2/iap/stores/{id}/keys` |
| `iap_list_events` | `iap events` | `GET /api/v2/iap/stores/{id}/events` |

The full request and response schemas are in the [OpenAPI spec](https://docs.chaindaddy.io/openapi.yaml) under the **Creator Store** tag.

## Related docs

- [Actions](/developer/actions): every action an app can send its token page
- [App Manifest](/developer/manifest): the permissions your app declares
- [Token Prizes](/developer/token-prizes): paying your token from a prize pool
- [Webhooks](/api-reference/webhooks): signing, retries and subscriptions
- [CLI](/api-reference/cli): installing and configuring `chaindaddy`
