# Base Verify — LLM Docs Bundle (llms.txt) # # This file aggregates the Base Verify documentation into a single plaintext artifact # for LLM ingestion and quick offline reading. # # Sources (in order): # - docs/index.md # - docs/core-concepts.md # - docs/integration.md # - docs/traits.md # - docs/api.md # - docs/security.md # # Last generated: 2025-12-16 ================================================================================ SOURCE: docs/index.md ================================================================================ # Base Verify Documentation Base Verify allows users to prove ownership of verified accounts (X, Coinbase, Instagram, TikTok) without sharing credentials. Your app receives a deterministic token for Sybil resistance. **How it works:** 1. Check if wallet has verification → API returns yes/no 2. If no → redirect to Base Verify Mini App for OAuth 3. User verifies → returns to your app 4. Check again → now verified --- ## Support & Feedback **Want to integrate Base Verify?** Fill out the [interest form](https://forms.gle/6L4hWAHkojYcefz27) and we'll reach out with API access. ================================================================================ SOURCE: docs/core-concepts.md ================================================================================ # Core Concepts Base Verify relies on a few shared primitives. Refer back to this glossary whenever you build new flows or explain the system to partners. ## Provider An identity platform that Base Verify integrates with. Currently supports **X (Twitter)**, **Coinbase**, **Instagram**, and **TikTok**. ## Verification Cryptographic proof that a wallet owns an account with a specific provider. ## Trait A specific attribute of the provider account that can be verified. **Examples:** - `verified: true` – X account has blue checkmark - `coinbase_one_active: true` – Active Coinbase One subscription - `followers: gt:1000` – X account has over 1000 followers - `followers_count: gte:5000` – Instagram account with 5000+ followers - `video_count: gte:50` – TikTok account with 50+ videos ## Action A developer-defined string that identifies what the user is doing with their verification. Actions let you issue different tokens for different use cases within the same app. **Examples:** - `claim_daily_reward` – Claiming a daily reward - `join_allowlist` – Joining an exclusive allowlist - `unlock_premium_content` – Accessing gated content - `participate_in_raffle` – Entering a raffle ### How Actions Work Actions are specified in the SIWE message resources: ```typescript resources: [ 'urn:verify:provider:x', 'urn:verify:action:claim_daily_reward' // Your custom action ] ``` The action is returned in the API response: ```json { "token": "abc123...", "action": "claim_daily_reward", "wallet": "0x1234..." } ``` ### Why Actions Matter **Different actions produce different tokens.** This enables multiple independent claims from the same verified account: - User verifies X account with action `claim_airdrop` → Token: `abc123` - Same X account with action `join_allowlist` → Token: `def456` (different!) - Same X account with action `claim_airdrop` again → Token: `abc123` (same as first) **Use Cases:** - **Multiple campaigns**: Run separate airdrops without interference - **Feature gating**: Different tokens for different premium features - **Time-based events**: New action per event (e.g., `raffle_jan_2025`, `raffle_feb_2025`) ### Choosing Action Names Use descriptive, lowercase names with underscores: | Good ✅ | Bad ❌ | |---------|--------| | `claim_genesis_airdrop` | `airdrop` (too generic) | | `unlock_pro_features` | `action1` (meaningless) | | `enter_weekly_raffle` | `base_verify_token` (reserved/confusing) | **Important:** Once you've launched with an action name, don't change it. Changing the action will generate different tokens for the same users, breaking your Sybil resistance. --- ## Token – Sybil Resistance A deterministic identifier tied to the provider account, not the wallet. **This is the key anti-sybil mechanism.** ### How It Works - Wallet A verifies an X account → Base Verify returns `Token: abc123` → You have never seen it, so grant the airdrop. - The same X account tries again with Wallet B → Base Verify returns `Token: abc123` → You have seen it, so block the duplicate claim. Without Base Verify, users could claim multiple times with different wallets. With Base Verify, one verified account = one token = one claim. ### Token Properties - **Deterministic**: The same provider account always produces the same token. - **Unique per provider**: A user's X token is different from their Instagram token. - **Unique per app**: Your app receives different tokens than other apps (privacy). - **Action-specific**: Tokens can vary based on the action in your SIWE message. - **Persistent**: Tokens don't expire or rotate (unless the user deletes their verification). - **Trait-independent**: Tokens stay the same even if traits change (e.g., follower count increases). ### How to Store Tokens ```typescript // In your database { token: "abc123...", // The verification token from Base Verify walletAddress: "0x1234...", // The wallet that claimed (for your records) provider: "x", // Which provider was verified claimedAt: "2024-01-15", // When they claimed } ``` ### Example: Prevent Double Claims ```typescript async function claimAirdrop(verificationToken: string, walletAddress: string) { // Check if this token was already used const existingClaim = await db.findClaimByToken(verificationToken); if (existingClaim) { return { error: "This X account already claimed" }; } // Store the token await db.createClaim({ token: verificationToken, wallet: walletAddress, claimedAt: new Date() }); return { success: true }; } ``` ================================================================================ SOURCE: docs/integration.md ================================================================================ # Integration Guide This guide covers everything you need to integrate Base Verify into your mini app. --- ## What is Base Verify? 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 --- ## Architecture & Flow ### The Complete Flow ```ts ┌─────────────┐ │ │ 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 └─────────────┘ ``` ### The Contract: What Your App Does vs What Base Verify Does **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 ### Response Codes Explained - **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](./security.md#siwe-signature-requirement) section for the complete explanation. ## Core Concepts See [Core Concepts](./core-concepts.md) for the full glossary, examples, and storage guidance. --- ## Getting Started ### Prerequisites 1. **API Key** - Fill out the [interest form](https://forms.gle/6L4hWAHkojYcefz27) 2. **Wallet integration** - Users must connect and sign messages 3. **Backend server** - To securely call Base Verify API ### Get Your API Key Contact the Base Verify team for your **Secret Key**. **Security:** Never expose your secret key in frontend code or version control. ### Register Your App Provide the Base Verify team: 1. **Mini App Domain** 2. **Redirect URI** - Where users return after verification (e.g., `https://yourapp.com`) --- ## Implementation > **Security Warning:** Your secret key must NEVER be exposed in frontend code. All Base Verify API calls must go through your backend. ### Step 1: Configuration Create `lib/config.ts`: ```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`: ```shell BASE_VERIFY_SECRET_KEY=your_secret_key_here ``` ### Step 2: SIWE Signature Generator (Frontend) Create `lib/signature-generator.ts`: ```ts import { SiweMessage, generateNonce } from 'siwe' import { config } from './config' export async function generateSignature( signMessageFunction: (message: string) => Promise, 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 } } ``` ### Step 3: Check Verification (Frontend → Backend) **Frontend** generates signature, sends to **YOUR backend**, backend calls Base Verify API. **Frontend:** ```ts 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):** ```ts // 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' }); } ``` ### Step 4: Redirect to Base Verify (Frontend) If you get 404, redirect user to complete OAuth: ```ts 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. --- ## Error Handling **404** → User hasn't verified. Redirect to Base Verify Mini App. **400** (with `verification_traits_not_satisfied`) → User has account but doesn't meet requirements. Don't redirect. **200** → Success! Store the token. --- ## Next Steps - Learn about available traits for each provider → [Trait Catalog](/docs/traits) - See complete API documentation → [API Reference](/docs/api) - Understand security model → [Security Overview](/docs/security) ================================================================================ SOURCE: docs/traits.md ================================================================================ # Trait Catalog Complete reference for all available traits across providers. --- ## Trait System Reference Traits are specific attributes of a provider account that you can verify. ### Trait Syntax Traits are specified in SIWE message resources using this format: ```typescript urn:verify:provider:{provider}:{trait_name}:{operation}:{value} ``` **Example:** ```typescript urn:verify:provider:x:followers:gte:1000 ``` This checks if an X account has greater than or equal to 1000 followers. ### Operations | Operation | Symbol | Applies To | Description | Example | | :---- | :---- | :---- | :---- | :---- | | Equals | `eq` | All types | Exact match | `verified:eq:true` | | Greater Than | `gt` | Integers | Strictly greater | `followers:gt:1000` | | Greater/Equal | `gte` | Integers | Greater or equal | `followers:gte:1000` | | Less Than | `lt` | Integers | Strictly less | `followers:lt:5000` | | Less/Equal | `lte` | Integers | Less or equal | `followers:lte:5000` | | In (list) | `in` | Strings | Value in comma-separated list | `verified_type:in:blue,government` | ### Type System **Boolean Traits** - Values: `"true"` or `"false"` (as strings) - Only supports `eq` operation - Example: `verified:eq:true` **Integer Traits** - Values: Numbers as strings - Supports: `eq`, `gt`, `gte`, `lt`, `lte` - Example: `followers:gte:1000` **String Traits** - Values: Text strings - Supports: `eq`, `in` - Example: `verified_type:eq:blue` or `verified_type:in:blue,government` ### Combining Traits **Within One Provider (AND logic):** When you specify multiple traits for the same provider, ALL must be satisfied: ```typescript resources: [ 'urn:verify:provider:x', 'urn:verify:provider:x:verified:eq:true', 'urn:verify:provider:x:followers:gte:10000' ] // User must have verified X account AND 10k+ followers ``` **Multiple Providers:** Currently, you can only check one provider per request. To check multiple providers, make separate API calls. ### Code Examples **Using traits in signature generation:** ```typescript // Simple boolean check const signature = await generateSignature({ provider: 'x', traits: { 'verified': 'true' }, action: 'claim_airdrop' // Your custom action name }); // Integer comparison const signature = await generateSignature({ provider: 'instagram', traits: { 'followers_count': 'gte:5000' }, action: 'unlock_premium_content' }); // Multiple traits (AND logic) const signature = await generateSignature({ provider: 'tiktok', traits: { 'follower_count': 'gte:1000', 'likes_count': 'gte:10000', 'video_count': 'gte:50' }, action: 'join_creator_program' }); // String with IN operation const signature = await generateSignature({ provider: 'coinbase', traits: { 'verified_type': 'in:blue,government' }, action: 'claim_regional_bonus' }); ``` ### Common Patterns **Geographic Restrictions:** ```typescript // Europe only traits: { 'coinbase_one_billed': 'true' } ``` **Tiered Access:** ```typescript // Bronze tier: any verified account traits: { 'verified': 'true' } // Silver tier: 1k+ followers traits: { 'followers': 'gte:1000' } // Gold tier: 10k+ followers traits: { 'followers': 'gte:10000' } ``` **Multiple Requirements:** ```typescript // Active TikTok creator traits: { 'follower_count': 'gte:5000', 'video_count': 'gte:100', 'likes_count': 'gte:50000' } ``` --- ## Provider Traits ### About "Try It Live" Examples The interactive examples let you test real API calls to Base Verify: **What happens when you click "Try It":** 1. Your wallet signs a SIWE message with the specified traits 2. The request is sent to `https://verify.base.dev/v1/base_verify_token` 3. You see the full request (headers, body) and response (status, data) **Response Codes:** - **200 OK**: You have verified this provider and meet the trait requirements - **404 Not Found**: You haven't verified this provider yet (need to visit Base Verify Mini App) - **400 Bad Request** (message: `verification_traits_not_satisfied`): You have the provider account but don't meet traits (e.g., not enough followers) --- ### Coinbase **Provider:** `coinbase` **Available Traits:** | Trait | Type | Operations | Description | Example Values | | :---- | :---- | :---- | :---- | :---- | | `coinbase_one_active` | Boolean | `eq` | Active Coinbase One subscription | `"true"`, `"false"` | | `coinbase_one_billed` | Boolean | `eq` | User has been billed for Coinbase One | `"true"`, `"false"` | **Examples:** ```ts // Check for Coinbase One subscribers { provider: 'coinbase', traits: { 'coinbase_one_active': 'true' } } // Check for billed Coinbase One subscribers { provider: 'coinbase', traits: { 'coinbase_one_billed': 'true' } } ``` --- ### X (Twitter) **Provider:** `x` **Available Traits:** | Trait | Type | Operations | Description | Example Values | | :---- | :---- | :---- | :---- | :---- | | `verified` | Boolean | `eq` | Has any type of verification | `"true"`, `"false"` | | `verified_type` | String | `eq` | Type of verification | `"blue"`, `"government"`, `"business"`, `"none"` | | `followers` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Number of followers | `"1000"`, `"50000"` | **Examples:** ```ts // Check for any verified account { provider: 'x', traits: { 'verified': 'true' } } // Check for specific verification type { provider: 'x', traits: { 'verified_type': 'blue' } } // Check for follower count (greater than or equal to) { provider: 'x', traits: { 'followers': 'gte:1000' } } // Check for follower count (exact) { provider: 'x', traits: { 'followers': 'eq:50000' } } // Combine multiple traits { provider: 'x', traits: { 'verified': 'true', 'followers': 'gte:10000' } } ``` --- ### Instagram **Provider:** `instagram` **Available Traits:** | Trait | Type | Operations | Description | Example Values | | :---- | :---- | :---- | :---- | :---- | | `username` | String | `eq` | Instagram username | `"john_doe"` | | `followers_count` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Number of followers | `"1000"`, `"50000"` | | `instagram_id` | String | `eq` | Unique Instagram user ID | `"1234567890"` | **Examples:** ```ts // Check for specific username { provider: 'instagram', traits: { 'username': 'john_doe' } } // Check for follower count (greater than) { provider: 'instagram', traits: { 'followers_count': 'gt:1000' } } // Check for follower count (greater than or equal to) { provider: 'instagram', traits: { 'followers_count': 'gte:5000' } } // Combine multiple traits { provider: 'instagram', traits: { 'username': 'john_doe', 'followers_count': 'gte:10000' } } ``` --- ### TikTok **Provider:** `tiktok` **Available Traits:** | Trait | Type | Operations | Description | Example Values | | :---- | :---- | :---- | :---- | :---- | | `open_id` | String | `eq` | TikTok Open ID (unique per app) | `"abc123..."` | | `union_id` | String | `eq` | TikTok Union ID (unique across apps) | `"def456..."` | | `display_name` | String | `eq` | TikTok display name | `"John Doe"` | | `follower_count` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Number of followers | `"1000"`, `"50000"` | | `following_count` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Number of accounts following | `"500"`, `"2000"` | | `likes_count` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Total likes received | `"10000"`, `"100000"` | | `video_count` | Integer | `eq`, `gt`, `gte`, `lt`, `lte` | Number of videos posted | `"50"`, `"200"` | **Examples:** ```ts // Check for follower count { provider: 'tiktok', traits: { 'follower_count': 'gt:1000' } } // Check for likes count { provider: 'tiktok', traits: { 'likes_count': 'gte:10000' } } // Check for video count { provider: 'tiktok', traits: { 'video_count': 'gte:50' } } // Combine multiple traits (e.g., active creator) { provider: 'tiktok', traits: { 'follower_count': 'gte:5000', 'likes_count': 'gte:100000', 'video_count': 'gte:100' } } // Check for specific display name { provider: 'tiktok', traits: { 'display_name': 'John Doe' } } ``` --- ## Related Documentation - [Integration Guide](/docs/integration) - Complete implementation guide - [API Reference](/docs/api) - Endpoint documentation - [Security Overview](/docs/security) - Security and privacy ================================================================================ SOURCE: docs/api.md ================================================================================ # API Reference Complete API documentation for Base Verify endpoints. --- ## Authentication All API requests require authentication using your secret key in the `Authorization` header: ```typescript Authorization: Bearer YOUR_SECRET_KEY ``` **Security:** Your secret key must NEVER be exposed in frontend code. All Base Verify API calls should be proxied through your backend. --- ## POST /v1/base_verify_token Check if a wallet has a specific verification and retrieve the verification token. **Authentication:** Requires `Authorization: Bearer {SECRET_KEY}` **Important:** This endpoint must only be called from your backend. Never expose your secret key in frontend code. **Security Requirement:** Before calling this endpoint, your backend **MUST** validate that the trait requirements in the SIWE message match what your backend expects. See [Security Best Practices](/docs/security#validate-trait-requirements) for details. ### Request ```ts { signature: string, // SIWE signature from wallet message: string // SIWE message (includes provider/traits in resources) } ``` ### Backend Validation Required Before forwarding the request to Base Verify, validate trait requirements: ```typescript import { validateTraits } from './lib/trait-validator'; // Define expected traits (must match frontend) const expectedTraits = { 'verified': 'true', 'followers': 'gte:1000' }; // Validate message contains expected traits const validation = validateTraits(message, 'x', expectedTraits); if (!validation.valid) { return res.status(400).json({ error: 'Invalid trait requirements in message' }); } // Safe to forward to Base Verify API ``` This prevents users from modifying trait requirements on the frontend to bypass your access controls. ### Example Request ```bash curl -X POST https://verify.base.dev/v1/base_verify_token \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_SECRET_KEY" \ -d '{ "signature": "0x1234...", "message": "verify.base.dev wants you to sign in..." }' ``` ### Responses #### 200 OK - Verified Wallet has verified the provider account AND meets all trait requirements. ```json { "token": "abc123...", "action": "claim_airdrop", "wallet": "0x1234..." } ``` | Field | Type | Description | | :---- | :---- | :---- | | `token` | string | Deterministic verification token (for Sybil resistance). Same provider account + same action = same token. | | `action` | string | The custom action you specified in the SIWE message (e.g., `claim_airdrop`, `join_allowlist`). Different actions produce different tokens. See [Core Concepts](/docs/core-concepts#action). | | `wallet` | string | User's wallet address | #### 404 Not Found - Verification Not Found User doesn't have this verification. Redirect to mini app. ```json { "error": "verification_not_found" } ``` **What to do:** Redirect user to Base Verify Mini App to complete verification. #### 400 Bad Request - Traits Not Satisfied User has provider account but doesn't meet trait requirements. ```json { "code": 9, "message": "verification_traits_not_satisfied", "details": [] } ``` **What to do:** Show user they don't meet requirements. Do not redirect (they already have the account, just don't meet your thresholds). #### 401 Unauthorized - Invalid Key Invalid or missing API key. ```json { "error": "unauthorized" } ``` **What to do:** Check your secret key is correct and included in Authorization header. --- ## Mini App Redirect To redirect users to Base Verify for verification: ```typescript https://verify.base.dev?redirect_uri={your_app_url}&providers={provider} ``` ### Parameters | Parameter | Required | Description | Example | | :---- | :---- | :---- | :---- | | `redirect_uri` | Yes | Where to send user after verification | `https://yourapp.com` | | `providers` | Yes | Provider to verify | `x`, `coinbase`, `instagram`, `tiktok` | ### Example ```typescript const params = new URLSearchParams({ redirect_uri: 'https://yourapp.com', providers: 'x' }); const webAppUrl = `https://verify.base.dev?${params}`; window.location.href = webAppUrl; ``` After verification, user returns to your `redirect_uri` with `?success=true`. --- ## Error Handling Best Practices ### Do Not Retry 404 If you get 404, the user simply hasn't verified yet. Redirect them to Base Verify Mini App. ### Do Not Retry 400 (Traits Not Satisfied) If you get 400 with `verification_traits_not_satisfied`, the user won't pass your requirements. Retrying won't help unless their account metrics change (e.g., they gain more followers). ================================================================================ SOURCE: docs/security.md ================================================================================ # Security & Privacy How Base Verify protects user data and ensures secure verification. --- ## Data Storage ### What Base Verify Stores - Wallet addresses associated with verified provider accounts - Provider account metadata (username, follower counts, verification status) - OAuth tokens (encrypted, never shared with apps) - Verification timestamps ### What Base Verify Does NOT Store - Your users' private keys - Provider account passwords - User activity or browsing history - Any data beyond what's needed for verification --- ## What Your App Receives When you call `/v1/base_verify_token`, you receive: **Standard Response (200 OK):** ```json { "token": "abc123...", "action": "claim_airdrop", "wallet": "0x1234..." } ``` **No PII is returned** --- ## Privacy Protections ### 1. SIWE Signature Requirement Every API call requires a valid SIWE signature from the wallet owner. This prevents: - Arbitrary lookup of verification status - Third parties checking if a wallet is verified - Enumeration attacks **What is SIWE?** Sign-In with Ethereum lets the wallet owner prove control over their address by signing a structured message. Base Verify relies on it to ensure: 1. **Privacy protection** – Only the wallet owner can ask about their verification status. 2. **Security** – Requests come from the actual wallet, not a third party. 3. **Trait enforcement** – The SIWE message encodes the trait requirements you set; Base Verify validates that the signed traits match what your backend expects. **Example SIWE payload** ```typescript { domain: "your-app.com", address: "0x1234...", // User's wallet chainId: 8453, // Base resources: [ "urn:verify:provider:x", // Which provider "urn:verify:provider:x:verified:eq:true", // Trait requirements "urn:verify:action:claim_airdrop" // Your custom action name ] } ``` The user signs this message, proving they control the wallet and agree to check those specific traits. ### 2. OAuth Token Security - OAuth access tokens are encrypted at rest - Never exposed to your application - Used only by Base Verify to refresh provider data - Can be revoked by user at any time ### 3. User Control Users can delete their verifications at any time: - Removes all stored provider data - Invalidates future token generation - Your app's stored tokens become meaningless (user can't re-verify with same account) --- ## OAuth Security Model **How Base Verify validates provider accounts:** 1. **User initiates OAuth** in Base Verify Mini App 2. **Provider (X, Instagram, etc.) authenticates** the user 3. **Provider returns OAuth token** to Base Verify 4. **Base Verify fetches account data** using OAuth token 5. **Base Verify stores verification** linked to user's wallet 6. **OAuth token is encrypted** and stored securely **Your app never handles OAuth tokens or redirects.** This is all handled within the Base Verify Mini App. --- ## Best Practices ### Validate Trait Requirements **Critical Security Requirement:** When your backend receives a SIWE message from the frontend, you **MUST** validate that the trait requirements in the message match what your backend expects. This prevents users from modifying trait requirements on the frontend to bypass your access controls. **Example Attack Without Validation:** 1. Your app requires users to have 100 followers 2. User modifies the frontend to request only 10 followers 3. User signs the modified message 4. Without validation, your backend forwards the request to Base Verify 5. User gains access with less than 100 followers ❌ **Implementation:** ```typescript import { validateTraits } from './lib/trait-validator'; // Define what traits your app requires const expectedTraits = { 'followers': 'gte:100' }; // Validate before forwarding to Base Verify 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 forward to Base Verify API ``` **Key Points:** - Trait requirements in SIWE message are embedded as URNs (e.g., `urn:verify:provider:x:followers:gte:100`) - Users can modify these before signing - Backend must validate they match expected requirements - Validation must happen **before** calling Base Verify API ### Protect Your Secret Key **Never:** - Include secret key in frontend code - Use `NEXT_PUBLIC_*` or similar environment variables that expose to browser - Commit secret keys to version control - Share secret keys in chat, email, or documentation **Always:** - Store secret key in backend environment variables only - Use `.env` files that are gitignored - Rotate keys immediately if accidentally exposed - Call Base Verify API only from your backend ### Token Storage Store verification tokens securely: ```typescript // In your database { token: "abc123...", // Unique per provider account walletAddress: "0x1234...", // The wallet (for your records) provider: "x", // Which provider claimedAt: "2024-01-15" // When they verified } ``` Index by `token` to prevent duplicate claims across different wallets. ### Caching Cache verification results to reduce API calls: - Cache for the user's session (not permanently) - Clear cache when user disconnects wallet - Don't check verification on every page load ### Error Handling Handle each response code appropriately: - **200** → Grant access - **404** → Redirect to Base Verify Mini App - **400 (traits_not_satisfied)** → Show user they don't meet requirements (don't retry) --- ## Related Documentation - [Integration Guide](/docs/integration) - Full implementation guide - [API Reference](/docs/api) - Endpoint documentation - [Trait Catalog](/docs/traits) - Available traits