Skip to content

About

Simple Koa service to facilitate the for AR.IO Testnet Network.

Resources

Stars

0 stars

Watchers

3 watching

Forks

Latest commit

Β 

History

111 Commits

Folders and files

Repository files navigation

🚰 AR.IO Faucet Service

This service supports a request-based workflow for acquiring tokens on the AR.IO Testnet Network (tARIO).

Table of Contents

Claiming Tokens

Synchronous Workflow

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
Loading

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.

Asynchronous Workflow

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
Loading

Requesting a Captcha URL

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.

Generating an Authorization 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.

Verifying an Authorization Token

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.

Claiming Tokens with an Authorization Token

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> }'

Rate Limiting

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.

Captcha Protection

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.

Security Model & Single-Instance Limitation

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 sid in the JWT is matched against the faucet_sid HttpOnly 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 bare Authorization: Bearer header without the matching session cookie, and vice versa. This session binding is intentional and must not be weakened β€” do not disable the sid check 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.

Deployment topology (AR.IO testnet)

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 auth
  • GITHUB_OAUTH_CALLBACK_URL=https://faucet.services.ar-io.dev/api/auth/github/callback
  • COOKIE_SAMESITE=lax (the default; safe for same-site β€” see the OAuth note below)
  • leave CORS_ALLOWED_ORIGINS unset (defaults to FRONT_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 from github.com, so the browser must send the faucet_sid session cookie back on it. lax is generally sufficient (top-level navigations receive lax cookies). If a user hits this error, it is usually a stale faucet_sid cookie β€” retry in a fresh/incognito session. If it persists across browsers/clients, set COOKIE_SAMESITE=none (requires COOKIE_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.

Notifications (Slack)

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).

Environment Variables

The service supports the following environment variables:

  • TRUST_PROXY: Set to true only when running behind a known reverse proxy so X-Forwarded-For is trusted; otherwise the header is ignored (default: false).

  • COOKIE_SECURE: Whether the session/claim cookies carry the Secure flag. Defaults to true; set to false only 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 to https://hcaptcha.com/siteverify).

  • REQUIRE_CAPTCHA_VERIFICATION: Whether captcha verification is required, defaults to true.

  • ENABLE_SELF_HOSTED_FRONTEND: Whether the self-hosted front-end is enabled, defaults to true.

  • 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. Requires SLACK_WEBHOOK_URL.

About

Simple Koa service to facilitate the for AR.IO Testnet Network.

Resources

Stars

0 stars

Watchers

3 watching

Forks

Packages

Contributors

Languages