Gacha EVM API Documentation
The EVM machine is the same gacha, paid in USDC instead of Solana. You pay a contract, the server rolls, and an ERC-721 card is minted to your wallet. You can sell it back to the vault for USDC inside the buyback window.
Machines, odds, and buyback percentages match the Solana machine — only the payment and the card are EVM-native.
Base URL
| API | https://dev-gacha.collectorcrypt.com |
| Payment token | USDC (decimals vary by chain — read usdcDecimals) |
Chains — GET /api/evm/chains
The machine is multichain. Ask which chains it serves rather than assuming one, and pass the
chainId you chose on every later call.
GET /api/evm/chains
Response:
{
"success": true,
"defaultChainId": 84532,
"chains": [
{
"chainId": 84532,
"chainKey": "base-sepolia",
"name": "Base Sepolia",
"nativeCurrency": "ETH",
"usdc": "0x794A5456be0058a5B8B519AaC274347afC8320A4",
"usdcDecimals": 6,
"paymentContract": "0x2426a0B826E8FF4C5257C21Ff2B7D0602948084A",
"vaultContract": "0x2426a0B826E8FF4C5257C21Ff2B7D0602948084A",
"cardContract": "0x34684f2392259D87339f0F7c1601D46A4d47B142",
"paused": false,
"scopesPaused": { "generatePack": false, "openPack": false, "webhook": false, "buyback": false },
"ready": true
}
]
}
paused— the whole chain is stopped; don't offer it.scopesPaused— which individual operations are stopped, with any global switches already folded in.ready— configured well enough to sell at all. Grey the chain out on any of them.usdcDecimalsis not always 6. Binance-Peg USDC on BNB is 18. Never build an amount yourself from a human number — use the base-unitamountthe API hands you.- Omitting
chainIdon a request meansdefaultChainId. Sending one we don't serve is never a silent fallback:generatePackandbuybackanswer400 UNSUPPORTED_CHAINand name the chains that are enabled.buyback/availableis a pure read and simply answersavailable: false.
chainId is accepted on generatePack (body), buyback (body), buyback/available (query) and
buyback/settle (body). openPack takes only the memo — the chain is read from the pack record,
so you cannot open a pack on the wrong chain.
The same contract addresses are deployed on every chain, so (contract, tokenId) alone does not
identify a card. Token 2615 exists on Base and on Robinhood and they are different cards. Always
carry chainId alongside a token id in your own storage.
Contract addresses
Don't hardcode them. They come back in the API responses:
- Every address, per chain →
/api/evm/chains - USDC + payment contract →
/api/evm/generatePack(usdc,paymentContract) - Card contract + token id →
/api/evm/openPack(evm_contract_address,evm_token_id) - Buyback vault →
/api/evm/buyback(vault)
Contracts get redeployed. A card you were awarded stays sellable against the vault named in its
own quote, which is why the quote carries vault rather than expecting you to remember one.
Authentication
Same as the Solana API: send x-api-key on every request.
- Prefixes your
memowith your slug so you can filter your own activity. - Required for partner machines — they are private to their owner's key and return
400 Unknown machinewithout it. - Public machines work without a key (memos are prefixed
cc-).
Machine list
Use the shared GET /api/machines to get code
(the packType), price, odds, and stock.
Buying and opening a pack
Five steps: generatePack → approve → pay → openPack → (optional) status.
1. Generate Pack — POST /api/evm/generatePack
Reserves the pack and returns the on-chain parameters. Nothing has been paid yet.
Request Body:
{
"playerAddress": "0xEeC1...9A21",
"packType": "pokemon_50",
"chainId": 84532
}
chainId is optional and defaults to defaultChainId.
Response:
{
"memo": "cc-6f3a1b2c-...",
"chainId": 84532,
"usdc": "0x794A5456be0058a5B8B519AaC274347afC8320A4",
"usdcDecimals": 6,
"paymentContract": "0x2426a0B826E8FF4C5257C21Ff2B7D0602948084A",
"treasury": "0x1234...abcd",
"amount": "50000000",
"amountHuman": 50
}
amount is in USDC base units — pass it verbatim to approve and pay. Don't recompute it from
amountHuman; at 18 decimals that arithmetic drifts. The memo is the identifier for the whole
pack lifecycle.
Error Responses:
- 400: Invalid
playerAddress, unknown machine, orUNSUPPORTED_CHAIN - 503:
MACHINE_OFF,OFF_BALANCE,MACHINE_LOW,NO_INVENTORY— retry shortly or use another machine. Also returned when the chain or itsgeneratePackscope is paused;/api/evm/chainssays which. - 500: Server error
2. Approve USDC
Standard ERC-20 approve of amount to paymentContract.
approve(address spender, uint256 amount)
3. Pay
pay(uint256 amount, string memo)
Call it on paymentContract with the exact amount and memo from step 1. This pulls the
USDC and emits Paid(from, amount, memo). Keep the transaction hash.
EVM payments are final. There is no refund path.
4. Open Pack — POST /api/evm/openPack
Verifies the payment on-chain, rolls the VRF, and mints the ERC-721 to playerAddress.
Idempotent — calling it twice returns the same award.
Request Body:
{
"memo": "cc-6f3a1b2c-...",
"payTxHash": "0x9ab3...f10c"
}
payTxHash is optional, but pass it: it confirms your payment immediately. Without it you'll get
202 WAITING_FOR_PAYMENT until the payment is confirmed independently.
Response (Success):
{
"success": true,
"memo": "cc-6f3a1b2c-...",
"roll": 87654321,
"rarity": "common",
"rarity_label": "Common",
"prize_tier": 4,
"nft_address": "H9ZXYkud...TptLA",
"card_name": "Charizard Base Set",
"nftWon": {
"id": "H9ZXYkud...TptLA",
"content": {
"links": { "image": "https://arweave.net/abc..." },
"metadata": {
"name": "1999 Pokemon Base Charizard",
"description": "1999 Pokemon Base Set Charizard Holo #4 PSA 9",
"insuredValue": 50,
"attributes": [{ "trait_type": "Insured Value", "value": 50 }]
},
"files": [
{ "uri": "https://arweave.net/abc...", "cdn_uri": "https://cdn.helius...", "cc_cdn": "https://cdn.collectorcrypt.com/...front-m.jpg", "mime": "image/jpeg" },
{ "uri": "", "cc_cdn": "https://cdn.collectorcrypt.com/...back-m.jpg", "mime": "image/jpeg" }
]
}
},
"points": 0,
"insured_value": 50,
"buyback_amount": 42.5,
"escrow_signature": null,
"escrow_wallet": null,
"evm_contract_address": "0x34684f...",
"evm_token_id": "2615",
"transaction_signature": "0x77c1...ba90",
"transactionSignature": "0x77c1...ba90",
"chain_id": 84532,
"chain_key": "base-sepolia"
}
Field notes:
nftWonis the full card metadata, byte-identical to thenftWonreturned by the Solana/api/openPack. Read the image fromcontent.files[0].cc_cdn→content.files[0].cdn_uri→content.files[0].uri(in that preference order;files[1]is the card back), the name fromcontent.metadata.name, and the rest fromcontent.metadata.attributes[](trait_type/valuepairs —Insured Value,Category,Grading Company,GradeNum,The Grade,Year).content.links.imageis a fallback image.nftWonisnullonly if the card had no stored metadata — don't dereference it unguarded, and don't use it as a retry condition.rarityis lowercase:"epic","rare","uncommon","common".rarity_labelis the same value in the Solana API's capitalized form ("Epic","Rare","Uncommon","Common"), so a client shared with the Solana API can read one key on both.prize_tier:1= epic,2= rare,3= uncommon,4= common.pointsis always0. There is no points ledger on the EVM machine; the key exists so a shared client always gets a number.insured_valueandbuyback_amountare in whole USD, not base units.buyback_amountis an indicative quote, not a payment — the EVM machine never auto-sells. Get a signed, executable quote from/api/evm/buyback.transaction_signatureis the mint transaction hash.transactionSignatureis the same value under the Solana API's key name.evm_token_idis what you use for buybacks.escrow_signature/escrow_walletarenullon a fresh award;cron/evm-escrow-reapfills them only for cards that outlive the buyback window.- A replay of an already-opened pack returns the same keys plus
"replay": trueandstatus. On a replay,nftWonis served from the award record, so it survives even after the backing card is reaped.
Error Responses:
- 202:
WAITING_FOR_PAYMENT(payment not confirmed yet — retry, or passpayTxHash) ·PROCESSING(a concurrent open is in flight — poll) - 400: Missing
memo - 402:
PAYMENT_NOT_VERIFIED— thepayTxHashdoesn't contain a matchingPaidevent;reasonsays why - 404: No pending pack for that memo
- 502:
MINT_FAILED·MINT_PENDING— the card wasn't issued; your payment is on record and the pack stays completable - 503:
NO_INVENTORY
5. Pack Status — GET /api/evm/pack/status
Everything on record for one memo: the payment, the award, and any buybacks.
Query Parameters:
memo(required, orsignature— the pay transaction hash)
GET /api/evm/pack/status?memo=cc-6f3a1b2c-...
Response:
{
"memo": "cc-6f3a1b2c-...",
"pack": { "wallet": "0xEeC1...", "spin_cost": 50, "pack_type": "pokemon_50", "status": "confirmed", "transaction_signature": "0x9ab3...", "chain_id": 84532 },
"send": { "to_wallet": "0xEeC1...", "roll": 87654321, "prize_tier": 4, "evm_token_id": "2615", "evm_contract_address": "0x34684f...", "transaction_signature": "0x77c1...", "status": "confirmed" },
"buyback": []
}
pack and send are null until they exist; buyback is an array, newest first.
Selling a card back
The buyback is atomic: the server signs a price quote, and the vault pulls the NFT, pays you, and burns it in a single transaction you send. The server never holds your card.
Window: 72 hours from the award in production; the dev deployment runs a deliberately longer one, so don't calibrate against what you see there. Payout is the machine's buyback percentage (typically 85%) of the card's insured value.
1. Check eligibility — GET /api/evm/buyback/available
Read-only, no quote issued.
Query Parameters:
wallet(required): EVM addresstokenId(required): theevm_token_idfrom openPackcontract(optional): card contract; defaults to the configured onechainId(optional): defaults todefaultChainId. Pass the chain the card was awarded on — the same token id on another chain is a different card and will answeravailable: false.
GET /api/evm/buyback/available?wallet=0xEeC1...&tokenId=2615&chainId=84532
Response:
{ "available": true, "amount": 42.5, "amountBase": "42500000", "chainId": 84532 }
Returns { "available": false } when out of window, already sold back, or unknown.
2. Get a quote — POST /api/evm/buyback
Request Body:
{
"playerAddress": "0xEeC1...9A21",
"evmTokenId": "2615",
"evmContract": "0x34684f...",
"chainId": 84532
}
evmContract and chainId are optional. playerAddress must be the current on-chain owner — it's
checked.
Response:
{
"success": true,
"memo": "cc-6f3a1b2c-...",
"refundAmount": 42.5,
"refundAmountBase": "42500000",
"cardContract": "0x34684f...",
"tokenId": "2615",
"chainId": 84532,
"chainKey": "base-sepolia",
"vault": "0x2426a0B826E8FF4C5257C21Ff2B7D0602948084A",
"quoteId": 481,
"deadline": 1786000000,
"signature": "0x1c9d...ff",
"suggestedGasLimit": 250000,
"instructions": "Approve the vault for token 2615, then call sellBack(2615, 42500000, 1786000000, 481, signature) on 0x2426a0B8... with gas limit 250000 (a bare estimate reverts — unused gas is refunded)."
}
The quote is valid for 10 minutes (deadline, a unix timestamp). Re-requesting while it's live
returns the same amount and deadline.
The signature is bound to chainId and vault, so a quote from one chain is rejected by
another chain's vault. Send it to the vault in the same response.
Error Responses:
- 400: Invalid
playerAddressorevmTokenId, orUNSUPPORTED_CHAIN - 404:
NOT_ELIGIBLE— outside the window, or already bought back - 409:
NOT_TOKEN_OWNER(you don't hold it on-chain) ·ALREADY_SOLD·BACKING_RELEASED - 503:
INSUFFICIENT_FLOAT(retry shortly) ·OWNER_CHECK_UNAVAILABLE(retry) ·NO_VAULT. Also when thebuybackscope is paused — quotes already issued stay spendable until their deadline.
3. Approve the vault
approve(address vault, uint256 tokenId)
on the card contract.
4. Sell back
sellBack(uint256 tokenId, uint256 amount, uint256 deadline, uint256 quoteId, bytes signature)
on vault, using refundAmountBase, deadline, quoteId, and signature from the quote. One
transaction: your NFT is pulled, USDC is paid to you, the NFT is burned.
suggestedGasLimitDo not send this with a bare estimateGas. sellBack does three things in one transaction and
EIP-150 lets it forward only 63/64 of its remaining gas to the final burn, so a tight estimate leaves
the card collection's reentrancy guard short, the burn reverts, and the whole atomic sell unwinds —
you keep the card, get no USDC, and pay for the failed gas anyway. Measured over a 500-pack run,
that hit about 15% of sales.
Put suggestedGasLimit (currently 250000) on the transaction. Over-asking is free: unused gas is
refunded.
5. Settle — POST /api/evm/buyback/settle
Optional — records the completed sale immediately so it shows up in status right away.
Request Body:
{ "txHash": "0x5f21...c0de" }
Response:
{ "ok": true, "results": [ { "ok": true, "quoteId": 481 } ] }
Returns 409 if nothing settleable was found in that transaction.
Buyback Check — GET /api/evm/buyback/check
Query Parameters:
memo(required)
Response:
{
"exists": true,
"status": "complete",
"userWallet": "0xEeC1...",
"evmTokenId": "2615",
"evmContractAddress": "0x34684f...",
"refundAmount": 42.5,
"refundAmountBase": "42500000",
"payoutTxHash": "0x5f21...c0de",
"quoteId": "481",
"vault": "0xb66B22...6507",
"quoteExpiresAt": "2026-08-07T18:20:00.000Z",
"createdAt": "2026-08-07T18:10:00.000Z"
}
status is "quoted" while a quote is outstanding and "complete" once the sale is confirmed.
Returns { "exists": false } when there's no buyback for that memo.
Provable fairness — GET /api/evm/vrf/verify
Every EVM pack uses the same ECVRF roll as the Solana machine — see Provable Fairness (VRF).
Query Parameters:
memo(required)
GET /api/evm/vrf/verify?memo=cc-6f3a1b2c-...
Response:
{
"valid": true,
"memo": "cc-6f3a1b2c-...",
"proof": "0x...",
"publicKey": "0x...",
"actualRoll": 87654321,
"calculatedRoll": 87654321,
"rollMatches": true,
"wallet": "0xEeC1...",
"evmTokenId": "2615",
"chainId": 84532,
"algorithm": "ECVRF-SECP256K1-SHA256-TAI"
}
The roll is sha256(memo + payTxHash) fed through the VRF; valid and rollMatches must both be
true.
End-to-end example
Using Foundry's cast:
BASE=https://dev-gacha.collectorcrypt.com
PLAYER=0xEeC1...9A21
# 0. pick a chain
CHAINS=$(curl -s "$BASE/api/evm/chains")
CHAIN_ID=$(echo "$CHAINS" | jq -r '.chains[] | select(.paused == false and .ready) | .chainId' | head -1)
RPC=... # an RPC for that chain
# 1. reserve the pack
GEN=$(curl -s -XPOST "$BASE/api/evm/generatePack" \
-H 'content-type: application/json' \
-H 'x-api-key: API_KEY' \
-d "{\"playerAddress\":\"$PLAYER\",\"packType\":\"pokemon_50\",\"chainId\":$CHAIN_ID}")
MEMO=$(echo "$GEN" | jq -r .memo)
AMOUNT=$(echo "$GEN" | jq -r .amount)
USDC=$(echo "$GEN" | jq -r .usdc)
GATEWAY=$(echo "$GEN" | jq -r .paymentContract)
# 2 + 3. approve and pay
cast send "$USDC" "approve(address,uint256)" "$GATEWAY" "$AMOUNT" --rpc-url "$RPC"
PAYTX=$(cast send "$GATEWAY" "pay(uint256,string)" "$AMOUNT" "$MEMO" --rpc-url "$RPC" --json | jq -r .transactionHash)
# 4. open — mints the card
curl -s -XPOST "$BASE/api/evm/openPack" \
-H 'content-type: application/json' \
-H 'x-api-key: API_KEY' \
-d "{\"memo\":\"$MEMO\",\"payTxHash\":\"$PAYTX\"}"
Then to sell it back:
TOKEN_ID=2615
# 1. quote
BB=$(curl -s -XPOST "$BASE/api/evm/buyback" \
-H 'content-type: application/json' \
-d "{\"playerAddress\":\"$PLAYER\",\"evmTokenId\":\"$TOKEN_ID\",\"chainId\":$CHAIN_ID}")
VAULT=$(echo "$BB" | jq -r .vault)
CARD=$(echo "$BB" | jq -r .cardContract)
AMT=$(echo "$BB" | jq -r .refundAmountBase)
DL=$(echo "$BB" | jq -r .deadline)
QID=$(echo "$BB" | jq -r .quoteId)
SIG=$(echo "$BB" | jq -r .signature)
GAS=$(echo "$BB" | jq -r .suggestedGasLimit)
# 2 + 3. approve the vault, then sell back atomically — note --gas-limit, not an estimate
cast send "$CARD" "approve(address,uint256)" "$VAULT" "$TOKEN_ID" --rpc-url "$RPC"
SELL=$(cast send "$VAULT" "sellBack(uint256,uint256,uint256,uint256,bytes)" \
"$TOKEN_ID" "$AMT" "$DL" "$QID" "$SIG" --gas-limit "$GAS" --rpc-url "$RPC" --json | jq -r .transactionHash)
# 4. settle
curl -s -XPOST "$BASE/api/evm/buyback/settle" \
-H 'content-type: application/json' \
-d "{\"txHash\":\"$SELL\",\"chainId\":$CHAIN_ID}"
Differences from the Solana API
| Solana | EVM | |
|---|---|---|
| Payment | Server-built transaction you sign | You call pay(amount, memo) yourself |
| Prize | NFT transferred to you | ERC-721 minted to you |
| Buyback | Server-built transaction | Server-signed quote, you call sellBack |
| Refunds | Supported | None — payments are final |
| Networks | One | Several — carry chainId everywhere |
| Gas | You pay for the transactions you sign | Same, and sellBack needs suggestedGasLimit |
rarity casing | "Epic" | "epic" — plus rarity_label ("Epic") for parity |
| Values | Base units | Whole USD (insured_value, buyback_amount) |
| Auto-sell | Turbo mode returns code: "TURBO_MODE_BUYBACK" + buybackAmount | None — buyback_amount is a quote, never a payment |
points | Earned per pack | Always 0 — no points ledger |
openPack otherwise returns every key the Solana one does: success, roll, nft_address,
nftWon, points, transactionSignature, and rarity_label.