How a mini app sells items for Tookans — the balance players buy in Casa Tookan: your server signs an offer, the player approves it with their CEID, and a receipt signed by Casa Tookan reaches your server.
Preview — in Casa Tookan development builds, not in the store app yetwindow.casaTookan.tookans.spend({ offer }), the player sees the item, the price and their balance after, and approves: Casa Tookan debits them and sends you a receipt — signed by Casa Tookan, carrying the player's own signature — in the page and at your callback.Bought in Casa Tookan's Me tab (App Store, Google Play), kept on the player's CEID. A purchase is paid at once from that balance.
Casa Tookan keeps 15 %, to the hundredth: an item at 3 leaves you 2.55. The rest is held 90 days — the time refunds need — then joins your CEID's balance, which you spend like any player. Only whole Tookans join it: 4.90 gives you 4, the 0.90 waits for the next sale to make a fifth.
Tookans are not traced to the purchase that brought them: a received Tookan is a Tookan.
You sell as a CEID. A group ties together one app and the server that sells for it; you register it once, signed with your CEID in Casa Tookan (the playground's Tookans tab does it for you).
origin | Your mini app (https://myapp.example). It never changes; a receipt's aud is this origin. |
name | What players see as the seller (“Orb Shop”). |
callback | The URL Casa Tookan POSTs receipts and events to. |
serverKey | The public key (EC P-256 JWK) your server signs offers with. Its RFC 7638 thumbprint is the kid of your offers. Replace it by registering the group again. |
In production, your Wallet App Manifest must name you as its seller, and declare the capability:
{
"wallet": {
"capabilities": { "optional": ["casatookan:tookans.spend"] },
"extensions": { "casatookan": { "seller": { "ceid": 48213907 } } }
}
}
Development CEIDs (Casa Tookan Dev, simulators) need no proof: their groups only sell to development CEIDs, whose Tookans are bought in the sandbox.
One offer per order, signed by your server (ES256, kid = your key's thumbprint), valid at most an hour. The price is yours: discounts, quantities, a price per player.
import { SignJWT, importJWK, calculateJwkThumbprint } from 'jose';
const key = await importJWK(PRIVATE_JWK, 'ES256');
const offer = await new SignJWT({
grp: 'grp_8f2c41d07a9b3e15', // your group
sku: 'sword-iron',
title: 'Iron sword', // one line, what the player sees
price: 10, // whole Tookans
order: orderId, // yours: one payment per order
// buyer: 48213907, // optional: only this CEID may pay
// image: 'https://…/sword.png', // optional
})
.setProtectedHeader({ alg: 'ES256', kid: await calculateJwkThumbprint(PUBLIC_JWK) })
.setIssuedAt()
.setExpirationTime('10m')
.sign(key);
const { offer } = await fetch('/shop/offer', { method: 'POST', body: JSON.stringify({ sku }) }).then((r) => r.json());
try {
const { receipt, sale } = await window.casaTookan.tookans.spend({ offer });
// show it; your server gets the same receipt at its callback
} catch (e) {
if (e.code === 'rejected') return; // the player closed the sheet
throw e;
}
Casa Tookan shows the seller, the item, the price, the player's balance and what is left after — and, when it isn't enough, the packs to buy. The player signs exactly those lines with their CEID key:
Casa Tookan CEID
Version: 1
Action: spend
CEID key: 0x…
App: https://myapp.example
Nonce: …
Issued at: 1792100000
Item: Iron sword
Price: 10 Tookans
Seller: Orb Shop
Order: 7f3c…
Offer: <SHA-256 hex of the offer JWT>
Paying the same order again answers the same receipt: a retry never charges twice.
A JWT (ES256) signed with the CEID token keys — verify it against the same JWKS as a CEID token (/.well-known/jwks.json, jwks-dev.json for development), with aud your origin. It does not expire.
{
"iss": "https://ctserver.everdreamsoft.com", "env": "production", "iat": 1792100000,
"typ": "tookans.receipt", "aud": "https://myapp.example", "sub": "sale_3b9d…",
"group": "grp_8f2c41d07a9b3e15",
"seller": { "ceid": 48213907, "name": "Orb Shop" },
"buyer": { "ceid": 70010002, "key": "0x…", "message": "Casa Tookan CEID\n…", "signature": "0x…" },
"item": { "sku": "sword-iron", "title": "Iron sword" },
"price": 10, "fee": 1.5, "net": 8.5, "order": "7f3c…",
"at": 1792100000, "releaseAt": 1799876000
}
buyer.signature is the player's own EIP-191 signature over buyer.message: anyone can recover buyer.key from it, and check that the message carries this price and this order.
Casa Tookan POSTs { "type", "sale", "token" } as JSON. Answer 2xx. Anything else — an error, a timeout (5 s), no answer — is tried again after 1 min, 5 min, 30 min, 2 h, 6 h, then once a day: eight tries over about three and a half days, each carrying the sale's latest news (its last event, signed anew, or the receipt). Make your handler idempotent: the same sale and type can arrive more than once. Deliveries are kept on the sale and can be sent again from the playground.
| type | token |
|---|---|
sale | The receipt. Deliver the item — once per order. |
refunded | typ: tookans.event: you refunded the sale. |
reversed | typ: tookans.event: Casa Tookan reversed a sale still held, because the player's App Store purchase was refunded. Take the item back if you can. |
Disputes are yours. Your server signs a refund and posts it to /api/v4?action=shopRefund as { "refund": "<JWT>" }; the claims are grp, sale, reason?, iat, exp. The player gets the whole price back — from your held takings, or from your balance once released.
When a player's App Store or Google Play refund leaves their balance below zero, Casa Tookan reverses their sales still held, newest first, until the debt is covered, and tells you (reversed). Released sales are final.
The Tookans tab of the developer playground registers a group with your CEID — the playground can be its server and its callback —, signs offers, pays them from another CEID (copy the buy link to a second device), and shows your sales, their deliveries and what the callback received, checked as above. It needs Casa Tookan Dev: a development CEID, with sandbox Tookans.