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. Non-custodial 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 — 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.

The custodial version collapses steps 1–3 into one POST /mystery/purchase: credits are debited, rip.fun makes the purchase, and an Idempotency-Key header is required.

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 — 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) — validates the address, locks in the cheapest rate, and returns the transaction to send in data.unsigned ({ chain_id, calls[] }, status PREPARED).
  3. End user: sends unsigned.calls[0] as an on-chain transaction (confirm and pay gas — a few cents on Base) from the wallet holding the card. This flags the card for redemption; it does not transfer the card. Capture the returned tx hash.
  4. Partner: POST /mystery/redemption/submit with { redemption_id, tx_hash } (the hash from step 3) → BURN_SUBMITTED.
  5. rip.fun: ships the item and burns the card when it ships. Track via GET /mystery/redemption/:purchase_id or redemption.updated webhooks: IN_FULFILLMENTCOMPLETED.

Step 3 in the end user's wallet — send the transaction, don't just sign a message:

// data.unsigned from POST /mystery/redemption/prepare
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 — a REAL transaction the holder signs + pays gas for.
//    (personal_sign / signTypedData would not touch the chain.)
const txHash = await wallet.request('eth_sendTransaction', [
  { from: holderAddress, to: calls[0].to, data: calls[0].data, value: '0x0' }
]);

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

3. Buyback → pool wallet → daily settlement

Offer the end-user instant cash for a pull. The bought-back card is sent to your pool wallet, and the USDC rip.fun fronted is settled daily — you refill the buyback pool with each day's accrual.

  1. Partner: POST /mystery/buyback/:purchase_id — rip.fun makes a buyback offer (default 85% of card value; check the price first with GET /mystery/price). The path accepts a purchase id or an on-chain token id, so you can buy back any rip.fun card by token id, even one that wasn't pulled through this API.
  2. End user (holder): accepts the offer and receives USDC minus the marketplace fee → buyback.confirmed webhook. Two ways:
    • from their own wallet on the rip.fun marketplace, or
    • in your app, gas-free: POST …/accept/prepare returns the EIP-712 payload the holder signs (eth_signTypedData_v4) plus nft_approval — if approved is false, have the holder send the ready-made nft_approval.call transaction first (a one-time marketplace approval, never repeated for that contract). Then POST …/accept with the signature and rip.fun submits the acceptance on-chain, gas covered.
    • in your app, self-submitted: prepare's self_submit.call is complete acceptOffer calldata the holder's wallet sends directly — no signing step, and it can be batched with nft_approval.call in one EIP-5792 wallet_sendCalls (approval + accept in a single confirmation, the same path the rip.fun app uses). Gas is on the submitter here.
  3. rip.fun: sends the card to your active pool walletbuyback.card_transferred webhook. If no pool wallet is set, you get a one-time buyback.transfer_held and the transfer retries every 15 minutes until you PUT /mystery/pool-wallet; after 10 failed attempts it ends with buyback.transfer_failed.
  4. Billing: the USDC rip.fun fronted to the holder accrues to your balance whether or not the card transfer succeeded — settle it daily via GET /mystery/billing (accrued_today_usdc is the day's number; items later pulled out of your pool wallet accrue too, each item billed once ever). Your offer volume shows up in GET /mystery/stats.