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.
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).
wallet_address + tier_id → the USDC-approve and
purchase transactions (calls[]). Creates nothing.calls[0] (USDC approve — skip if the allowance
already covers it) then calls[1] (purchase) from their own wallet.request_id from the receipt logs, if you have it) so
rip.fun can link the reveal.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.
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.
idempotency_key) — validates the address, locks in the
cheapest rate, and returns the transaction to send in data.unsigned ({ chain_id, calls[] }, status PREPARED).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.{ redemption_id, tx_hash } (the hash from step 3) → BURN_SUBMITTED.redemption.updated webhooks: IN_FULFILLMENT → COMPLETED.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 } 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.
buyback.confirmed webhook. Two ways: 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.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.buyback.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.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.