Skip to content

Faucet API endpoints for devnet test tokens (DEMO, PROMPT, USDCX) #350

Description

@sadiq1971

Summary

Add faucet endpoints to the middleware so a public faucet web app can dispense the devnet test tokens (DEMO, PROMPT, USDCX) to any registered address — no wallet connection, rate-limited server-side.

UI issue (approved design + frontend scope): ChainSafe/canton-snap#96

The middleware is the natural home: it already has ledger access, the EVM→party mapping, and the drip logic in scripts/utils/fund-wallet.go — this feature essentially productizes fund-wallet.go behind rate-limited endpoints, executed from a dedicated funded faucet party with server-side signing (like the custodial transfer path).

All endpoints follow existing conventions: snake_case JSON, { "error": "..." } error bodies, { "items": [...] } list envelopes.

Endpoints

1. GET /api/v2/faucet/tokens

Faucet config for the UI token selector (amounts/cooldowns not hardcoded client-side).

{ "items": [
  { "symbol": "DEMO",  "name": "Demo Token",     "drip_amount": "100",
    "cooldown_seconds": 3600, "kind": "direct", "enabled": true },
  { "symbol": "PROMPT", "name": "Prompt Token",  "drip_amount": "50",
    "cooldown_seconds": 3600, "kind": "direct", "enabled": true },
  { "symbol": "USDCX", "name": "USDC on Canton", "drip_amount": "250",
    "cooldown_seconds": 3600, "kind": "offer",  "enabled": true }
] }

kind reflects the existing offer-based distinction (OFFER_BASED_TOKENS) — the UI uses it to explain that USDCX arrives as a transfer offer to accept in the dapp. Drip amounts / cooldowns above are placeholders, final values TBD.

2. POST /api/v2/faucet/drip

The main action. Public / unauthenticated (no wallet connection by design), so all abuse control is server-side.

// request
{ "address": "0x4f8AC1b02fA9DcB4f21e83Ec5700A3B2E1D09a21", "token": "DEMO", "captcha_token": "..." }

// 200 (direct token)
{ "tx_id": "1220ab...", "kind": "direct", "amount": "100",
  "token": "DEMO", "next_available_at": "2026-07-10T12:00:00Z" }

// 200 (offer-based token)
{ "contract_id": "00ab...", "kind": "offer", "amount": "250", "token": "USDCX",
  "expires_at": "2026-07-11T12:00:00Z", "next_available_at": "2026-07-10T12:00:00Z" }

Server-side flow:

  1. Validate address + token; verify captcha if enabled.
  2. Resolve the EVM address to its Canton party (same lookup as GET /profile) — recipients must already be registered.
  3. Check cooldown (per address + token) and IP-level limits.
  4. Execute the transfer from the faucet party (server-side signing). Direct for DEMO/PROMPT; for USDCX create a transfer offer with a validity_seconds (e.g. the existing 86400 default).

Error responses (distinct so the UI can render specific states):

Status Meaning Body
400 Malformed address / unknown or disabled token / bad captcha { "error": "..." }
404 Address not registered as a Canton party { "error": "address not registered" }
429 Cooldown active { "error": "cooldown active", "retry_after_seconds": 1740 }
503 Faucet reserves depleted for that token { "error": "faucet empty" }

3. GET /api/v2/faucet/status?address=0x...

Per-token readiness so the UI can show "ready now" vs. a countdown (and disable the button) before submitting.

{ "items": [
  { "token": "DEMO",   "available": true },
  { "token": "PROMPT", "available": true },
  { "token": "USDCX",  "available": false, "retry_after_seconds": 1740 }
] }

4. GET /api/v2/faucet/drips/recent?limit=10

Powers the public "Recent drips" feed. Unauthenticated — truncate addresses server-side (0x91bD…44eC), same as the public transfer-list endpoints truncate party IDs.

{ "items": [
  { "address": "0x91bD…44eC", "token": "USDCX", "amount": "250",
    "kind": "offer", "created_at": "2026-07-10T11:58:03Z" }
] }

5. Health

Reuse the existing GET /health (nothing new needed if faucet routes live in the middleware).

New infrastructure required

  • Faucet party: a dedicated, funded Canton party whose key the middleware holds; reserves monitored/topped-up out-of-band.
  • Cooldown store: per (address, token) last-drip timestamps (PostgreSQL is already in the stack).
  • Rate limiting: net-new — nothing exists today (current abuse control is the manual registration whitelist). Needs per-address+token cooldown plus an IP-level limit; optional captcha (e.g. Cloudflare Turnstile) behind a config flag, hence captcha_token.
  • Config: per-token drip amount / cooldown / enabled flag; devnet-only guard so these routes never ship enabled on mainnet.

Out of scope

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type: FeatureAdded to issues and PRs to identify that the change is a new feature.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions