A modular, embeddable dApp that lets anyone receive Arc Testnet USDC in a single click — drop it into any project (PARSEC, a MetaMask dApp, plain HTML) with one tag.
It plugs into any EVM browser wallet that exposes an EIP-1193 provider — MetaMask, Coinbase Wallet, Rabby, Brave — and automates the whole flow from request to receipt:
pick a recipient -> (switch to Arc Testnet) -> prove/skip -> faucet sends USDC -> explorer link
Grounded in the lablab.ai tutorial
Getting Started with Arc Testnet: Send USDC with ethers.js —
USDC is dispensed via the ERC-20 transfer() interface on Arc's USDC system contract.
The <arc-faucet> widget offers three recipient modes:
- Connect wallet — connect an existing EIP-1193 wallet; sign a free message to prove address control. Verified claim.
- Generate a wallet — have a wallet created in your browser for you; the private key is shown locally (copy it / download an encrypted keystore) and used to sign. The key never leaves your browser. Verified claim.
- Enter an address — paste any
0x…address. Unverified claim — rate-limited only.
As an end user of this app, you can — that is exactly what the widget delivers. A wallet signature costs no gas and is proof of ownership, not a credential.
The friction is supply-side: a faucet pays out of a pre-funded treasury wallet an operator funds first; that funding comes from Circle's faucet, which caps ~20 USDC per address per 2h and is captcha-gated (its API needs a paid mainnet account). And every public faucet must rate-limit to survive bots. See POLICY.md.
This project ships a committed demo treasury (treasury.demo.json) so the app runs with
zero setup — once that wallet is funded (one human captcha click), every end user afterwards
claims with no captcha.
The widget is a framework-agnostic custom element. It works in plain HTML, React, Vue, PARSEC, or any MetaMask-style dApp.
Plain HTML / any project — single file, no build:
<script type="module" src="/path/to/arc-faucet.iife.js"></script>
<arc-faucet faucet-url="https://your-faucet.example"></arc-faucet>ESM / bundled projects:
import '@arc-faucet/widget'; // registers the <arc-faucet> element<arc-faucet faucet-url="https://your-faucet.example"></arc-faucet>The element emits an arc-faucet:claim DOM event on success (detail = the confirmed claim).
For a custom UI, use the headless @arc-faucet/wallet-core module
directly — see "Using the wallet module" below.
Just a browser. No sign-up, no API key, no gas. Either connect a wallet, let the app generate one, or paste an address.
The faucet dispenses from a treasury wallet. Two ways to provide it:
- Zero-config demo: the committed
packages/faucet-server/treasury.demo.json— a deliberately-public throwaway wallet (testnet only, no value). Used automatically when no env key is set. Anyone can fund or drain it; never send real funds to it. - Production: set
TREASURY_PRIVATE_KEYinpackages/faucet-server/.env(git-ignored) — this overrides the demo wallet and is the only true secret.
| Variable | Secret? | Purpose | Default |
|---|---|---|---|
TREASURY_PRIVATE_KEY |
secret (optional) | Treasury wallet key. If unset, the committed demo wallet is used. | (demo wallet) |
ARC_TESTNET_RPC_URL |
no | Arc Testnet JSON-RPC endpoint. | https://rpc.testnet.arc.network |
USDC_ADDRESS |
no | USDC system contract on Arc. | 0x3600…0000 |
FAUCET_AMOUNT_USDC |
no | USDC sent per successful claim. | 0.5 |
ALLOW_UNVERIFIED_CLAIMS |
no | Allow the enter-an-address (unsigned) mode. | true |
CLAIM_COOLDOWN_MS |
no | Per-address cooldown between claims. | 86400000 (24h) |
NONCE_TTL_MS |
no | Challenge validity window. | 300000 (5m) |
CORS_ORIGIN |
no | Allowed browser origin. | http://localhost:5173 |
EXPLORER_BASE_URL |
no | Explorer base URL for tx links. | https://testnet.arcscan.app |
IP_RATE_LIMIT_WINDOW_MS / IP_RATE_LIMIT_MAX |
no | Per-IP rate limiting. | 900000 / 20 |
No Circle API key is required. See packages/faucet-server/.env.example.
An npm-workspaces monorepo; ethers v6 throughout, viem for wallet generation.
| Package | Role |
|---|---|
packages/shared |
Shared faucet API types. |
packages/wallet-core |
Framework-agnostic EIP-1193 / EIP-6963 wallet module + viem wallet generation. |
packages/widget |
The embeddable <arc-faucet> web component. |
packages/faucet-server |
Automated ethers v6 faucet API. |
apps/demo |
Demo page that embeds <arc-faucet>. |
| Field | Value |
|---|---|
| Chain ID | 5042002 (0x4cef52) |
| RPC | https://rpc.testnet.arc.network |
| Explorer | https://testnet.arcscan.app |
| USDC contract | 0x3600000000000000000000000000000000000000 (6 decimals) |
| Native gas token | USDC (18 decimals) |
| Circle faucet | https://faucet.circle.com |
Prerequisites: Node.js ≥ 18.
git clone https://github.com/parsec-wallet/ARCtestnetUI.git
cd ARCtestnetUI
npm install
npm run dev # faucet API + demo, using the committed demo treasuryOpen the demo (e.g. http://localhost:5173) and use any of the three recipient modes.
To actually dispense tokens, the treasury must hold USDC. Fund the demo treasury's address (printed at server startup) once at faucet.circle.com — or run your own:
npm run generate:wallet # viem — prints a fresh address + key
# fund the address at faucet.circle.com, then put the key in
# packages/faucet-server/.env as TREASURY_PRIVATE_KEYnpm run typecheck && npm run build@arc-faucet/wallet-core is framework-agnostic and works against any EIP-1193 provider. The
whole claim is one call, with a Recipient for each mode:
import { runClaimFlow, discoverWallets, generateWallet } from '@arc-faucet/wallet-core';
// connect an existing wallet
const [wallet] = await discoverWallets();
await runClaimFlow({ recipient: { kind: 'connected', provider: wallet.provider },
faucetBaseUrl: 'http://localhost:8787' });
// generate a wallet in-browser (key stays local)
const fresh = generateWallet(); // { address, privateKey }
await runClaimFlow({ recipient: { kind: 'generated', privateKey: fresh.privateKey },
faucetBaseUrl: 'http://localhost:8787' });
// claim to a plain address (unverified)
await runClaimFlow({ recipient: { kind: 'address', address: '0x…' },
faucetBaseUrl: 'http://localhost:8787' });runClaimFlow emits a ClaimStateEvent for every transition via onState.
Base URL defaults to http://localhost:8787.
Body { "address": "0x…" } → ChallengeResponse { nonce, message, issuedAt, expiresAt }.
Body { "address", "nonce", "signature?" }. With a signature, the server recovers the signer
and requires it to equal address (verified: true). Without one, the claim is accepted only
if ALLOW_UNVERIFIED_CLAIMS is set (verified: false). Returns
ClaimResponse { status, txHash, explorerUrl, amount, recipient, verified }.
Treasury address + USDC balance — an operator readiness check.
{ error, message } with one of: BAD_REQUEST, INVALID_SIGNATURE, ADDRESS_MISMATCH,
NONCE_UNKNOWN, NONCE_EXPIRED, NONCE_USED, RATE_LIMITED, TREASURY_INSUFFICIENT,
RPC_ERROR.
- Non-custodial. A participant's private key is never compromised: a generated key is created only in the browser, never transmitted, logged, or persisted. The faucet server only ever sees addresses and signatures. See SECURITY.md.
- The only private key in the repo is
treasury.demo.json— a deliberate, labelled public throwaway. A productionTREASURY_PRIVATE_KEYlives only in.env(git-ignored). - Challenge nonces are single-use with a TTL (replay protection); per-address cooldown + per-IP rate limiting guard the treasury; the treasury balance is checked before broadcast.
See CONTRIBUTING.md. Faucet policy and data handling: POLICY.md.
MIT © Parsec Wallet