Why
Public HTTP faucets get abused. The current POST /v1/drip endpoint rate-limits per-address, but an attacker rotates addresses trivially: any cloud provider, any VPN, any throwaway keypair. Per-IP would hurt good actors behind shared NATs (corporate networks, university Wi-Fi) without slowing real abusers.
Discord-based faucets sidestep this entirely. The pattern across many testnets:
- User joins our Discord server
- User runs
/drip lig1... in a faucet channel
- Bot validates: account meets minimum-age threshold, hasn't dripped in 24h, address is shaped correctly
- Bot hits
POST /v1/drip against the api with a privileged token
- Bot replies with the tx hash + a link to
explorer.ligate.io/tx/{hash}
Anti-abuse layers stack: Discord's own anti-spam (CAPTCHA on join, account-age checks, role gating), plus per-Discord-user cooldown enforced by the bot, plus the api's per-address cooldown as backstop. Sybil-resistance becomes "spin up N legitimate-looking Discord accounts and wait Y days each" instead of "rotate keypairs in a loop".
Scope (v0 of the bot)
- Discord slash command
/drip <address> — only callable in a designated faucet channel, by users with account_age >= 7 days and member_of_server >= 1 day.
- Per-user cooldown — 24h, enforced via Discord-user-id keyed table. Persists in the same Postgres the api already uses (new table
discord_drip_history).
- Privileged drip path — bot calls
POST /v1/drip with a Bearer token that bypasses the api's per-address cooldown (since Discord-user-id is the new rate-limit dimension). Token is a Discord-bot secret separate from the user-facing api.
- Audit log — every drip emits a tracing event with
discord_user_id, target_address, tx_hash, at. Goes to the same JSON logs the api emits; ops can grep for abuse patterns.
- Failure modes — bot DMs the user with a clear "address rejected because X" / "you've dripped recently, try in N hours" / "address-only drips, paste the lig1..." rather than failing silently.
Architecture options
The bot is a small Rust process that connects to Discord via the gateway (serenity or twilight crate) and calls the api over loopback (deployed in the same Railway project) or HTTPS (if deployed elsewhere). Two options:
A. New crate in this repo (crates/discord-bot/)
- Pro: shares the workspace's existing chain types + drip primitives + Postgres pool.
- Pro: single Railway project, single Postgres, single deploy.
- Con: Discord gateway runs forever; bot's failure modes become api-pod-restart triggers if not isolated.
B. New repo ligate-io/ligate-discord-bot
- Pro: clean separation. Bot is a Discord-side concern; api is a chain-side concern.
- Pro: independent deploy lifecycle.
- Con: more repos to maintain. Same code-drift / context-switching tax we just folded the faucet to avoid.
Lean toward A unless the bot grows into other Discord features (mod tools, server stats, attestation lookups). Revisit if so.
Privileged-drip endpoint design
The api needs a way for the bot to drip without being subject to the per-address cooldown. Two options:
A. Bearer-auth path on /v1/drip — accept an Authorization: Bearer <token> header. If valid, skip the per-address rate limit; the bot does its own rate-limit-per-Discord-user instead.
B. Separate endpoint POST /internal/drip — same shape, no rate limit, locked to the bot's IP / Bearer token. Not exposed to public CORS.
Lean toward (B) — separate endpoint keeps the public /v1/drip semantics simple (what you see is what you get; rate limit always applies). Internal endpoint is invisible to humans.
Out of scope for v0 of the bot
- Mod tools (kick, ban) — wrong concern; community can use stock Discord tools.
- Attestation lookup commands (
/attestation lat1...) — frontend's job (explorer.ligate.io).
- Embedded captchas — Discord's join-server captcha is enough for v0.
- Multi-language replies — English only.
- Admin dashboard —
journalctl -fu ligate-discord-bot is enough; build a real dashboard if abuse pressure surfaces real signal.
Pre-reqs (operator-side, when the bot is built)
- Create a Discord application at https://discord.com/developers/applications
- Add a bot user; copy the bot token to
DISCORD_BOT_TOKEN in Railway secrets
- Invite the bot to the official Ligate Discord with
applications.commands + bot scopes
- Create a
#faucet channel; restrict /drip to that channel
- Set role requirements (e.g.,
@verified after Discord captcha pass)
When
Post-devnet. We're shipping the HTTP faucet first because partner integrations (Mneme browser wallet, Themisra demo flows) need programmatic access. The Discord bot is the human-facing alternative, important once devnet starts seeing real community traffic.
Tracking: ~Week 2-4 post-devnet, depending on abuse signal on the HTTP faucet.
Refs
Why
Public HTTP faucets get abused. The current
POST /v1/dripendpoint rate-limits per-address, but an attacker rotates addresses trivially: any cloud provider, any VPN, any throwaway keypair. Per-IP would hurt good actors behind shared NATs (corporate networks, university Wi-Fi) without slowing real abusers.Discord-based faucets sidestep this entirely. The pattern across many testnets:
/drip lig1...in a faucet channelPOST /v1/dripagainst the api with a privileged tokenexplorer.ligate.io/tx/{hash}Anti-abuse layers stack: Discord's own anti-spam (CAPTCHA on join, account-age checks, role gating), plus per-Discord-user cooldown enforced by the bot, plus the api's per-address cooldown as backstop. Sybil-resistance becomes "spin up N legitimate-looking Discord accounts and wait Y days each" instead of "rotate keypairs in a loop".
Scope (v0 of the bot)
/drip <address>— only callable in a designated faucet channel, by users withaccount_age >= 7 daysandmember_of_server >= 1 day.discord_drip_history).POST /v1/dripwith a Bearer token that bypasses the api's per-address cooldown (since Discord-user-id is the new rate-limit dimension). Token is a Discord-bot secret separate from the user-facing api.discord_user_id,target_address,tx_hash,at. Goes to the same JSON logs the api emits; ops can grep for abuse patterns.Architecture options
The bot is a small Rust process that connects to Discord via the gateway (
serenityortwilightcrate) and calls the api over loopback (deployed in the same Railway project) or HTTPS (if deployed elsewhere). Two options:A. New crate in this repo (
crates/discord-bot/)B. New repo
ligate-io/ligate-discord-botLean toward A unless the bot grows into other Discord features (mod tools, server stats, attestation lookups). Revisit if so.
Privileged-drip endpoint design
The api needs a way for the bot to drip without being subject to the per-address cooldown. Two options:
A. Bearer-auth path on
/v1/drip— accept anAuthorization: Bearer <token>header. If valid, skip the per-address rate limit; the bot does its own rate-limit-per-Discord-user instead.B. Separate endpoint
POST /internal/drip— same shape, no rate limit, locked to the bot's IP / Bearer token. Not exposed to public CORS.Lean toward (B) — separate endpoint keeps the public
/v1/dripsemantics simple (what you see is what you get; rate limit always applies). Internal endpoint is invisible to humans.Out of scope for v0 of the bot
/attestation lat1...) — frontend's job (explorer.ligate.io).journalctl -fu ligate-discord-botis enough; build a real dashboard if abuse pressure surfaces real signal.Pre-reqs (operator-side, when the bot is built)
DISCORD_BOT_TOKENin Railway secretsapplications.commands+botscopes#faucetchannel; restrict/dripto that channel@verifiedafter Discord captcha pass)When
Post-devnet. We're shipping the HTTP faucet first because partner integrations (Mneme browser wallet, Themisra demo flows) need programmatic access. The Discord bot is the human-facing alternative, important once devnet starts seeing real community traffic.
Tracking: ~Week 2-4 post-devnet, depending on abuse signal on the HTTP faucet.
Refs
crates/drip/+ handlers incrates/api/src/handlers.rsligate-io/faucet