Skip to main content

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

APIhttps://dev-gacha.collectorcrypt.com
Payment tokenUSDC (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.
  • usdcDecimals is not always 6. Binance-Peg USDC on BNB is 18. Never build an amount yourself from a human number — use the base-unit amount the API hands you.
  • Omitting chainId on a request means defaultChainId. Sending one we don't serve is never a silent fallback: generatePack and buyback answer 400 UNSUPPORTED_CHAIN and name the chains that are enabled. buyback/available is a pure read and simply answers available: 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.

warning

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 memo with 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 machine without 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, or UNSUPPORTED_CHAIN
  • 503: MACHINE_OFF, OFF_BALANCE, MACHINE_LOW, NO_INVENTORY — retry shortly or use another machine. Also returned when the chain or its generatePack scope is paused; /api/evm/chains says 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.

warning

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:

  • nftWon is the full card metadata, byte-identical to the nftWon returned by the Solana /api/openPack. Read the image from content.files[0].cc_cdncontent.files[0].cdn_uricontent.files[0].uri (in that preference order; files[1] is the card back), the name from content.metadata.name, and the rest from content.metadata.attributes[] (trait_type / value pairs — Insured Value, Category, Grading Company, GradeNum, The Grade, Year). content.links.image is a fallback image. nftWon is null only if the card had no stored metadata — don't dereference it unguarded, and don't use it as a retry condition.
  • rarity is lowercase: "epic", "rare", "uncommon", "common". rarity_label is 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.
  • points is always 0. There is no points ledger on the EVM machine; the key exists so a shared client always gets a number.
  • insured_value and buyback_amount are in whole USD, not base units. buyback_amount is an indicative quote, not a payment — the EVM machine never auto-sells. Get a signed, executable quote from /api/evm/buyback.
  • transaction_signature is the mint transaction hash. transactionSignature is the same value under the Solana API's key name.
  • evm_token_id is what you use for buybacks.
  • escrow_signature / escrow_wallet are null on a fresh award; cron/evm-escrow-reap fills them only for cards that outlive the buyback window.
  • A replay of an already-opened pack returns the same keys plus "replay": true and status. On a replay, nftWon is 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 pass payTxHash) · PROCESSING (a concurrent open is in flight — poll)
  • 400: Missing memo
  • 402: PAYMENT_NOT_VERIFIED — the payTxHash doesn't contain a matching Paid event; reason says 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, or signature — 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 address
  • tokenId (required): the evm_token_id from openPack
  • contract (optional): card contract; defaults to the configured one
  • chainId (optional): defaults to defaultChainId. Pass the chain the card was awarded on — the same token id on another chain is a different card and will answer available: 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 playerAddress or evmTokenId, or UNSUPPORTED_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 the buyback scope 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.

Set the gas limit from suggestedGasLimit

Do 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

SolanaEVM
PaymentServer-built transaction you signYou call pay(amount, memo) yourself
PrizeNFT transferred to youERC-721 minted to you
BuybackServer-built transactionServer-signed quote, you call sellBack
RefundsSupportedNone — payments are final
NetworksOneSeveral — carry chainId everywhere
GasYou pay for the transactions you signSame, and sellBack needs suggestedGasLimit
rarity casing"Epic""epic" — plus rarity_label ("Epic") for parity
ValuesBase unitsWhole USD (insured_value, buyback_amount)
Auto-sellTurbo mode returns code: "TURBO_MODE_BUYBACK" + buybackAmountNone — buyback_amount is a quote, never a payment
pointsEarned per packAlways 0 — no points ledger

openPack otherwise returns every key the Solana one does: success, roll, nft_address, nftWon, points, transactionSignature, and rarity_label.