CoinGecko's keyless API is the default data source for every "build a crypto dashboard" tutorial โ no key, CORS open, rich shapes. It also has the tightest rate limit of any tutorial API we've tested. While writing this guide (September 2026) we sent a small burst of /simple/price calls with varying params, the way a dashboard with a coin picker would. Request 4:
$ curl "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
HTTP/2 429
{"status":{"error_code":429,"error_message":"You've exceeded the Rate Limit.
Please visit https://www.coingecko.com/en/api/pricing to subscribe to our API
plans for higher rate limits."}}
What the free tiers actually allow, from CoinGecko's own pages (checked September 2026):
None of this is a complaint โ live market data is genuinely expensive to serve, and for production you should use their API (or pay for it). But while you're building the dashboard โ hot-reloading forty times an hour, running Playwright suites, letting CI loose โ you don't need live prices. You need the exact response shapes, deterministically, at zero cost. That's a mock.
Pull real market data one time, then host it as your own API. Mockbird record ids are integers, so keep CoinGecko's slug in a coin_id field (one jq rename):
curl -s "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&per_page=3" \
| jq '{coins: map(.coin_id = .id | del(.id))}' > db.json
curl -s -X POST "https://mockbird.mockbird.workers.dev/api/projects/import?name=coingecko-mock" \
-H 'content-type: application/json' --data-binary @db.json
(One real call โ if it 429s, wait a minute; you only need it once. Bump per_page for more coins.) No signup, no key. The response has your project id and adminKey, plus one honest warning โ roi is a nested object, kept verbatim in the records but not in the flat field schema:
{"id":"YOUR_ID","adminKey":"โฆ",
"warnings":["coins.roi: nested value kept in records but not in the field schema"]}
Every field survives verbatim โ current_price, market_cap_rank, ath_change_percentage, the coin image URLs, all of it. And your copy answers with the full query toolkit the real endpoint doesn't have on the free tier:
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/coins?symbol=btc"
[{"id":1,"coin_id":"bitcoin","symbol":"btc","name":"Bitcoin","current_price":81227,โฆ}]
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/coins?select=name,current_price,market_cap_rank"
[{"id":1,"name":"Bitcoin","current_price":81227,"market_cap_rank":1},
{"id":2,"name":"Ethereum","current_price":2631.22,"market_cap_rank":2},
{"id":3,"name":"Tether","current_price":0.999714,"market_cap_rank":3}]
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/coins?current_price_gte=1000"
# โ bitcoin, ethereum
Free bonuses the real API doesn't have: /m/YOUR_ID/types.ts generates TypeScript interfaces for your dashboard from the imported schema (?format=zod for Zod), and /graphql serves the same records with typed queries.
If your code hardcodes CoinGecko's paths, reproduce them as custom routes so only the base URL changes. We verified each of these parse-equal against the live API's response while writing this:
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/api/v3/ping","body":{"gecko_says":"(V3) To the Moon!"}}'
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/api/v3/simple/price","body":{"bitcoin":{"usd":81240,"eur":70714},
"ethereum":{"usd":2631.73,"eur":2290.75}}}'
Same for /api/v3/coins/markets with the captured array as the body (jq '{path:"/api/v3/coins/markets", body:.}' markets.json builds the payload). Then your app's fetch works unchanged:
$ curl "https://mockbird.mockbird.workers.dev/m/YOUR_ID/api/v3/simple/price?ids=bitcoin&vs_currencies=usd"
{"bitcoin":{"usd":81240,"eur":70714},"ethereum":{"usd":2631.73,"eur":2290.75}}
Honest caveat: these route bodies are static โ ?ids= and ?vs_currencies= are ignored, every caller gets the full object. Fetching a superset and reading the key you need is exactly what most dashboard code does anyway. If you need the query params to actually filter, use the /coins resource from ยง1.
The best reason to mock a rate-limited API: you can finally test the limit. Your retry logic has almost certainly never seen a real 429 in CI โ you can't order one from CoinGecko without burning your quota (and remember, their errors count against you). Order one from your mock:
$ curl -i "https://mockbird.mockbird.workers.dev/m/YOUR_ID/api/v3/coins/markets?mock_ratelimit=3"
# requests 1โ3 โ 200, with live x-ratelimit-limit / x-ratelimit-remaining headers
# request 4:
HTTP/2 429
retry-after: 34
x-ratelimit-limit: 3
x-ratelimit-remaining: 0
{"error":"simulated rate limit exceeded (mock_ratelimit=3 per 60s)","retry_after":34}
?mock_ratelimit=N allows N requests per 60s per client IP, then 429s with a real Retry-After โ point your countdown UI and backoff at it. For deterministic tests, ?mock_seq=429,429,200 fails exactly twice then succeeds โ assert your third attempt renders data. And to test your parser of CoinGecko's specific error shape, add a route that serves their exact 429 body (copied verbatim from the real response up top):
curl -X POST "https://mockbird.mockbird.workers.dev/api/projects/YOUR_ID/routes" \
-H "x-admin-key: YOUR_ADMIN_KEY" -H 'content-type: application/json' \
-d '{"path":"/drills/429","status":429,"body":{"status":{"error_code":429,"error_message":"You'"'"'ve exceeded the Rate Limit. Please visit https://www.coingecko.com/en/api/pricing to subscribe to our API plans for higher rate limits."}}}'
Now expect(err.status.error_code).toBe(429) runs against the same bytes production will see. More rate-limit recipes: the rate-limit simulation guide.
| CoinGecko keyless | CoinGecko Demo key | Your mock | |
|---|---|---|---|
| Real live prices | โ | โ | โ frozen at capture โ by design |
| Signup / key | none | account + key | none |
| Rate limit | IP-based, shared with your whole NAT (~5โ15/min) | 100/min, 10,000/month | 10,000 per day per project |
| Coin catalogue | 17,000+ coins, deep history | whatever you import | |
| Deterministic for tests | โ prices move | โ | โ |
| Can simulate 429 / errors / latency | โ (429s cost quota) | โ | โ mock_ratelimit/mock_seq/mock_delay |
| Right for | production | dev, CI, demos, workshops | |
The pattern that uses each where it wins: base URL in an env var โ COINGECKO_BASE=https://api.coingecko.com in production, your /m/YOUR_ID in .env.test. Your quota gets spent on users, not on your test suite. Related: the CoinDesk BPI API is dead (where many of these tutorials started), mock stock quotes, mock fiat exchange rates, mock any third-party API.