Discord bot that proves a user controls a Bitcoin address — and lists any Ordinal inscriptions held there — using BIP-322 signed messages.
- User runs
/verify address:<btc_addr>. The bot returns a single-use, 5-minute challenge string bound to the user's Discord ID, the supplied address, and a fresh random nonce. - User signs the challenge in their wallet (Sparrow / Leather / Xverse / Unisat / any BIP-322-capable wallet).
- User runs
/submit signature:<base64>. The bot:- Verifies the signature with the Rust-backed
bip322library (verify_simple_encoded). - Records the
discord_id ↔ btc_addressbinding in SQLite. - Looks up inscriptions held by the address via the Ordiscan API.
- Evaluates holder-rules (see below) and grants matching roles.
- Shows a link to a mobile-friendly signing page (Sats Connect) if configured.
- Verifies the signature with the Rust-backed
All bot replies are ephemeral, so addresses and signatures are never visible to other channel members.
- Migrated from deprecated Hiro Ordinals API to Ordiscan API
- Free-tier-friendly with no rate limits for basic usage
- Grant Discord roles automatically based on which inscriptions a user holds
- Three match types:
inscription: Specific inscription IDparent: Covers entire collection (children of a parent inscription)meta_collection: Ordinals Wallet slug for pre-Sept-2023 OG collections
- New slash commands:
/rule_add type:<inscription|parent|collection|meta_collection> value:<id|slug> role:@role/rule_remove rule_id:<n>/rule_list/rule_help
- Added Sats Connect integration
- When
SIGN_PAGE_URLis configured,/verifyshows a button linking to a standalone signing page with challenge pre-filled - Opens user's wallet app directly for seamless mobile verification
- Added
/sync_me— any verified user can re-sync their own roles - Added
/sync_roles— admins can sync one member or all verified members - Keeps roles in sync with current holdings after buys/sells
- Safe by design:
- Only strips roles granted by holder-rules or the verified role
- Skips members on API errors (never strips on transient failures)
- Respects bot's role hierarchy (won't strip roles above bot's top role)
- Idempotent — running twice does nothing if holdings unchanged
- Added
PRIVACY.md— Privacy Policy - Added
TERMS.md— Terms of Service
- Added extensive test suite:
tests/bip322_sign.py— BIP-322 signature generation for teststests/test_bot_logic.py— Core bot logic teststests/test_inscriptions.py— Ordiscan API integration teststests/test_role_rules.py— Holder-rule system teststests/test_verifier.py— BIP-322 signature verification tests
| Command | What it does |
|---|---|
/verify address:<btc_addr> |
Issue a fresh signing challenge. |
/submit signature:<base64> |
Submit the BIP-322 signature for the active challenge. |
/whoami |
List the addresses you've verified. |
/rule_add |
Bind a Discord role to ownership of an inscription, parent, or collection. |
/rule_remove |
Delete a holder-rule by its rule_id. |
/rule_list |
Show all holder-rules configured for this server. |
/rule_help |
Guide to setting up holder-rules. |
/sync_me |
Re-sync your roles based on current holdings. |
/sync_roles [@member] |
Re-sync roles for one member or all verified members (admin only). |
git clone https://github.com/novaoc/ordinal-verify-bot.git
cd ordinal-verify-bot
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env # then edit .env
python ordinal_verify_bot.py- Create an application + bot at https://discord.com/developers/applications.
- Copy the bot token into
DISCORD_TOKENin.env. - Invite the bot with the
applications.commandsscope (andbotscope withManage Rolesonly if you wantVERIFIED_ROLE_IDto work).
DISCORD_TOKEN— Bot token from Discord developer portal.GUILD_ID— Register slash commands to one guild for instant updates. Leave unset for global commands (propagation can take up to ~1 hour).VERIFIED_ROLE_ID— Role granted on successful verification. The bot's top role must sit above this role.ORDISCAN_API— Ordiscan API endpoint (default:https://api.ordiscan.com).ORDISCAN_API_KEY— API key (required — without one the bot still verifies ownership but always reports 0 inscriptions). Get a free key at https://ordiscan.com/docs/api/login.DB_PATH— SQLite database path (default:verifications.db).SIGN_PAGE_URL— Optional URL of the static "Sign with wallet" page. When set,/verifyshows a link button that opens this page with the challenge pre-filled, so mobile users don't have to copy the message into their wallet manually. Leave blank to omit the button.
- The challenge message binds
discord_id,btc_address, and a 128-bit random nonce, so signatures cannot be replayed across users or addresses. - Nonces are single-use and expire after 5 minutes; pending challenges are held in memory only and cleared on bot restart.
- Slash command responses use
ephemeral=Trueso signatures aren't broadcast. - The bot uses
discord.Intents.default()only — no privileged intents.
MIT
- Privacy Policy:
PRIVACY.md - Terms of Service:
TERMS.md