End-to-end flows

The three multi-party sequences in the API: who calls what, who signs what, and which webhooks fire. Every step links to the endpoint page where you can run it live.

1. Purchase

The end-user pays from their own wallet; rip.fun never touches their funds.

Trying this on the sandbox? Fund your test wallet first. See Get testnet funds (Base Sepolia).

  1. Partner: POST /mystery/purchase/prepare with the user's wallet_address + tier_id → the USDC-approve and purchase transactions (calls[]). Creates nothing.
  2. End user: sends calls[0] (USDC approve, which you can skip if the allowance already covers it) then calls[1] (purchase) from their own wallet.
  3. Partner: POST /mystery/purchase/submit with the tx hash (plus the request_id from the receipt logs, if you have it) so rip.fun can link the reveal.
  4. rip.fun: reveals the items. The reveal is random and takes a few seconds. Poll GET /mystery/purchase/:id until FULFILLED, or use the purchase.fulfilled webhook. The revealed cards land in the user's wallet.

Smart wallets / account abstraction

Two facts every smart-wallet integrator rediscovers the hard way — learned in rip.fun's own app, so you don't have to:

  • Send the approve as its own userop. Batching the USDC approval with the call that spends it fails Alchemy's wallet_prepareCalls gas estimation (the simulation sees no allowance yet). Every calls[] entry carries a machine-readable kind — split on kind === "erc20-approve" / "set-approval-for-all" and submit those first, alone. Skip the approve entirely when the current allowance already covers max_price.
  • @alchemy/wallet-apis@5.2.x needs bigint coercion on prepared-call fields (value/gas fields arrive as numbers or hex strings) or the first EIP-7702 delegation throws Expected bigint, got: 0. Coerce before calling wallet_prepareCalls.

2. Physical redemption

Ship a revealed card to the end-user. The holder sends the redemption transaction themselves (same prepare → send → submit pattern as a purchase), and the card isn't burned until the item ships. Identify the card by purchase_id or token_id. Use token_id to redeem a card you distributed yourself (no rip.fun purchase). The person who sends the transaction, and whom you must KYC, is then the card's current holder.

KYC is your responsibility. rip.fun runs no identity (KYC) or AML checks during redemption. Run whatever identity checks your jurisdiction and risk policy require on the end user before you call prepare, and keep those records. prepare returns the transaction right away for any eligible card the holder owns.

The end user sends the redemption transaction. The API does not. prepare returns the transaction to send, and submit only records the transaction hash after it's sent. Neither endpoint sends anything. The wallet holding the card must send the transaction itself, and signing a message is not the same as sending a transaction (a signature popup does nothing on-chain). rip.fun can't do this one for the holder: only the card's holder can send it.

No card leaves the wallet at this step. The holder's transaction only flags the card for redemption. rip.fun burns the card for real when the item ships. Expect the wallet to show a plain contract interaction with no token transfer. That is correct, not a failure.

  1. Partner (optional): POST /mystery/redemption/quote at checkout to show the shipping cost. Creates nothing.
  2. Partner: POST /mystery/redemption/prepare with the shipping address (plus idempotency_key). It validates the address, locks in the cheapest rate, and returns the transactions to send in data.unsigned ({ chain_id, calls[] }, status PREPARED): the redemption flag (kind: "burn") plus the shipping payment — a USDC approve (kind: "erc20-approve") and payShipping (kind: "shipping-payment") locked to the quoted amount. The end user pays shipping on-chain; you are never billed for it.
  3. End user: sends unsigned.calls as on-chain transactions, in order (confirm and pay gas, a few cents on Base, plus the quoted shipping in USDC) from the wallet holding the card. This flags the card for redemption and pays shipping; it does not transfer the card. Capture the returned tx hash. Keep the order — the payment call only succeeds after the burn call — and under account abstraction the approve must be its own userop.
  4. Partner: POST /mystery/redemption/submit with { redemption_id, tx_hash } (the hash from step 3) → BURN_SUBMITTED.
  5. rip.fun: sees the shipping payment land on-chain, ships the item and burns the card when it ships. Track via GET /mystery/redemption/:purchase_id or redemption.updated webhooks: IN_FULFILLMENTCOMPLETED. An item never ships before its shipping payment is on-chain.

Step 3 in the end user's wallet. Send the transactions, don't just sign a message:

// data.unsigned from POST /mystery/redemption/prepare — calls[] is:
//   [0] kind "burn"             flag the card for redemption
//   [1] kind "erc20-approve"    approve USDC to the shipping processor
//   [2] kind "shipping-payment" pay the quoted shipping (runs AFTER the burn)
const { chain_id, calls } = unsigned;

// 1. make sure the wallet is on the right chain
await wallet.request('wallet_switchEthereumChain', [
  { chainId: '0x' + chain_id.toString(16) }
]);

// 2. eth_sendTransaction, in order: REAL transactions the holder signs + pays
//    gas for. (personal_sign / signTypedData would not touch the chain.)
//    Under account abstraction, batch [0] and [2] but keep [1] its own userop.
let burnTxHash;
for (const call of calls) {
  const txHash = await wallet.request('eth_sendTransaction', [
    { from: holderAddress, to: call.to, data: call.data, value: '0x0' }
  ]);
  if (call.kind === 'burn') burnTxHash = txHash;
}

// 3. hand the burn tx hash to your backend -> POST /mystery/redemption/submit
//    { redemption_id, tx_hash: burnTxHash }

3. Buyback → the card returns to the pool

Give the end-user instant cash for a pull. This is the combo pool's own loop, not a marketplace trade: the pack price already deposited a buyback float on-chain, the holder sells the card straight back to the pool out of that float, and the card is restocked into the pool it came from. Nothing is fronted and nothing is owed — the sale simply reduces the net revenue your share is calculated on.

Two rules the contract enforces, both of which quote pre-flights for you: only the card's original recipient may sell it back (a card that changed hands never can), and only before its buyback window expires. A batch is capped at 50 cards and is all-or-nothing on-chain.

  1. Partner: POST /mystery/sellback/quote with the holder's wallet and the token ids → per-card eligibility and the exact payout, priced the way the contract prices it. Check every card is eligible before going further.
  2. Partner: POST /mystery/sellback/prepare → the calls[] the holder signs. The first is a one-time setApprovalForAll, included only when it is actually missing — the pool moves the card itself during the sale, so without it the whole transaction reverts. The second is the sale. min_total_usdc is on-chain slippage protection: the pool re-reads the price oracle at execution time, so the default allows 1% of downward drift.
  3. End user (holder): sends the calls from their own wallet and receives USDC immediately. The card transfers into the pool and is restocked as available inventory.
  4. Partner: POST /mystery/sellback/submit with the tx hash → sellback.confirmed webhook. The amount is decoded from the on-chain event, never from your request, because it is the deduction in your net revenue. rip.fun indexes the same event independently, so whichever lands first wins and re-submitting is safe.
  5. Revenue: the sale shows up as a deduction in GET /mystery/revenue (gross − sell backs − fees = net), and your share of that net is paid out at the end of the period — see payouts.