For developers / Application APIs

Faucet

The only first-party service that writes to the chain. It holds the operator key and submits the drip for you, which is why it needs no wallet - and why the quota is the only thing between a caller and that key.

What it does

You post an address; the service checks its own quota, signs a call to the faucet contract with the operator key, submits it and waits for the receipt before answering. There is no wallet on the page and no signature from you, because the transaction is not yours.

Unauthenticated, and it spends a real key

No token, no captcha, no signature. What stands in for authentication is three quotas reserved before anything is signed, plus a cooldown the contract itself enforces, so a caller who gets past one still meets the other. Nothing here is proof of anything about who you are.

Two of its routes are also re-exposed on the project's home origin - the drip and the status - so a page there can use it without a cross-origin call.

Base URL and CORS

EndpointValue
Publichttps://faucet.picklechain.xyzthe service serves its own static page from the same origin
Re-exposedPOST /drip and GET /faucet-statuson the project's home origin, proxied to the same service
A stack you runhttp://127.0.0.1:3000
CORSan allowlistOPTIONS answers 204 allowing POST, GET and OPTIONS; an Origin not on the list is refused 403

Everything not on the API routes or the explicit static allowlist is a 404 - never a 403, because a refusal that distinguishes the two confirms that a file exists.

Routes

Params
address (optional)
Returns
{faucet, pickle, ethAmount, bxAmount, ethAmountEth, bxAmountBx, treasuryEth, treasuryBx, treasuryEthFmt, treasuryBxFmt, cooldown, evmBlock, miniBlock}

The faucet and token addresses, the drip amounts in both base units and display units, what is left in the treasury and the cooldown in seconds. Pass `?address=` and it also answers `canDrip`, `retryIn` and a formatted `retryIn`.

Also at. GET /faucet-status

Worth knowing. The `bx`-prefixed keys are the PKL amounts. The names are historic and the SERVICE still emits them; the formatted string beside them says PKL. 502 when the node is unreachable, 500 for anything else.

/drip

POST
Params
body {"address": "0x..."}
Returns
{hash, address}

Reserves quota, signs `dripTo` with the operator key, submits it and polls for the receipt before answering. No authentication of any kind.

Worth knowing. It waits for the receipt: about three seconds of polling before it gives up with `no receipt yet`. A quota reservation is ROLLED BACK when the send fails, so a failed attempt does not cost you a daily slot.

Params
none
Returns
the deployment manifest, as JSON

Streams the manifest file straight through. This is where an application discovers what is deployed.

Also at. GET /op-deployment/l2-addresses.json

Worth knowing. 404 with a JSON body when the file cannot be read, which is also what you get before a deployment has written it.

any

OPTIONS
Params
none
Returns
204

Allowed methods are POST, GET and OPTIONS; the allowed header is content-type.

Worth knowing. 403 when an Origin is present and not on the allowlist.

bash
# What is left, and whether this address may claim right now.
curl -s "$FAUCET/status?address=$ADDR" | jq '{canDrip, retryIn, retryInFmt, treasuryEthFmt}'

# The claim. A body is required: a zero-length one answers 413, not 400.
curl -s -X POST "$FAUCET/drip" \
  -H 'content-type: application/json' \
  -d "{\"address\":\"$ADDR\"}"

# 200 -> {"hash": "0x…", "address": "0x…"}; the receipt has already been seen.

The quota

Quota is RESERVED before the transaction is signed and committed only once a hash comes back, so two simultaneous requests cannot both pass the check. The journal that backs it is an append-only file outside the served directory, rotated by size and by age, with a checkpoint at the head of each new file so the all-time total survives both rotation and a restart.

BudgetValue
Per client address5 a daykeyed on the forwarded client address
Per funded address2 a daykeyed on the address in the body
Globally1000 a dayand a separate all-time budget
On-chain cooldownone day per addressenforced by the contract and read from the chain, not remembered in the service

Two different refusals mean two different things

A 429 is the service's own daily quota and resets at the turn of the day. A 409 is the contract's cooldown, is counted from the last drip rather than from midnight, and carries the interval to wait in its message. Only the second is visible in advance: ask /status?address= and read canDrip and retryIn before posting.

The per-address budget is keyed on the FUNDED address and the per-client one on the address the request came from, read from the right-hand end of the forwarded chain. Put a cache in front without telling the service how many proxies it now sits behind and every visitor collapses into one bucket.

Errors

StatusMeaning
400invalid json, invalid address, invalid content lengthand a chunked request, which the handler refuses outright
409cooldownthe message carries the interval to retry in. The cooldown is enforced by the contract, one day per address, and is read from the chain rather than remembered here
413payload too largethe cap is 4096 bytes - and a ZERO-LENGTH body answers 413 as well, which reads as the opposite of what happened. Send a body
429quota exceeded5 a day per address seen, 2 a day per funded address, 1000 a day globally; the message says which one you hit
502upstream RPC unavailablethe node did not answer. Nothing was signed
503faucet empty, or the drip failedthe message names the remaining balance when it is a funding problem, and is the opaque `drip failed` otherwise

A 503 after the send is not a safe retry

A 502 means the node never answered and nothing was signed - retry that one freely. A 503 can mean the transaction went out and then failed, or that it went out and no receipt arrived within the polling window. The quota reservation is rolled back either way, so retrying is permitted; the transaction may still land. Poll the chain for the address's balance rather than assuming.