This Repo contains the code for the celo testnets faucet. This is contained in 2 apps.
-
The firebase app contains functions which do the actual fauceting.
-
The web app contains a UI for making requests.
The web app deploys automatically to vercel.
The deploy-chains gh actions deploys functions to staging and production environments from staging and master branches respectively
Note other branches are not deployed automatically but can be by manually triggering the flow
To set up the web app to run locally:
-
navigate to the
apps/webfolder$ cd apps/web -
link your local repository to the
faucetproject on Vercel$ yarn dlx vercel@latest link
You'll be asked to authenticate with your Vercel account. Once you've done that, you'll be guided through a series of prompts to link your local project to the
faucetVercel project.? Set up “~/Documents/celo-org/faucet/apps/web”? [Y/n] y ? Which scope should contain your project? Celo Ecosystem Project Hosting ? Link to existing project? [y/N] y ? What’s the name of your existing project? faucet ✅ Linked to c-labs/faucet (created .vercel) -
fetch environment variables from Vercel
$ yarn dlx vercel@latest env pull
If you get an error like
Error! No project found, you may need to runvercel linkagain. If everything worked, you should see a message like this:> Downloading `development` Environment Variables for Project faucet ✅ Created .env.local file [249ms]
-
run the app locally
$ yarn dev
You should see a message like this:
ready - started server on 0.0.0.0:3000, url: http://localhost:3000 info - Loaded env from /Users/arthur/Documents/celo-org/faucet/apps/web/.env.local
You can now view the app in your browser at http://localhost:3000.
To set up the firebase app to run locally:
- navigate to the
apps/firebasefolder$ cd apps/firebase - login to firebase
You'll be asked to authenticate with your Firebase account.
$ yarn dlx firebase-tools@latest login
- build the firebase app
$ yarn run preserve
- ensure that you are on required node version specified in
engines.nodeinfirebase/package.json. Currently this is Node 24 at the time of writing.$ nvm use <the-required-node-version>
- run the firebase app locally
$ yarn run serve
The browser flow is gated on reCAPTCHA v3, which scores headless callers badly and cannot be solved without driving a real browser. Scripts and AI agents use an API key instead.
Sign in with GitHub at /keys and create one.
The key is shown once and only a hash of it is stored, so it cannot be
recovered — create a new one if you lose it. You may hold up to 2 keys and they
expire after 90 days.
Send the key as a bearer token and omit captchaToken:
curl -X POST https://faucet.celo.org/api/faucet \
-H "Authorization: Bearer cfk_prd_…" \
-H 'Content-Type: application/json' \
-d '{"beneficiary":"0xYourAddress","network":"celo-sepolia"}'A successful call returns 202-style bookkeeping: {"status":"Pending","key":"…"}.
Poll for the outcome with that key:
curl "https://faucet.celo.org/api/status?key=<key>&network=celo-sepolia"
# {"status":"Done","beneficiary":"0x…","txHash":"0x…"}Other responses carry a machine-readable error code so you can branch without
parsing prose:
| Status | error |
Meaning |
|---|---|---|
400 |
— | Invalid network or beneficiary address |
401 |
invalid_api_key |
Missing, malformed, unknown or expired key |
401 |
api_key_disabled |
Programmatic access is switched off |
429 |
faucet_limit_exceeded |
Rate limited; honour Retry-After |
405 |
— | Wrong HTTP method |
The browser flow keeps its existing 403 for rate limits; 429 is used only on
the key path, matching the Coinbase CDP faucet.
- Keyed requests count against your GitHub account's existing daily allowance (10 per 24h), the same bucket the signed-in browser flow uses. A key changes how you prove who you are, not how much you can request.
- Holding two keys does not double your allowance — the limit is bound to the account, not the key, as it is on the Coinbase CDP faucet.
- The per-address limit is shared with the browser path.
- Two ceilings apply across all API keys: a daily one that bounds total spend, and a 10-minute burst window that bounds arrival rate. The burst window is what stops programmatic traffic locking the payout pool and starving the browser flow.
- Testnet only. There is no mainnet exposure.
Never commit a key. Keys found in public repositories are revoked without notice. If you are integrating a tool that others will install, read the key from the user's environment — do not ship a default.
Environment variables on the web app:
| Variable | Required | Purpose |
|---|---|---|
FAUCET_API_KEY_PEPPER |
yes | Secret mixed into the HMAC of every stored key. Without it, key minting and verification both fail closed. Rotating it invalidates every existing key. |
FAUCET_API_KEY_NETWORKS |
no | Comma-separated networks that accept key auth. Defaults to celo-sepolia. |
PROGRAMMATIC_GLOBAL_DAILY_LIMIT |
no | Daily ceiling across all keys. Defaults to 200. |
PROGRAMMATIC_BURST_LIMIT |
no | Keyed requests allowed per 10 minutes across all keys. Bounds arrival rate so the payout pool cannot be locked. Defaults to 20. |
To stop all programmatic traffic immediately without a deploy, set the
api-keys:disabled key in Upstash to any truthy value. The browser flow is
unaffected. Delete it to re-enable.
apps/web/scripts/smoke-api-key.mjs exercises the whole key path over HTTP —
rejection of unissued keys, address and method validation, a real payout, the
status poll, and the returned tx hash. It needs no repo credentials:
export FAUCET_API_KEY=cfk_... # minted at <deployment>/keys
yarn --cwd apps/web smoke:api-key \
--url https://<deployment> --beneficiary 0xYourAddressIt exits non-zero if any check fails and never prints the key.
Every other test mocks the sender, so nothing otherwise proves the hash written
to goldTxHash is a real transaction. apps/firebase/src/celo-adapter.onchain.test.ts
sends a real transfer on Celo Sepolia. It is skipped unless explicitly opted in,
because it spends gas:
RUN_ONCHAIN_TESTS=1 PRIVATE_KEY=<funded key> \
yarn --cwd apps/firebase test:ci onchainThe transfer is a self-send, so only the gas fee (~0.0011 CELO at the 50 gwei floor) is consumed, and the test asserts the balance falls by exactly that fee.
Keys are self-serve on purpose. Allowlisted keys need a human to triage each request and hand the secret over out of band, which does not scale and tends to leave secrets in chat logs. Tying keys to a GitHub account instead reuses an identity the faucet already understands, which is also what the Coinbase CDP, Circle, and Chainstack faucets do.
If you are testing contracts in CI you probably do not want the faucet at all.
anvil --fork-url https://forno.celo-sepolia.celo-testnet.org gives you
pre-funded accounts against forked state, with no rate limits and no network
flakiness. Reach for a key only when you need funds on the real testnet.
-
Add the chain config and token info to
config/chains.ts. -
Add chain name to the networks array, and
ChainIdandFaucetAddressto enums intypes/index.ts.
Dispatch the deploy-chains workflow. ensure chain name is kebab case and matches a network in config/chains.ts.