Skip to Content
Welcome to RitoSwap's documentation!
DAppAPIToken Gate Verification

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:

  1. Client-side signature generation - The user’s wallet signs a message containing their address, token ID, and timestamp
  2. Server-side verification - The API verifies the signature matches the claimed address
  3. On-chain validation - The smart contract confirms actual token ownership
  4. Usage tracking - The database ensures each token can only be used once
  5. 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

PropertyValue
URL/api/verify-token-gate
MethodPOST
Content-Typeapplication/json
AuthenticationCryptographic 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:

  1. RitoNet (Local Development) - Active when NEXT_PUBLIC_RITONET=true
  2. Sepolia (Testnet) - Active when NEXT_PUBLIC_SEPOLIA=true
  3. 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 Mainnettoken_ethereum table
  • Sepolia Testnettoken_sepolia table
  • RitoNet Localtoken_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

ParameterTypeRequiredDescription
tokenIdnumberYesThe unique identifier of the Colored Key NFT being used for access. Must match the token currently owned by the signing address.
messagestringYesThe content message from the user. Limited to 10,000 characters to prevent abuse while allowing substantial communication.
signaturestringYesHex-encoded EIP-191 signature proving the address holder authorized this request. Format: 0x...
signMessagestringYesThe exact message that was signed by the wallet. This should contain structured data about the request for verification.
addressstringYesEthereum address claiming to own the token. Format: 0x... (42 characters)
timestampnumberYesUnix 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 allowed
  • X-RateLimit-Remaining: Requests remaining in window
  • Retry-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:

  1. Check the Retry-After header
  2. Wait the specified number of seconds
  3. 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 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:

  1. Mint test tokens using the /mint page on your chosen network
  2. Test successful verification with a valid token and signature
  3. Test rate limiting by making multiple rapid requests
  4. 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)
  5. Verify email delivery works correctly in production mode
  6. 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:

  1. Module Mocking - All external dependencies are mocked before importing the route
  2. Environment Testing - Shows how to test different configurations (dev vs production)
  3. Email Configuration Testing - Tests both Cloudflare Worker and direct Brevo API paths
  4. Error Coverage - Tests all error scenarios with appropriate status codes
  5. 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.