This service supports a request-based workflow for acquiring tokens on the AR.IO Testnet Network (tARIO).
sequenceDiagram
participant Client/User
participant Backend
participant Token
Client/User->>Backend: Send token request with captcha response
Backend->>Backend: Rate limit check
Backend-->>Client/User: Return 429 if rate limited
Backend->>Backend: Captcha verification
Backend-->>Client/User: Return 400 if captcha verification fails
Backend->>Backend: Faucet wallet balance check
Backend->>Token: Send transfer notice to contract
Token-->>Backend: Confirm transfer
Backend-->>Client/User: Return transfer message id
To claim tokens synchronously with a captcha response, send a POST request to the /api/claim/sync endpoint with the following JSON body:
curl -X POST http://localhost:3000/api/claim/sync -H "Content-Type: application/json" -d '{"captchaResponse": "<captcha-response>", "recipient": "<recipient-address>", "qty": "<quantity>"}'The response will be a JSON object with the following properties:
id: The transaction id of the token transfer, if successful.status: The status of the claim request.error: The error message if the claim request failed.
sequenceDiagram
participant Client/User
participant Backend
participant Token Contract
Note over Client/User,Token Contract: Authorization Flow
Client/User->>Backend: Request captcha URL
Backend-->>Client/User: Return captcha URL
Client/User->>Backend: Solve captcha
Backend->>Backend: Rate limit check
Backend-->>Client/User: Return 429 if rate limited
Backend->>Backend: Verify captcha
Backend-->>Client/User: Return auth token
Note over Client/User,Token Contract: Async claim Flow
Client/User->>Backend: Send claim request with auth token, recipient address, and quantity
Backend->>Backend: Rate limit check
Backend->>Backend: Verify auth token
Backend->>Backend: Verify faucet balance
Backend->>Token Contract: Send transfer notice to recipient
Token Contract-->>Backend: Confirm transfer
Backend->>Backend: Mark authorization token as used
Backend-->>Client/User: Return transfer message id
Users can request a captcha URL by sending a GET request to the /api/captcha/url endpoint with the process-id in the query parameters.
curl -X GET http://localhost:3000/api/captcha/url?process-id=<processId>The response will be a JSON object with the following properties:
processId: The processId of the process that is requesting the token.captchaUrl: The URL for the captcha. This URL will redirect to the front-end where the user can solve the captcha and then return to the back-end with the token.
Users can generate an authorization token by sending a POST request to the /api/captcha/verify endpoint with the processId and captchaResponse in the body.
curl -X POST http://localhost:3000/api/captcha/verify -H "Content-Type: application/json" -d '{"processId": "<processId>", "captchaResponse": "<captcha-response>"}'The response will be a JSON object with the following properties:
status: The status of the captcha verification.token: The auth token that can be used to claim tokens.expiresAt: The timestamp when the token will expire.
Users can verify an existing authorization token by sending a GET request to the /api/token/verify endpoint with the token in the query parameters.
curl -X GET http://localhost:3000/api/token/verify?process-id=<processId> \
-H "Authorization: Bearer <auth-token>"The response will be a JSON object with the following properties:
valid: Whether the token is valid and can be used to claim tokens.expiresAt: The timestamp when the token will expire.
Users can then claim tokens to a recipient by sending a POST request to the /api/claim/async endpoint with the authorization token returned after the captcha is solved. The authorization token is verified, the faucet balance is checked, and the tokens are transferred to the recipient's wallet address.
curl -X POST http://localhost:3000/api/claim/async \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <auth-token>" \
-d '{"processId": "<process_id>", "recipient": "<recipient_address>", "qty": <qty> }'The service includes various rate limiting mechanisms to prevent abuse, defaulting to a global rate limit of 10 requests per 8-hour window and a captcha rate limit of 1 request per hour. This can be adjusted by changing the *_RATE_LIMIT_* environment variables.
The service includes a hCaptcha protection mechanism to prevent abuse. By default, the service will require a captcha to be solved before a token can be claimed. This can be disabled by setting the DISABLE_CAPTCHA_VERIFICATION environment variable to true.
Anti-replay and anti-sybil state (consumed token nonces and the per-GitHub-id
claim window) are held in-process, in memory. The concurrent-replay drain β where
N simultaneous requests carrying the same JWT could all pass the check and all
transfer β is closed by an atomic reserve-before-transfer: the nonce (and,
at the OAuth callback, the per-GitHub-id slot) is reserved with a synchronous
set-if-absent (has()+set() in one critical section, no await in between)
before the on-chain transfer is dispatched. Of N concurrent claims sharing a
token, exactly one wins the reservation; the rest are rejected before any tokens
move. Reservations roll back only on a definitive transfer failure.
Accepted limitation (single-box devnet target): because this state is
per-process, it resets on restart and is NOT shared across replicas. Running
multiple instances behind a load balancer would let the same JWT/GitHub id claim
once per replica, and a restart clears the window early. This is acceptable for
the intended single-instance devnet faucet. A durable, shared store
(SQLite/Redis) is intentionally deferred; see the TODO markers in
src/cache/token-cache.ts. Do not run more than one replica without adding one.
The browser claim flow delivers the claim JWT via an HttpOnly + Secure +
SameSite=Lax cookie scoped to /api (not a URL fragment), bound to the session
that initiated the OAuth flow (a session id is carried in the OAuth state and
embedded in the JWT, then matched against the faucet_sid cookie at claim time).
Note: Because GitHub-gated claim tokens are bound to the initiating browser session (the
sidin the JWT is matched against thefaucet_sidHttpOnly cookie), the browser OAuth flow and header-only API clients are mutually exclusive for gated tokens: a JWT minted in the browser session cannot be replayed from a bareAuthorization: Bearerheader without the matching session cookie, and vice versa. This session binding is intentional and must not be weakened β do not disable thesidcheck to make gated tokens usable from header-only clients.
When running behind a reverse proxy, set TRUST_PROXY=true so X-Forwarded-For
is honored (via ctx.ip) for rate limiting and hCaptcha. When it is unset the
header is ignored and the socket address is used, so clients cannot spoof their
source IP to bypass rate limits.
The AR.IO testnet faucet runs as a single origin β faucet.services.ar-io.dev
(matching the bundler's *.services.ar-io.dev convention) serves both the API
and the built-in self-hosted UI (ENABLE_SELF_HOSTED_FRONTEND=true). Because the
UI and API share an origin, cookies are same-site and no CORS grant is needed:
FRONT_END_URL=https://faucet.services.ar-io.devβ the faucet itself; this is where the OAuth callback redirects after authGITHUB_OAUTH_CALLBACK_URL=https://faucet.services.ar-io.dev/api/auth/github/callbackCOOKIE_SAMESITE=lax(the default; safe for same-site β see the OAuth note below)- leave
CORS_ALLOWED_ORIGINSunset (defaults toFRONT_END_URL)
Run behind a TLS-terminating reverse proxy that forwards X-Forwarded-Proto: https
and overwrites X-Forwarded-For (with TRUST_PROXY=true + COOKIE_SECURE=true).
OAuth callback &
Session mismatch for OAuth callback. The GitHub callback returns as a cross-site top-level redirect fromgithub.com, so the browser must send thefaucet_sidsession cookie back on it.laxis generally sufficient (top-level navigations receivelaxcookies). If a user hits this error, it is usually a stalefaucet_sidcookie β retry in a fresh/incognito session. If it persists across browsers/clients, setCOOKIE_SAMESITE=none(requiresCOOKIE_SECURE=true).
Optional split-origin mode. If you instead host the UI on a different site
(e.g. a frontend on Arweave at faucet.ar.io with this API at
faucet.services.ar-io.dev), that's cross-site, so you must additionally set
CORS_ALLOWED_ORIGINS=https://faucet.ar.io (credentialed CORS), COOKIE_SAMESITE=none
(requires COOKIE_SECURE=true), and FRONT_END_URL=https://faucet.ar.io.
Agents/devs: see
.claude/skills/ario-testnet-faucet/SKILL.md
for the API + claim flow.
Set SLACK_WEBHOOK_URL (a Slack Incoming Webhook)
to post to a channel on faucet activity. Both messages are fire-and-forget (5s
timeout, errors swallowed) β a missing, misconfigured, or unreachable webhook never
blocks or fails a claim. Notifications are disabled when SLACK_WEBHOOK_URL is unset.
- On each successful claim: the GitHub login, amount, recipient, and a Solana explorer link.
- Low balance (optional): set
SLACK_LOW_BALANCE_THRESHOLD(in SPL base units) to also warn when the faucet's remaining balance drops below it after a claim β e.g.50000000000= 50,000 ARIO at 6 decimals.
Treat the webhook URL as a secret (anyone with it can post to the channel).
The service supports the following environment variables:
-
TRUST_PROXY: Set totrueonly when running behind a known reverse proxy soX-Forwarded-Foris trusted; otherwise the header is ignored (default:false). -
COOKIE_SECURE: Whether the session/claim cookies carry theSecureflag. Defaults totrue; set tofalseonly for local plain-HTTP development. -
GLOBAL_RATE_LIMIT_WINDOW_SECONDS: The global rate limit window in seconds (e.g. 1 hour) -
GLOBAL_RATE_LIMIT_THRESHOLD: The global rate limit threshold (e.g. 100 requests per window) -
CAPTCHA_RATE_LIMIT_WINDOW_SECONDS: The captcha rate limit window in seconds (e.g. 1 hour) -
CAPTCHA_RATE_LIMIT_THRESHOLD: The captcha rate limit threshold (e.g. 100 requests per window) -
CAPTCHA_ENABLED: Whether captcha protection is enabled. By default, the service will require a captcha. -
CAPTCHA_SECRET: The secret key for the captcha. This is used to verify the captcha on the back-end. -
CAPTCHA_SITE_KEY: The site key for the captcha. This is used to render the captcha on the front-end. -
CAPTCHA_SITE_VERIFY_URL: The URL for the captcha site verify endpoint (defaults tohttps://hcaptcha.com/siteverify). -
REQUIRE_CAPTCHA_VERIFICATION: Whether captcha verification is required, defaults totrue. -
ENABLE_SELF_HOSTED_FRONTEND: Whether the self-hosted front-end is enabled, defaults totrue. -
WALLET_FILE: The path to the wallet file. This wallet is must have sufficient balance of requested tokens. -
DEFAULT_FAUCET_TOKEN_TRANSFER_QTY: The default quantity of tokens to transfer when claiming tokens. -
DEFAULT_MIN_FAUCET_TOKEN_TRANSFER_QTY: The minimum quantity of tokens to transfer when claiming tokens. -
DEFAULT_MAX_FAUCET_TOKEN_TRANSFER_QTY: The maximum quantity of tokens to transfer when claiming tokens. -
PORT: The port for the service to run on -
LOG_LEVEL: The log level for the service. -
LOG_FORMAT: The log format for the service. -
SLACK_WEBHOOK_URL: Slack Incoming Webhook for claim + low-balance alerts. Unset disables notifications. Treat as a secret. -
SLACK_LOW_BALANCE_THRESHOLD: Low-balance alert threshold in SPL base units; when set, warns after a claim if the faucet's remaining balance drops below it. RequiresSLACK_WEBHOOK_URL.