This guide covers everything you need to integrate Base Verify into your web app.
Base Verify is for web app builders to allow their users to prove they have verified accounts (X Blue, Coinbase One) without sharing credentials.
Why This Matters:
Even if a wallet has few transactions, Base Verify reveals if the user is high-value through their verified social accounts (X Blue, Instagram, TikTok) or Coinbase One subscription. This lets you identify quality users regardless of on-chain activity.
Example Use Cases:
- Token-gated airdrops or daily rewards
- Exclusive content access (e.g. creator coins)
- Identity-based rewards and loyalty programs
┌─────────────┐
│ │ 1. User connects wallet
│ Your │
│ Mini App │
│ (Frontend) │
└──────┬──────┘
│
│ 2. App generates SIWE message (frontend)
│ • Includes wallet address
│ • Includes provider (x, coinbase, instagram, tiktok)
│ • Includes traits (verified:true, followers:gt:1000)
│ • Includes action (your custom action, e.g. claim_airdrop)
│
│ 3. User signs SIWE message with wallet
│
│ 4. Send signature + message to YOUR backend
│
▼
┌──────────────┐
│ Mini App │ • Validates trait requirements
│ Backend │ • Verifies signature with Base Verify API
│ (Your API) │
└──────┬───────┘
│
▼
200 OK ←───────┌──────────────────┐───────→ 400
Verified! │ │ User has account
(DONE) │ Base Verify API │ However, traits not met
│ verify.base.dev │ (DONE)
└────────┬─────────┘
│
│ 404 Not Found
▼
5. Redirect to Base Verify Mini App
│
▼
┌──────────────────────┐
│ Base Verify │ 6. User completes OAuth
│ Mini App │ (X, Coinbase, Instagram, TikTok)
│ verify.base.dev │ 7. Base Verify stores verification
└──────────┬───────────┘
│
│ 8. Redirects back to your app
▼
┌─────────────┐
│ Your │ 9. Check again (step 4)
│ Mini App │ → Now returns 200 ✅ or 400
└─────────────┘ Your App's Responsibilities:
- Generate SIWE messages with trait requirements
- Handle user wallet connection
- Redirect to Base Verify Mini App when verification not found
- Store the returned verification token to prevent reuse
- Keep your secret key secure on the backend
Base Verify's Responsibilities:
- Validate SIWE signatures
- Store provider verifications (X, Coinbase, Instagram, TikTok)
- Check if verification meets trait requirements
- Facilitate OAuth flow with providers
- Return deterministic tokens for Sybil resistance
- 200 OK: Wallet has verified the provider account AND meets all trait requirements. Returns a unique token.
- 404 Not Found: Wallet has never verified this provider. Redirect user to Base Verify Mini App.
- 400 Bad Request (with message
"verification_traits_not_satisfied"): Wallet has verified the provider, but doesn't meet the trait requirements (e.g., has X account but not enough followers).
Need the full rationale for SIWE and the exact message template? See the Security section for the complete explanation.
See Core Concepts for the full glossary, examples, and storage guidance.
- API Key - Fill out the interest form
- Wallet integration - Users must connect and sign messages
- Backend server - To securely call Base Verify API
Contact the Base Verify team for your Secret Key.
Security: Never expose your secret key in frontend code or version control.
Provide the Base Verify team:
- Mini App Domain
- Redirect URI - Where users return after verification (e.g.,
https://yourapp.com)
Security Warning: Your secret key must NEVER be exposed in frontend code. All Base Verify API calls must go through your backend.
Create lib/config.ts:
export const config = {
appUrl: 'https://your-app.com',
baseVerifySecretKey: process.env.BASE_VERIFY_SECRET_KEY, // Backend only!
baseVerifyApiUrl: 'https://verify.base.dev/v1',
baseVerifyWebAppUrl: 'https://verify.base.dev',
}Add to .env.local:
BASE_VERIFY_SECRET_KEY=your_secret_key_hereCreate lib/signature-generator.ts:
import { SiweMessage, generateNonce } from 'siwe'
import { config } from './config'
export async function generateSignature(
signMessageFunction: (message: string) => Promise<string>,
address: string
) {
// Build resources array for SIWE
const resources = [
'urn:verify:provider:x',
'urn:verify:provider:x:verified:eq:true',
'urn:verify:provider:x:followers:gte:100',
'urn:verify:action:claim_airdrop' // Your custom action name
]
// Create SIWE message
const siweMessage = new SiweMessage({
domain: new URL(config.appUrl).hostname,
address,
statement: 'Verify your X account',
uri: config.appUrl,
version: '1',
chainId: 8453, // Base
nonce: generateNonce(),
issuedAt: new Date().toISOString(),
expirationTime: new Date(Date.now() + 6 * 60 * 60 * 1000).toISOString(),
resources,
})
const message = siweMessage.prepareMessage()
const signature = await signMessageFunction(message)
return { message, signature, address }
}Frontend generates signature, sends to YOUR backend, backend calls Base Verify API.
Frontend:
async function checkVerification(address: string) {
// Generate SIWE signature
const signature = await generateSignature(
async (msg) => {
return new Promise((resolve, reject) => {
signMessage(
{ message: msg },
{ onSuccess: resolve, onError: reject }
)
})
},
address
)
// Send to YOUR backend (not directly to Base Verify)
const response = await fetch('/api/check-verification', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
signature: signature.signature,
message: signature.message,
address: address
})
})
const data = await response.json();
return data; // Your backend returns the result
}Backend code (YOUR API endpoint):
// pages/api/check-verification.ts
import { validateTraits } from '../../lib/trait-validator';
export default async function handler(req, res) {
const { signature, message, address } = req.body;
// CRITICAL: Validate trait requirements match what YOUR backend expects
// This prevents users from modifying trait requirements on the frontend
const expectedTraits = {
'verified': 'true',
'followers': 'gte:100'
};
const validation = validateTraits(message, 'x', expectedTraits);
if (!validation.valid) {
return res.status(400).json({
error: 'Invalid trait requirements in message',
details: validation.error
});
}
// Now safe to verify signature with Base Verify API
const response = await fetch('https://verify.base.dev/v1/base_verify_token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.BASE_VERIFY_SECRET_KEY}`, // Secret key stays on backend
},
body: JSON.stringify({
signature: signature,
message: message,
})
});
if (response.ok) {
const data = await response.json();
return res.status(200).json({ verified: true, token: data.token });
} else if (response.status === 404) {
return res.status(404).json({ verified: false, needsVerification: true });
} else if (response.status === 400) {
const data = await response.json();
if (data.message === 'verification_traits_not_satisfied') {
return res.status(400).json({ verified: false, traitsNotMet: true });
}
}
return res.status(500).json({ error: 'Verification check failed' });
}If you get 404, redirect user to complete OAuth:
function redirectToVerifyWebApp(provider: string) {
const params = new URLSearchParams({
redirect_uri: config.appUrl,
providers: provider,
})
const webAppUrl = `${config.baseVerifyWebAppUrl}?${params.toString()}`
window.location.href = webAppUrl
}User returns with ?success=true. Check again (step 3) → now returns 200 with token.
404 → User hasn't verified. Redirect to Base Verify web app.
400 (with verification_traits_not_satisfied) → User has account but doesn't meet traits. Don't redirect.
200 → Success! Store the token.
- Learn about available traits for each provider → Trait Catalog
- See complete API documentation → API Reference
- Understand security model → Security Overview