Privacy-preserving age verification for Discord servers using Self Protocol's zero-knowledge proof technology.
This project provides a Discord bot that verifies users are 18+ years old using Self Protocol without exposing their actual age or identity. The bot uses cryptographic attestations and zero-knowledge proofs to confirm minimum age requirements and OFAC compliance, then automatically grants access to age-restricted channels.
- Zero-knowledge age verification: Confirm users are 18+ without revealing their actual birthdate
- Privacy-preserving: No personal information or identity data is exposed or stored
- Automated role assignment: Users who verify automatically receive the configured role
- OFAC compliance: Built-in sanctions list checking
- No database required: All state managed in-memory and Discord roles
- Simple UX: Single
/verifycommand triggers the entire flow - Comprehensive logging: JSON-formatted logs for monitoring and debugging
User runs /verify → Bot generates QR code → User scans with Self app
→ Self app creates zero-knowledge proof → Backend verifies proof
→ Bot assigns role → User gains access to restricted channels
The verification process:
- User runs
/verifyin Discord - Bot generates a unique verification QR code and DMs it to the user
- User scans the QR with the Self.xyz mobile app
- Self app generates a zero-knowledge proof confirming user is 18+ and OFAC compliant
- Backend validates the proof without learning the user's actual age or birthdate
- Bot automatically assigns the verified role
- User can now access age-restricted channels
Express Backend (index.mjs)
- Handles webhook requests from Self mobile app
- Verifies zero-knowledge proofs using
@selfxyz/core - Validates minimum age (18+) and OFAC compliance
- Exposes
POST /api/verifyendpoint
Discord Bot (discordBot.mjs)
- Registers and handles
/verifyslash command - Generates Self verification QR codes
- Manages pending verification sessions (in-memory)
- Assigns roles on successful verification
- Sends DMs with QR codes and status updates
Self Verifier (selfVerifier.mjs)
- Configures
SelfBackendVerifierin offchain mode - Decodes user-defined data from proofs
- Validates cryptographic attestations
- Backend: Node.js, Express.js v5
- Discord: Discord.js v14 with slash commands
- Self Protocol: @selfxyz/core (verifier), @selfxyz/common (QR generation)
- Utilities: ethers.js (hex encoding), qrcode (PNG generation)
┌─────────────┐ /verify ┌──────────────┐
│ Discord │ ───────────────────>│ Discord Bot │
│ User │ └──────────────┘
└─────────────┘ │
│ │ Generate QR + sessionId
│ v
│ ┌──────────────────┐
│<─────────────────────────│ QR Code (DM) │
│ └──────────────────┘
│
│ Scan QR
v
┌──────────────┐
│ Self.xyz │
│ Mobile App │
└──────────────┘
│
│ Generate proof
v
┌──────────────────┐ POST ┌──────────────────┐
│ Express Server │ <────────────│ Self Protocol │
│ /api/verify │ └──────────────────┘
└──────────────────┘
│
│ Verify proof + decode sessionId
v
┌──────────────────┐
│ Discord Bot │
│ Assign Role │
└──────────────────┘
│
v
┌──────────────────┐
│ User gets role │
│ & success DM │
└──────────────────┘
- In-memory: Pending verification sessions (lost on restart)
- File system: QR codes (
server/qrcodes/) and logs (server/logs/) - No database: All persistent state is stored in Discord roles
- Node.js 18+ (LTS recommended)
- npm (included with Node.js)
- Discord account with:
- Server (guild) admin permissions
- Ability to create Discord applications
- ngrok account (free tier works) for HTTPS tunnel
- Alternative: Any public HTTPS URL (Railway, Heroku, etc.)
- Self.xyz mobile app for testing (download from App Store/Google Play)
# Clone the repository
git clone <your-repo-url>
cd self-discord-verification
# Install server dependencies
cd server
npm installFor local development, you need a public HTTPS URL. ngrok provides this for free.
# Install ngrok
# macOS
brew install ngrok
# Or download from https://ngrok.com/download
# Authenticate with your ngrok token
ngrok config add-authtoken <YOUR_NGROK_TOKEN>
# Start the tunnel (after server is running)
ngrok http 3001Note: For production deployment, use a real HTTPS URL from your hosting provider.
- Go to Discord Developer Portal
- Click New Application, name it (e.g., "Self Verifier"), click Create
- Navigate to the Bot tab:
- Click Add Bot → Yes, do it!
- Click Reset Token and copy the token → this is
DISCORD_BOT_TOKEN - Enable Privileged Gateway Intents:
- ✅ SERVER MEMBERS INTENT
- ✅ MESSAGE CONTENT INTENT
- Click Save Changes
- Go to General Information tab:
- Copy Application ID → this is
DISCORD_CLIENT_ID
- Copy Application ID → this is
- In Developer Portal, go to OAuth2 → URL Generator
- Select Scopes:
- ✅
bot - ✅
applications.commands
- ✅
- Select Bot Permissions:
- ✅ View Channels
- ✅ Send Messages
- ✅ Send Messages in Threads
- ✅ Read Message History
- ✅ Manage Roles (required)
- Copy the generated URL, open in browser
- Select your server and authorize
- In Discord: Settings → Advanced → Enable Developer Mode
- Right-click your server icon → Copy Server ID → this is
DISCORD_GUILD_ID
- Server Settings → Roles → Create Role
- Name it "Self.xyz Verified Role" (or similar)
- Right-click the role → Copy Role ID → this is
DISCORD_VERIFIED_ROLE_ID - IMPORTANT: Drag your bot's role above this verified role in the role list
- Create a category (e.g., "18+ Verified")
- Add channels inside it
- Right-click category → Edit Category → Permissions:
- @everyone: ❌ View Channel
- Your verified role: ✅ View Channel
- Save changes
Create server/.env:
# Copy the sample file
cd server
cp ../.sample.env .envEdit .env with your values:
# Server Configuration
PORT=3001
# Self Protocol Configuration
SELF_ENDPOINT=https://your-ngrok-id.ngrok-free.app/api/verify
# Discord Bot Configuration
DISCORD_BOT_TOKEN=your-bot-token-from-step-3
DISCORD_CLIENT_ID=your-application-id-from-step-3
DISCORD_GUILD_ID=your-server-id-from-step-5
DISCORD_VERIFIED_ROLE_ID=your-role-id-from-step-5
# Optional Branding
SELF_APP_NAME=Self Discord Verification
SELF_LOGO_URL=https://i.postimg.cc/mrmVf9hm/self.png| Variable | Required | Description |
|---|---|---|
PORT |
No | Express server port (default: 8080) |
SELF_ENDPOINT |
Yes | Public HTTPS URL + /api/verify (no localhost!) |
DISCORD_BOT_TOKEN |
Yes | Bot token from Developer Portal |
DISCORD_CLIENT_ID |
Yes | Application ID from Developer Portal |
DISCORD_GUILD_ID |
Yes | Your Discord server ID |
DISCORD_VERIFIED_ROLE_ID |
Yes | Role to assign on verification |
SELF_APP_NAME |
No | Name shown in Self app |
SELF_LOGO_URL |
No | Logo shown in Self app |
Critical: SELF_ENDPOINT must be:
- HTTPS (not HTTP)
- Publicly accessible (not localhost/127.0.0.1)
- Include the full path:
https://your-domain.com/api/verify
-
Start the server:
cd server npm run dev -
Start ngrok (in a separate terminal):
ngrok http 3001
-
Update
.envwith the ngrok URL (if it changed):SELF_ENDPOINT=https://abc123.ngrok-free.app/api/verify
-
Restart the server if you changed
.env
cd server
npm startUse your production HTTPS URL in SELF_ENDPOINT.
- User runs
/verifyin any Discord channel - Bot replies: "Generating your Self verification QR… I'll DM it to you shortly."
- Bot sends QR code via DM
- User scans QR with Self.xyz mobile app
- User completes verification in the app
- Bot automatically assigns the verified role
- Bot sends success DM: "✅ Your Self verification succeeded. [Add your custom message here]"
- User can now see restricted channels
To verify everything works:
# Check server is running
curl http://localhost:3001
# Should return: {"status":"ok","message":"Self Express Backend + Discord verifier bot (offchain)"}
# Check Discord bot is online
# Look in Discord - bot should show as online
# Check logs
tail -f server/logs/discord-verifier.log
# Should see: {"event":"discord.ready",...}Webhook endpoint called by Self mobile app after user completes verification.
Request:
{
"attestationId": "string",
"proof": {
"pi_a": [...],
"pi_b": [...],
"pi_c": [...],
"protocol": "groth16"
},
"publicSignals": [...],
"userContextData": {...}
}Response (Success):
{
"status": "success",
"result": true,
"credentialSubject": {...},
"userData": {
"userDefinedData": "hex-encoded-metadata"
}
}Response (Failure):
{
"status": "error",
"result": false,
"reason": "Minimum age verification failed",
"details": {
"isValid": false,
"isMinimumAgeValid": false,
"isOfacValid": true
}
}Validation Rules:
- ✅ Proof must be cryptographically valid
- ✅ User must be 18+ years old
- ✅ User must NOT be on OFAC sanctions list
Health check endpoint.
Response:
{
"status": "ok",
"message": "Self Express Backend + Discord verifier bot (offchain)",
"verifyEndpoint": "/api/verify",
"endpoint": "https://your-domain.com/api/verify"
}All events are logged to server/logs/discord-verifier.log in JSON Lines format.
| Event | Description |
|---|---|
discord.ready |
Bot connected and ready |
discord.commands_registered |
Slash commands registered |
verification.started |
User initiated /verify |
verification.succeeded |
Proof validated successfully |
verification.failed |
Proof validation failed |
verification.role_assigned |
Role assigned to user |
qr.created |
QR code generated |
# Tail logs in real-time
tail -f server/logs/discord-verifier.log
# Parse JSON logs (requires jq)
cat server/logs/discord-verifier.log | jq .
# Filter by event type
cat server/logs/discord-verifier.log | jq 'select(.event == "verification.succeeded")'Symptoms: Slash command not appearing in Discord
Solutions:
- ✅ Check logs for
discord.commands_registeredevent - ✅ Verify
DISCORD_CLIENT_IDandDISCORD_GUILD_IDare correct - ✅ Ensure bot has
applications.commandsscope - ✅ Restart the server after changing
.env - ✅ Wait up to 1 hour (Discord cache) or kick/re-invite bot
Symptoms: "I'll DM it to you shortly" but no DM received
Solutions:
- ✅ Enable DMs: Discord Settings → Privacy & Safety → Allow DMs from server members
- ✅ Or: Right-click server → Privacy Settings → Allow direct messages
- ✅ Check logs for
verification.dm_error
Symptoms: Verification succeeds but user doesn't get role
Solutions:
- ✅ Check
server/logs/discord-verifier.logfor errors:verification.role_not_found: WrongDISCORD_VERIFIED_ROLE_IDor role deletedverification.discord_error: Bot lacks permissions
- ✅ Ensure bot has
Manage Rolespermission - ✅ CRITICAL: Bot's role must be above the verified role in Server Settings → Roles
- ✅ Verify
DISCORD_VERIFIED_ROLE_IDis correct (right-click role → Copy Role ID)
Symptoms: QR scans but Self app reports endpoint error
Solutions:
- ✅ Verify
SELF_ENDPOINTis HTTPS (not HTTP) - ✅ Ensure
SELF_ENDPOINTdoes NOT containlocalhostor127.0.0.1 - ✅ Check ngrok is running:
ngrok http 3001 - ✅ Confirm ngrok URL matches
SELF_ENDPOINTin.env - ✅ Test endpoint:
curl https://your-ngrok-url.ngrok-free.app/ - ✅ Restart server after changing
.env
Symptoms: Pending verifications don't work after server restart
Cause: Pending verifications are stored in-memory only
Solutions:
- ✅ Users must run
/verifyagain after server restarts - ✅ Consider implementing persistent storage for production
Symptoms: Bot appears offline, commands don't work
Solutions:
- ✅ Check server is running:
npm run dev - ✅ Check logs for
discord.readyevent - ✅ Verify
DISCORD_BOT_TOKENis correct (may need to reset token) - ✅ Check privileged intents are enabled in Developer Portal
Symptoms: Discord shows "The application did not respond"
Solutions:
- ✅ Check server logs for errors
- ✅ Ensure server is running and responding
- ✅ Verify ngrok tunnel is active (dev) or server is accessible (prod)
- ✅ Check network/firewall settings
This project includes Railway configuration (railway.toml).
-
Create Railway project:
# Install Railway CLI npm i -g @railway/cli # Login and deploy railway login railway init railway up
-
Configure environment variables in Railway dashboard:
- Set all variables from
.env(exceptPORT) - Update
SELF_ENDPOINTto your Railway URL +/api/verify
- Set all variables from
-
Deploy:
git push # Railway auto-deploys on push
The project works on any Node.js hosting platform:
- Heroku: Add
Procfilewithweb: npm start - Vercel: Configure
vercel.jsonfor serverless functions - DigitalOcean App Platform: Set build command to
npm install, run command tonpm start - AWS EC2: Use PM2 for process management
Requirements:
- Node.js 18+ runtime
- Public HTTPS URL
- Working directory:
server/ - Start command:
npm start
- ✅ Zero-knowledge proofs: User's actual age/identity never disclosed
- ✅ Cryptographic attestations: Strong identity guarantees
- ✅ OFAC compliance: Sanctions list checking
- ✅ Session-based: Unique sessionId prevents replay attacks
- ✅ HTTPS required: All communication encrypted
⚠️ In-memory state: Server restart clears pending verifications⚠️ No QR cleanup: QR codes accumulate on disk⚠️ No session expiry: Pending verifications never expire⚠️ No rate limiting: Users can spam/verifycommand⚠️ DM dependency: Users must allow DMs from server
- Add rate limiting: Prevent
/verifyspam - Implement session expiry: Clean up old pending verifications
- Add QR cleanup: Delete QR files after verification completes
- Use persistent storage: Redis/database for pending verifications
- Add monitoring: Sentry, Datadog, or similar
- Implement webhook authentication: Verify requests to
/api/verifycome from Self
self-discord-verification/
├── server/
│ ├── src/
│ │ ├── config.mjs # Environment configuration
│ │ ├── logger.mjs # JSON logging utility
│ │ ├── selfVerifier.mjs # Self Protocol verifier
│ │ └── discordBot.mjs # Discord bot logic
│ ├── index.mjs # Express server entry point
│ ├── package.json # Dependencies
│ ├── logs/ # Runtime logs (gitignored)
│ │ └── discord-verifier.log
│ └── qrcodes/ # Generated QR codes (gitignored)
│ └── self-qr-*.png
├── .sample.env # Environment template
├── railway.toml # Railway deployment config
├── prettier.config.cjs # Code formatting
├── banner.png # README banner
└── README.md # This file
Contributions are welcome! Please:
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit changes:
git commit -m 'Add amazing feature' - Push to branch:
git push origin feature/amazing-feature - Open a Pull Request
- Use Prettier for code formatting:
npm run format - Test with ngrok before submitting
- Update README for new features
- Add logging for important events
[Add your license here]
- Self Protocol: https://self.xyz
- Self Docs: https://docs.self.xyz
- Discord.js Guide: https://discord.js.org
- Built with Self Protocol for zero-knowledge identity verification
- Uses Discord.js for Discord bot functionality
- Inspired by the need for privacy-preserving age verification
Note: This project uses Self Protocol's offchain verification mode. No blockchain transactions or gas fees are required.
