Token Gate Verification API
The Token Gate Verification API provides secure access control for token-gated content in the RitoSwap dApp. This endpoint verifies that users own specific Colored Key NFTs before granting access to exclusive features and sends notifications when gates are successfully passed.
Overview
The token gate system implements a multi-layered security approach that combines cryptographic signature verification, on-chain ownership validation, and usage tracking to ensure that only legitimate token holders can access gated content. This creates a trustless system where access rights are determined entirely by blockchain state rather than centralized databases.
How Token Gating Works
When a user attempts to access gated content, the following verification flow occurs:
- Client-side signature generation - The user’s wallet signs a message containing their address, token ID, and timestamp
- Server-side verification - The API verifies the signature matches the claimed address
- On-chain validation - The smart contract confirms actual token ownership
- Usage tracking - The database ensures each token can only be used once
- Notification dispatch - An email alert is sent to administrators about the successful access
This multi-step process ensures that even if one layer is compromised, the overall security remains intact.
Endpoint Details
Property | Value |
---|---|
URL | /api/verify-token-gate |
Method | POST |
Content-Type | application/json |
Authentication | Cryptographic signature (EIP-191) |
Network-Aware Database Operations
This API leverages RitoSwap’s sophisticated network routing infrastructure to ensure all operations target the correct blockchain network and corresponding database table. The system automatically handles network detection and routing, allowing the API to work seamlessly across Ethereum mainnet, Sepolia testnet, and local RitoNet development networks.
Automatic Network Routing
The API uses two key utilities from the prismaNetworkUtils module to handle multichain operations:
Database Operations via getTokenModel()
This function returns a unified interface that automatically routes database queries to the correct network-specific table:
import { getTokenModel } from '@/app/lib/prisma/prismaNetworkUtils'
const tokenModel = getTokenModel()
// Automatically queries token_ethereum, token_sepolia, or token_ritonet
const token = await tokenModel.findUnique({
where: { tokenId }
})
Blockchain Configuration via getChainConfig()
This function provides the correct RPC endpoints and chain metadata for the active network:
import { getChainConfig } from '@/app/lib/prisma/prismaNetworkUtils'
import { createPublicClient, http } from 'viem'
const chainConfig = getChainConfig()
const publicClient = createPublicClient({
chain: chainConfig.chain,
transport: http(chainConfig.transport)
})
Network Detection Priority
The system determines the active network through environment variables in the following order of precedence:
- RitoNet (Local Development) - Active when
NEXT_PUBLIC_RITONET=true
- Sepolia (Testnet) - Active when
NEXT_PUBLIC_SEPOLIA=true
- Ethereum (Mainnet) - Default when no network flags are set
This precedence ensures development and testing environments take priority, preventing accidental mainnet operations during development.
Cross-Network Isolation
Each blockchain network maintains its own isolated database table with identical structure:
- Ethereum Mainnet →
token_ethereum
table - Sepolia Testnet →
token_sepolia
table - RitoNet Local →
token_ritonet
table
This isolation prevents cross-network data conflicts while enabling network-specific optimizations and simplified debugging. The same token ID can exist on different networks representing entirely different assets, and this architecture ensures they never interfere with each other.
The network routing happens transparently to the API logic. Developers write network-agnostic code while the infrastructure handles proper routing behind the scenes. For detailed information about the network utilities, see the Network-Aware Database Utils documentation.
Request Format
The request body must be a JSON object containing all required fields for verification:
interface TokenGateRequest {
tokenId: number; // The ID of the Colored Key NFT
message: string; // The user's message (max 10,000 characters)
signature: string; // EIP-191 signature as hex string
signMessage: string; // The exact message that was signed
address: string; // Ethereum address claiming ownership
timestamp: number; // Unix timestamp when signature was created
}
Request Parameters
Parameter | Type | Required | Description |
---|---|---|---|
tokenId | number | Yes | The unique identifier of the Colored Key NFT being used for access. Must match the token currently owned by the signing address. |
message | string | Yes | The content message from the user. Limited to 10,000 characters to prevent abuse while allowing substantial communication. |
signature | string | Yes | Hex-encoded EIP-191 signature proving the address holder authorized this request. Format: 0x... |
signMessage | string | Yes | The exact message that was signed by the wallet. This should contain structured data about the request for verification. |
address | string | Yes | Ethereum address claiming to own the token. Format: 0x... (42 characters) |
timestamp | number | Yes | Unix timestamp (milliseconds) when the signature was created. Must be within 5 minutes of server time. |
Signature Message Format
The signMessage
field should follow a structured format that includes all verification data. Here’s the recommended format that clients should use:
RitoSwap Token Gate Access
Token ID: ${tokenId}
Address: ${address}
Timestamp: ${timestamp}
Nonce: ${randomNonce}
This structured message ensures that signatures cannot be reused across different contexts or replay attacks.
Response Formats
Success Response
When token verification succeeds and access is granted:
{
"success": true,
"message": "Access granted"
}
HTTP Status Code: 200 OK
Error Responses
The API returns detailed error messages to help diagnose issues:
Rate Limit Exceeded
{
"error": "Too many requests",
"limit": 3,
"remaining": 0,
"retryAfter": 45
}
HTTP Status Code: 429 Too Many Requests
Response Headers:
X-RateLimit-Limit
: Maximum requests allowedX-RateLimit-Remaining
: Requests remaining in windowRetry-After
: Seconds until rate limit resets
Validation Errors
{
"error": "Missing required fields"
}
HTTP Status Code: 400 Bad Request
Common validation errors:
"Invalid JSON"
- Request body is not valid JSON"Missing required fields"
- One or more required parameters are missing"Message too long"
- Message exceeds 10,000 character limit"Signature expired"
- Timestamp is more than 5 minutes old
Authentication Errors
{
"error": "Invalid signature"
}
HTTP Status Code: 401 Unauthorized
This occurs when the cryptographic signature cannot be verified against the claimed address.
Authorization Errors
{
"error": "You do not own this token"
}
HTTP Status Code: 403 Forbidden
Common authorization errors:
"You do not own this token"
- On-chain verification shows different ownership"This token has already been used"
- Token was previously used for gate access
Server Errors
{
"error": "Failed to verify token ownership"
}
HTTP Status Code: 500 Internal Server Error
Possible causes:
- Blockchain RPC connection issues
- Database connectivity problems
- Email service failures
Rate Limiting
The API implements intelligent rate limiting to prevent abuse while allowing legitimate usage:
Rate Limit Configuration
- Limit: 3 requests per time window
- Window: Sliding window based on IP address and nonce
- Reset: Automatic after time window expires
Rate Limit Headers
Every response includes rate limit information:
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 2
Retry-After: 60
Handling Rate Limits
When rate limited, clients should:
- Check the
Retry-After
header - Wait the specified number of seconds
- Retry the request with the same parameters
// Example client-side rate limit handling
async function verifyWithRetry(data, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
const response = await fetch('/api/verify-token-gate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '60');
console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
return response;
}
throw new Error('Max retries exceeded');
}
Security Considerations
Signature Verification
The API uses EIP-191 signature verification to ensure requests originate from the claimed address:
// Server-side verification using viem
const isValid = await verifyMessage({
address: address as `0x${string}`,
message: signMessage,
signature: signature as `0x${string}`,
});
Timestamp Validation
Signatures expire after 5 minutes to prevent replay attacks:
if (Date.now() - timestamp > 5 * 60 * 1000) {
// Signature has expired
}
On-Chain Verification
The API performs real-time blockchain queries to verify token ownership:
const [ownedTokenId, hasToken] = await publicClient.readContract({
address: KEY_TOKEN_ADDRESS,
abi: fullKeyTokenAbi,
functionName: 'getTokenOfOwner',
args: [address],
});
This ensures that even if other checks pass, only actual token owners can access gated content.
Email Notification System
The API provides flexible email delivery configuration, allowing you to choose between direct serverless function delivery or delegating to a Cloudflare Worker based on your deployment needs and scale requirements.
Email Delivery Configuration
The email notification system intelligently selects its delivery method based on your environment configuration. When VERCEL_ENV
is set to production
, the API will send real email notifications using one of two configurable methods. In development mode, emails are stubbed and logged to the console instead of being sent.
Direct Serverless (Brevo API)
Direct Serverless Function Delivery
When USE_CLOUDFLARE_WORKER
is set to false
or not defined, the API sends emails directly from the Vercel serverless function using the Brevo (formerly SendinBlue) API. This configuration is ideal for:
- Smaller deployments with moderate email volume
- Simpler infrastructure requirements
- Direct control over email formatting and delivery
The serverless function handles all aspects of email composition and delivery within the same API request cycle. This approach minimizes external dependencies but may impact response times for high-volume scenarios.
Required environment variables:
# Email service configuration
BREVO_API_KEY=xkeysib-xxxxxxxxxxxx
SENDER_EMAIL=noreply@ritoswap.com
RECEIVER_EMAIL=admin@ritoswap.com
# Disable worker mode (or omit this variable)
USE_CLOUDFLARE_WORKER=false
Email Content Format
Regardless of the delivery method chosen, notification emails maintain a consistent format that includes:
- Subject Line:
Gated Msg by ${truncatedAddress}
(e.g., “Gated Msg by 0x1234…5678”) - Token ID: The specific NFT used for access
- Ethereum Address: Full address of the user
- Timestamp: Formatted date and time of access
- Message Content: The full user message in a styled container
The HTML email template provides a clean, professional appearance with proper formatting for readability across email clients.
Development Mode Behavior
In development environments (when VERCEL_ENV
is not set to production
), the API logs email details to the console instead of sending actual emails:
[DEV] Stubbing email, token ${tokenId} by ${address}: ${message}
This allows you to test the full verification flow without configuring email services or sending test emails.
Environment Configuration
The API requires several environment variables for proper operation:
# Deployment Environment
VERCEL_ENV=production # Set to 'production' to enable email sending
# Email Configuration Option 1: Direct Serverless Delivery
USE_CLOUDFLARE_WORKER=false # Or omit this variable entirely
BREVO_API_KEY=xkeysib-xxxxxxxxxxxx
SENDER_EMAIL=noreply@ritoswap.com
RECEIVER_EMAIL=admin@ritoswap.com
# Email Configuration Option 2: Cloudflare Worker Delegation
USE_CLOUDFLARE_WORKER=true
CLOUDFLARE_WORKER_URL=https://your-worker.workers.dev
# Blockchain Configuration (handled by getChainConfig)
# These are configured in your chain configuration utilities
Integration Example
Here’s a complete example of integrating with the Token Gate API from a React component:
import { useAccount, useSignMessage } from 'wagmi';
import { useState } from 'react';
function TokenGateAccess({ tokenId }: { tokenId: number }) {
const { address } = useAccount();
const { signMessageAsync } = useSignMessage();
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async () => {
if (!address) return;
setIsLoading(true);
try {
// Generate timestamp and nonce for security
const timestamp = Date.now();
const nonce = Math.random().toString(36).substring(7);
// Create the message to sign
const signMessage = `RitoSwap Token Gate Access
Token ID: ${tokenId}
Address: ${address}
Timestamp: ${timestamp}
Nonce: ${nonce}`;
// Request signature from wallet
const signature = await signMessageAsync({
message: signMessage
});
// Submit to API
const response = await fetch('/api/verify-token-gate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tokenId,
message,
signature,
signMessage,
address,
timestamp
})
});
const data = await response.json();
if (response.ok) {
// Access granted - proceed with gated content
console.log('Access granted!', data);
} else {
// Handle specific error cases
if (response.status === 429) {
console.error('Rate limited. Try again later.');
} else {
console.error('Access denied:', data.error);
}
}
} catch (error) {
console.error('Failed to verify:', error);
} finally {
setIsLoading(false);
}
};
return (
<div>
<textarea
value={message}
onChange={(e) => setMessage(e.target.value)}
placeholder="Enter your message..."
maxLength={10000}
/>
<button onClick={handleSubmit} disabled={isLoading}>
{isLoading ? 'Verifying...' : 'Access Gated Content'}
</button>
</div>
);
}
Error Handling Best Practices
When integrating with this API, implement comprehensive error handling:
try {
const response = await fetch('/api/verify-token-gate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
});
const data = await response.json();
switch (response.status) {
case 200:
// Success - proceed with gated content
break;
case 400:
// Validation error - check your request format
console.error('Invalid request:', data.error);
break;
case 401:
// Signature verification failed
console.error('Authentication failed:', data.error);
break;
case 403:
// Not authorized - check token ownership
console.error('Authorization failed:', data.error);
break;
case 429:
// Rate limited - implement retry logic
const retryAfter = response.headers.get('Retry-After');
console.error(`Rate limited. Retry after ${retryAfter} seconds`);
break;
case 500:
// Server error - likely temporary
console.error('Server error. Please try again later.');
break;
}
} catch (error) {
// Network or parsing error
console.error('Request failed:', error);
}
Testing Recommendations
When developing and testing token gate functionality, you have two excellent options for blockchain environments:
Option 1: Local Blockchain Network
For complete control over your testing environment, use the RitoSwap Local Blockchain setup. This provides a private Proof-of-Authority blockchain with pre-funded accounts and instant block times. The Local Blockchain is ideal for:
- Rapid iteration during development
- Testing without internet connectivity
- Complete control over blockchain state
- Zero gas costs and instant transactions
To set up your local testing environment, follow the comprehensive guide at /local-network. This guide walks you through spinning up a local Geth node with Blockscout explorer for full visibility into your blockchain interactions.
Option 2: Sepolia Testnet
For testing in a more production-like environment, use the Ethereum Sepolia testnet. This public test network provides:
- Real network conditions and latency
- Interaction with other deployed contracts
- Testing across multiple independent nodes
- Closer simulation of mainnet behavior
You can obtain free testnet ETH from the Google Cloud Sepolia faucet to cover gas costs for your testing.
Testing Checklist
Regardless of which network you choose, ensure you test these scenarios:
- Mint test tokens using the
/mint
page on your chosen network - Test successful verification with a valid token and signature
- Test rate limiting by making multiple rapid requests
- Test error cases including:
- Expired signatures (wait 5+ minutes before submitting)
- Non-owned tokens (try to use a token ID you don’t own)
- Already-used tokens (submit the same token twice)
- Invalid signatures (modify the signature before sending)
- Verify email delivery works correctly in production mode
- Test wallet disconnection during the verification flow
Comprehensive Test Suite Example
Here’s a complete test suite that demonstrates how to test the Token Gate API using Vitest. This example shows testing patterns for both development and production modes, including email delivery via both Cloudflare Worker and direct Brevo API:
/// <reference types="vitest/globals" />
import type { Mock } from 'vitest'
import type { NextRequest } from 'next/server'
// Mock all external dependencies before importing the route
vi.mock('viem', () => ({
createPublicClient: vi.fn(),
http: vi.fn(),
verifyMessage: vi.fn(),
}))
vi.mock('@/app/lib/prisma/prismaNetworkUtils', () => ({
getTokenModel: vi.fn(),
getChainConfig: vi.fn(),
}))
vi.mock('@/app/config/contracts', () => ({
fullKeyTokenAbi: [],
KEY_TOKEN_ADDRESS: '0xContract',
}))
vi.mock('@/app/lib/rateLimit/rateLimit.server', () => ({
checkRateLimitWithNonce: vi.fn(),
}))
import { POST } from '../route'
import { createPublicClient, verifyMessage } from 'viem'
import { getTokenModel, getChainConfig } from '@/app/lib/prisma/prismaNetworkUtils'
import { checkRateLimitWithNonce } from '@/app/lib/rateLimit/rateLimit.server'
describe('POST /api/verify-token-gate (dev mode)', () => {
let tokenModel: { findUnique: Mock; upsert: Mock }
let mockClient: { readContract: Mock }
const now = Date.now()
beforeEach(() => {
// Default: passes rate limit
vi.mocked(checkRateLimitWithNonce).mockResolvedValue({
success: true,
limit: 5,
remaining: 5
})
// Mock database model
tokenModel = { findUnique: vi.fn(), upsert: vi.fn() }
vi.mocked(getTokenModel).mockReturnValue(tokenModel as any)
// Mock chain configuration
vi.mocked(getChainConfig).mockReturnValue({
chain: {} as any,
transport: 'http://rpc',
})
// Mock blockchain client
mockClient = { readContract: vi.fn() }
vi.mocked(createPublicClient).mockReturnValue(mockClient as any)
vi.mocked(verifyMessage).mockReset()
})
const makeReq = (body: unknown): Promise<Response> =>
POST({ json: async () => body } as unknown as NextRequest)
it('returns 429 when rate limit exceeded', async () => {
const resetTs = now + 120_000
vi.mocked(checkRateLimitWithNonce).mockResolvedValueOnce({
success: false,
limit: 10,
remaining: 0,
reset: resetTs,
})
const res = await makeReq({
tokenId: 1,
message: 'msg',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
expect(res.status).toBe(429)
expect(res.headers.get('X-RateLimit-Limit')).toBe('10')
expect(res.headers.get('X-RateLimit-Remaining')).toBe('0')
// Retry-After should be approximately 120 seconds
expect(Number(res.headers.get('Retry-After'))).toBeGreaterThanOrEqual(119)
})
it('returns 400 for invalid JSON', async () => {
const badReq = { json: async () => { throw new Error('bad') } } as any
const res = await POST(badReq)
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ error: 'Invalid JSON' })
})
it('returns 400 for missing required fields', async () => {
const res = await makeReq({ tokenId: 1 })
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ error: 'Missing required fields' })
})
it('returns 400 when message is too long', async () => {
const body = {
tokenId: 1,
message: 'x'.repeat(10_001),
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
}
const res = await makeReq(body)
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ error: 'Message too long' })
})
it('returns 400 when signature is expired', async () => {
const body = {
tokenId: 1,
message: 'msg',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now - 6 * 60 * 1000, // 6 minutes ago
}
const res = await makeReq(body)
expect(res.status).toBe(400)
expect(await res.json()).toEqual({ error: 'Signature expired' })
})
it('returns 401 when signature verification fails', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(false)
const res = await makeReq({
tokenId: 1,
message: 'm',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
expect(res.status).toBe(401)
expect(await res.json()).toEqual({ error: 'Invalid signature' })
})
it('returns 403 when user does not own the token', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(true)
// User owns token ID 2, not token ID 1
mockClient.readContract.mockResolvedValueOnce([BigInt(2), true])
tokenModel.findUnique.mockResolvedValueOnce(null)
const res = await makeReq({
tokenId: 1,
message: 'm',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
expect(res.status).toBe(403)
expect(await res.json()).toEqual({ error: 'You do not own this token' })
})
it('returns 403 when token has already been used', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(true)
mockClient.readContract.mockResolvedValueOnce([BigInt(1), true])
tokenModel.findUnique.mockResolvedValueOnce({ used: true })
const res = await makeReq({
tokenId: 1,
message: 'm',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
expect(res.status).toBe(403)
expect(await res.json()).toEqual({ error: 'This token has already been used' })
})
it('returns 200 on successful verification in dev mode', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(true)
mockClient.readContract.mockResolvedValueOnce([BigInt(1), true])
tokenModel.findUnique.mockResolvedValueOnce({ used: false })
tokenModel.upsert.mockResolvedValueOnce({})
const res = await makeReq({
tokenId: 1,
message: 'm',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ success: true, message: 'Access granted' })
// Verify token was marked as used
expect(tokenModel.upsert).toHaveBeenCalledWith({
where: { tokenId: 1 },
update: { used: true, usedBy: '0xaddr', usedAt: expect.any(Date) },
create: { tokenId: 1, used: true, usedBy: '0xaddr', usedAt: expect.any(Date) },
})
})
})
describe('POST /api/verify-token-gate (production with Cloudflare Worker)', () => {
let POSTProd: (req: NextRequest) => Promise<Response>
let tokenModel: { findUnique: Mock; upsert: Mock }
let mockClient: { readContract: Mock }
const now = Date.now()
// Save original environment variables
const originalEnv = { ...process.env }
beforeAll(async () => {
process.env.VERCEL_ENV = 'production'
process.env.USE_CLOUDFLARE_WORKER = 'true'
process.env.CLOUDFLARE_WORKER_URL = 'https://worker.test'
// Clear module cache so environment variables are re-read
vi.resetModules()
const route = await import('../route')
POSTProd = route.POST
})
afterAll(() => {
// Restore original environment
Object.assign(process.env, originalEnv)
})
beforeEach(() => {
// Set up mocks similar to dev mode
vi.mocked(checkRateLimitWithNonce).mockResolvedValue({
success: true,
limit: 5,
remaining: 5
})
tokenModel = { findUnique: vi.fn(), upsert: vi.fn() }
vi.mocked(getTokenModel).mockReturnValue(tokenModel as any)
mockClient = { readContract: vi.fn() }
vi.mocked(createPublicClient).mockReturnValue(mockClient as any)
vi.mocked(verifyMessage).mockReset()
// Mock fetch for email sending
global.fetch = vi.fn()
})
it('sends email via Cloudflare Worker on success', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(true)
mockClient.readContract.mockResolvedValueOnce([BigInt(1), true])
tokenModel.findUnique.mockResolvedValueOnce({ used: false })
tokenModel.upsert.mockResolvedValueOnce({})
// Mock successful worker response
;(global.fetch as Mock).mockResolvedValueOnce({
ok: true,
status: 200,
text: async () => JSON.stringify({ messageId: 'msg-123' }),
} as any)
const res = await POSTProd({
json: async () => ({
tokenId: 1,
message: 'hello',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
} as unknown as NextRequest)
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ success: true, message: 'Access granted' })
// Verify worker was called with correct data
expect(global.fetch).toHaveBeenCalledWith(
'https://worker.test',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
tokenId: 1,
message: 'hello',
address: '0xaddr',
timestamp: now,
}),
})
)
})
it('returns 500 when email service fails', async () => {
vi.mocked(verifyMessage).mockResolvedValueOnce(true)
mockClient.readContract.mockResolvedValueOnce([BigInt(1), true])
tokenModel.findUnique.mockResolvedValueOnce({ used: false })
// Mock worker failure
;(global.fetch as Mock).mockResolvedValueOnce({
ok: false,
status: 500,
text: async () => JSON.stringify({ error: 'Worker error' }),
} as any)
const res = await POSTProd({
json: async () => ({
tokenId: 1,
message: 'test',
signature: 'sig',
signMessage:'sign',
address: '0xaddr',
timestamp: now,
})
} as unknown as NextRequest)
expect(res.status).toBe(500)
expect(await res.json()).toEqual({ error: 'Worker error' })
})
})
This comprehensive test suite demonstrates several important testing patterns:
- Module Mocking - All external dependencies are mocked before importing the route
- Environment Testing - Shows how to test different configurations (dev vs production)
- Email Configuration Testing - Tests both Cloudflare Worker and direct Brevo API paths
- Error Coverage - Tests all error scenarios with appropriate status codes
- State Verification - Confirms that tokens are properly marked as used in the database
The tests provide a blueprint for developers to ensure their integration with the Token Gate API handles all possible scenarios correctly.
Summary
The Token Gate Verification API provides a secure, rate-limited endpoint for controlling access to exclusive content based on NFT ownership. By combining cryptographic signatures, on-chain verification, and usage tracking, it ensures that only legitimate token holders can access gated features while preventing abuse through rate limiting and single-use enforcement.
The flexible email notification system supports both direct serverless delivery and Cloudflare Worker delegation, allowing you to choose the architecture that best fits your scale and infrastructure requirements. Whether you’re testing locally with the RitoSwap Local Blockchain or deploying to production with mainnet integration, the API provides a robust foundation for token-gated experiences in the RitoSwap ecosystem.