Skip to Content
Welcome to RitoSwap's documentation!
DAppAPIToken Status

Token Status API

The Token Status API provides real-time information about Colored Key NFT existence and usage status within the RitoSwap ecosystem. This endpoint enables the dApp to display accurate token states, track which tokens have been used for gating, and synchronize on-chain data with the off-chain database.

Overview

Understanding token status is crucial for the RitoSwap user experience. When users view their tokens or attempt to access gated content, the dApp needs to know not just whether a token exists on-chain, but also whether it has already been used for token-gated access. This API bridges that gap by providing a unified view of both on-chain existence and off-chain usage tracking.

The Dual Nature of Token Status

The API manages two distinct but related pieces of information about each token:

  1. On-chain existence - Whether the token has been minted and exists in the smart contract
  2. Off-chain usage - Whether the token has been used to access gated content

This dual tracking system allows RitoSwap to enforce single-use token gating while maintaining the immutability of the blockchain. The smart contract doesn’t need to track usage state (which would cost gas), while the database provides instant lookups for the UI.

How Status Checking Works

The API follows an intelligent cascade of checks to provide accurate status information:

  1. Database lookup - First checks if the token exists in the database with usage information
  2. On-chain verification - If not in database, queries the blockchain to check existence
  3. Database synchronization - If found on-chain but not in database, creates a record
  4. Status response - Returns comprehensive status including existence and usage data

This approach minimizes blockchain queries while ensuring data accuracy, making it efficient for the frequent polling required by real-time UI updates.

Endpoint Details

PropertyValue
URL/api/token-status/[tokenId]
MethodGET
URL ParameterstokenId - The numeric ID of the token
AuthenticationNone required (public endpoint)

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 API uses a simple GET request with the token ID embedded in the URL path:

GET /api/token-status/123

URL Parameters

ParameterTypeRequiredDescription
tokenIdstring (numeric)YesThe ID of the token to check. Must be a non-negative integer. The API will parse this string into a number for processing.

Request Examples

// Check status of token #42 const response = await fetch('/api/token-status/42'); const status = await response.json(); // Check status of token from user input const tokenId = document.getElementById('tokenInput').value; const response = await fetch(`/api/token-status/${tokenId}`);

Response Formats

Success Response

The API returns a consistent response structure regardless of whether the token exists:

interface TokenStatusResponse { count: number; // 1 if exists, 0 if not exists: boolean; // True if token exists on-chain used: boolean; // True if used for gating usedBy: string | null; // Address that used the token usedAt: string | null; // ISO timestamp of usage }

Token Exists and Unused

{ "count": 1, "exists": true, "used": false, "usedBy": null, "usedAt": null }

Token Exists and Used

{ "count": 1, "exists": true, "used": true, "usedBy": "0x1234567890123456789012345678901234567890", "usedAt": "2024-03-15T10:30:00.000Z" }

Token Does Not Exist

{ "count": 0, "exists": false, "used": false, "usedBy": null, "usedAt": null }

HTTP Status Code: 200 OK for all valid requests, even if token doesn’t exist

Error Responses

The API distinguishes between client errors and server errors:

Invalid Token ID

{ "error": "Invalid token ID" }

HTTP Status Code: 400 Bad Request

This occurs when:

  • The token ID is not a valid number
  • The token ID is negative
  • The URL parameter is missing or malformed

Rate Limit Exceeded

{ "error": "Too many requests", "limit": 60, "remaining": 0, "retryAfter": 45 }

HTTP Status Code: 429 Too Many Requests

Response Headers:

  • X-RateLimit-Limit: Requests allowed per window
  • X-RateLimit-Remaining: Requests remaining
  • Retry-After: Seconds until rate limit resets

Server Error

{ "error": "Failed to check token status" }

HTTP Status Code: 500 Internal Server Error

This indicates issues with:

  • Database connectivity
  • Blockchain RPC endpoints
  • Internal processing errors

Rate Limiting

The Token Status API implements rate limiting with special considerations for UI polling requirements:

Rate Limit Configuration

  • Limit: 60 requests per minute per IP
  • Window: Sliding window implementation
  • Global limit: Disabled (third parameter is false in rate limit check)

The higher rate limit and disabled global limit accommodate the real-time polling nature of the RitoSwap UI, which checks token status frequently to provide immediate feedback when tokens are minted, transferred, or used.

Why No Global Rate Limit?

The code explicitly disables the global rate limit by passing false as the third parameter:

const rateLimitResult = await checkRateLimitWithNonce(request, 'tokenStatus', false)

This design decision recognizes that token status checks are lightweight read operations that:

  • Don’t modify any state
  • Use efficient database queries with indexing
  • Are cached at the blockchain RPC level
  • Are essential for responsive UI updates

Handling Rate Limits in Polling

When implementing polling, respect rate limits to maintain service reliability:

class TokenStatusPoller { constructor(tokenId) { this.tokenId = tokenId; this.retryAfter = 0; this.polling = false; } async checkStatus() { // Respect rate limit retry timing if (this.retryAfter > Date.now()) { console.log(`Waiting for rate limit to reset...`); return null; } try { const response = await fetch(`/api/token-status/${this.tokenId}`); if (response.status === 429) { // Extract retry timing from response const retrySeconds = parseInt( response.headers.get('Retry-After') || '60' ); this.retryAfter = Date.now() + (retrySeconds * 1000); const data = await response.json(); console.warn(`Rate limited. Retry after ${retrySeconds}s`); return null; } return await response.json(); } catch (error) { console.error('Status check failed:', error); return null; } } startPolling(interval = 5000, callback) { this.polling = true; const poll = async () => { if (!this.polling) return; const status = await this.checkStatus(); if (status) { callback(status); } // Schedule next poll, accounting for rate limits const nextInterval = this.retryAfter > Date.now() ? this.retryAfter - Date.now() : interval; setTimeout(poll, nextInterval); }; poll(); } stopPolling() { this.polling = false; } } // Usage const poller = new TokenStatusPoller(123); poller.startPolling(5000, (status) => { console.log('Token status:', status); if (status.used) { poller.stopPolling(); // Stop polling once used } });

Database Synchronization

One of the key features of this API is its ability to synchronize on-chain data with the off-chain database. This synchronization happens automatically and transparently:

Synchronization Flow

Step 1: Database Check

The API first queries the database for existing token records. If found, it returns immediately with the stored usage information.

Step 2: Blockchain Verification

If not in the database, the API queries the blockchain by attempting to read the token’s URI. This call will revert if the token doesn’t exist, providing a reliable existence check.

Step 3: Automatic Record Creation

If the token exists on-chain but not in the database, the API creates a new database record marked as unused. This ensures future queries can skip the blockchain check.

Step 4: Response Generation

The API returns the unified status, combining on-chain existence with off-chain usage data.

Why This Matters

This synchronization pattern provides several benefits:

  1. Performance - Database queries are much faster than blockchain calls
  2. Consistency - All tokens that exist on-chain are tracked in the database
  3. Reliability - The system self-heals if database records are missing
  4. Scalability - Reduces blockchain RPC load as the system grows

Integration Patterns

The Token Status API is designed to support various integration patterns common in dApp development:

Single Status Check

For one-time status checks, such as when a page loads:

async function checkTokenStatus(tokenId: number): Promise<TokenStatus | null> { try { const response = await fetch(`/api/token-status/${tokenId}`); if (!response.ok) { if (response.status === 429) { console.warn('Rate limited, please try again later'); } else { console.error('Failed to check status:', response.statusText); } return null; } return await response.json(); } catch (error) { console.error('Network error checking token status:', error); return null; } }

React Hook for Token Status

Create a reusable hook for React components:

import { useState, useEffect } from 'react'; interface TokenStatus { count: number; exists: boolean; used: boolean; usedBy: string | null; usedAt: string | null; } export function useTokenStatus(tokenId: number | null) { const [status, setStatus] = useState<TokenStatus | null>(null); const [loading, setLoading] = useState(false); const [error, setError] = useState<string | null>(null); useEffect(() => { if (tokenId === null || tokenId < 0) { setStatus(null); return; } let cancelled = false; async function fetchStatus() { setLoading(true); setError(null); try { const response = await fetch(`/api/token-status/${tokenId}`); if (!response.ok) { const data = await response.json(); throw new Error(data.error || 'Failed to fetch status'); } const data = await response.json(); if (!cancelled) { setStatus(data); } } catch (err) { if (!cancelled) { setError(err instanceof Error ? err.message : 'Unknown error'); setStatus(null); } } finally { if (!cancelled) { setLoading(false); } } } fetchStatus(); return () => { cancelled = true; }; }, [tokenId]); return { status, loading, error }; } // Usage in component function TokenDisplay({ tokenId }: { tokenId: number }) { const { status, loading, error } = useTokenStatus(tokenId); if (loading) return <div>Checking token status...</div>; if (error) return <div>Error: {error}</div>; if (!status || !status.exists) return <div>Token does not exist</div>; return ( <div> <h3>Token #{tokenId}</h3> <p>Status: {status.used ? 'Used' : 'Available'}</p> {status.used && ( <> <p>Used by: {status.usedBy}</p> <p>Used at: {new Date(status.usedAt!).toLocaleDateString()}</p> </> )} </div> ); }

Batch Status Checking

For checking multiple tokens efficiently:

class TokenStatusBatcher { private queue: Set<number> = new Set(); private results: Map<number, TokenStatus> = new Map(); private timer: NodeJS.Timeout | null = null; private batchDelay = 100; // milliseconds async checkToken(tokenId: number): Promise<TokenStatus | null> { // Return cached result if available if (this.results.has(tokenId)) { return this.results.get(tokenId)!; } // Add to queue this.queue.add(tokenId); // Schedule batch processing if (!this.timer) { this.timer = setTimeout(() => this.processBatch(), this.batchDelay); } // Wait for result return new Promise((resolve) => { const checkResult = setInterval(() => { if (this.results.has(tokenId)) { clearInterval(checkResult); resolve(this.results.get(tokenId)!); } }, 50); }); } private async processBatch() { const tokens = Array.from(this.queue); this.queue.clear(); this.timer = null; // Process tokens with rate limit awareness for (const tokenId of tokens) { try { const response = await fetch(`/api/token-status/${tokenId}`); if (response.ok) { const status = await response.json(); this.results.set(tokenId, status); } else if (response.status === 429) { // Rate limited - reschedule remaining tokens tokens.slice(tokens.indexOf(tokenId)).forEach(id => { this.queue.add(id); }); const retryAfter = parseInt( response.headers.get('Retry-After') || '60' ); this.timer = setTimeout( () => this.processBatch(), retryAfter * 1000 ); break; } } catch (error) { console.error(`Failed to check token ${tokenId}:`, error); } // Small delay between requests to avoid hitting rate limits await new Promise(resolve => setTimeout(resolve, 100)); } } }

Testing Strategies

Thorough testing of the Token Status API ensures reliable integration with your dApp:

Testing Environments

Local Blockchain Testing

The RitoSwap Local Blockchain provides the ideal environment for comprehensive API testing. With instant block times and complete control, you can:

  • Test the full synchronization flow between blockchain and database
  • Mint tokens and immediately verify their status
  • Simulate various edge cases without gas costs
  • Reset the entire state for clean test runs

Set up your local environment following the guide at /local-network, then use these test scenarios:

// Test: Token doesn't exist const response1 = await fetch('/api/token-status/99999'); expect(response1.json()).toMatchObject({ count: 0, exists: false, used: false }); // Test: Mint token and check status await mintToken(1); // Your minting function const response2 = await fetch('/api/token-status/1'); expect(response2.json()).toMatchObject({ count: 1, exists: true, used: false }); // Test: Use token for gating then check await useTokenForGating(1); // Your gating function const response3 = await fetch('/api/token-status/1'); expect(response3.json()).toMatchObject({ count: 1, exists: true, used: true });

Test Checklist

Ensure your integration handles these scenarios correctly:

  • ✅ Valid token IDs return correct status
  • ✅ Invalid token IDs (negative, non-numeric) return 400 errors
  • ✅ Non-existent tokens return exists: false
  • ✅ Rate limiting returns proper headers and retry timing
  • ✅ Database synchronization creates records for on-chain tokens
  • ✅ Network errors are handled gracefully
  • ✅ Polling respects rate limits and backs off appropriately

Comprehensive Test Suite Example

Here’s a complete test suite that demonstrates proper testing patterns for the Token Status API using Vitest. This example showcases mocking strategies, error handling, and verification of the synchronization flow:

/// <reference types="vitest/globals" /> import type { NextRequest } from 'next/server' import type { Mock } from 'vitest' // Module mocks must come before any imports of the route handler vi.mock('viem', () => ({ createPublicClient: vi.fn(), http: vi.fn(), })) vi.mock('@/app/lib/prisma/prismaNetworkUtils', () => ({ getTokenModel: vi.fn(), getChainConfig: vi.fn(), })) vi.mock('@/app/lib/rateLimit/rateLimit.server', () => ({ checkRateLimitWithNonce: vi.fn(), })) vi.mock('@/app/config/contracts', () => ({ fullKeyTokenAbi: [], KEY_TOKEN_ADDRESS: '0xContract', })) import { GET } from '../route' import { createPublicClient } from 'viem' import { getTokenModel, getChainConfig } from '@/app/lib/prisma/prismaNetworkUtils' import { checkRateLimitWithNonce } from '@/app/lib/rateLimit/rateLimit.server' import { fullKeyTokenAbi, KEY_TOKEN_ADDRESS } from '@/app/config/contracts' describe('GET /api/token-status/[tokenId]', () => { let tokenModel: { findUnique: Mock findMany: Mock upsert: Mock } let mockClient: { readContract: Mock } const now = Date.now() beforeAll(() => { // Create a mock blockchain client once for all tests mockClient = { readContract: vi.fn() } vi.mocked(createPublicClient).mockReturnValue(mockClient as any) }) beforeEach(() => { // Default: pass rate limit check vi.mocked(checkRateLimitWithNonce).mockResolvedValue({ success: true, limit: 60, remaining: 60, }) // Set up database model mock tokenModel = { findUnique: vi.fn(), findMany: vi.fn(), upsert: vi.fn(), } vi.mocked(getTokenModel).mockReturnValue(tokenModel as any) // Mock chain configuration vi.mocked(getChainConfig).mockReturnValue({ chain: { id: 1, name: 'TestChain', network: 'testchain', nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: ['https://rpc.example.com'] }, public: { http: ['https://rpc.example.com'] }, }, }, transport: 'https://rpc.example.com', } as any) mockClient.readContract.mockReset() }) afterAll(() => { vi.restoreAllMocks() }) // Helper function to create requests function makeRequest(tokenId: string) { const req = {} as NextRequest return GET(req, { params: { tokenId } }) } it('returns 400 for invalid token IDs', async () => { // Test various invalid inputs for (const badId of ['foo', '-1', '']) { const res = await makeRequest(badId) expect(res.status).toBe(400) expect(await res.json()).toEqual({ error: 'Invalid token ID' }) } }) it('returns 429 when rate limit is exceeded', async () => { const resetTs = now + 90 * 1000 // 90 seconds from now vi.mocked(checkRateLimitWithNonce).mockResolvedValueOnce({ success: false, limit: 5, remaining: 0, reset: resetTs, }) const res = await makeRequest('1') const retryAfter = Math.ceil((resetTs - now) / 1000) expect(res.status).toBe(429) expect(res.headers.get('X-RateLimit-Limit')).toBe('5') expect(res.headers.get('X-RateLimit-Remaining')).toBe('0') expect(Number(res.headers.get('Retry-After'))).toBe(retryAfter) expect(await res.json()).toEqual({ error: 'Too many requests', limit: 5, remaining: 0, retryAfter, }) }) it('returns database record when token exists in DB', async () => { // Mock a token that exists in the database const dbRecord = { tokenId: 7, used: false, usedBy: 'Bob', usedAt: '2025-07-01T08:00:00Z', } tokenModel.findUnique.mockResolvedValueOnce(dbRecord) const res = await makeRequest('7') const body = await res.json() // Verify only database was queried, not blockchain expect(tokenModel.findUnique).toHaveBeenCalledWith({ where: { tokenId: 7 } }) expect(mockClient.readContract).not.toHaveBeenCalled() expect(tokenModel.findMany).not.toHaveBeenCalled() expect(tokenModel.upsert).not.toHaveBeenCalled() // Verify response matches database record expect(body).toEqual({ count: 1, exists: true, used: false, usedBy: 'Bob', usedAt: '2025-07-01T08:00:00Z', }) }) it('checks blockchain and creates DB record when token not in database', async () => { // Token not in database tokenModel.findUnique.mockResolvedValueOnce(null) // Debug query shows existing tokens tokenModel.findMany.mockResolvedValueOnce([{ tokenId: 3 }]) // Token exists on blockchain mockClient.readContract.mockResolvedValueOnce('uri://3') // Database upsert creates new record tokenModel.upsert.mockResolvedValueOnce({ tokenId: 3, used: false, usedBy: null, usedAt: null, }) const res = await makeRequest('3') const body = await res.json() // Verify the complete flow expect(tokenModel.findUnique).toHaveBeenCalledWith({ where: { tokenId: 3 } }) expect(tokenModel.findMany).toHaveBeenCalledWith({ take: 10, orderBy: { tokenId: 'asc' }, }) expect(mockClient.readContract).toHaveBeenCalledWith({ address: KEY_TOKEN_ADDRESS, abi: fullKeyTokenAbi, functionName: 'tokenURI', args: [BigInt(3)], }) expect(tokenModel.upsert).toHaveBeenCalledWith({ where: { tokenId: 3 }, update: {}, // Don't update if it already exists create: { tokenId: 3, used: false }, }) // Verify response for newly synchronized token expect(body).toEqual({ count: 1, exists: true, used: false, usedBy: null, usedAt: null, }) }) it('returns "not found" when token exists neither in DB nor on-chain', async () => { // Not in database tokenModel.findUnique.mockResolvedValueOnce(null) tokenModel.findMany.mockResolvedValueOnce([]) // Not on blockchain (readContract reverts) mockClient.readContract.mockRejectedValueOnce(new Error('Token does not exist')) const res = await makeRequest('10') expect(await res.json()).toEqual({ count: 0, exists: false, used: false, usedBy: null, usedAt: null, }) }) it('returns 500 when unexpected errors occur', async () => { // Simulate database connection failure vi.mocked(getTokenModel).mockImplementationOnce(() => { throw new Error('DB connection failed') }) const res = await makeRequest('4') expect(res.status).toBe(500) expect(await res.json()).toEqual({ error: 'Failed to check token status', }) }) })

This test suite demonstrates several key testing principles for the Token Status API:

  1. Proper Mock Setup - All external dependencies are mocked before importing the route handler to ensure clean test isolation
  2. Comprehensive Coverage - Tests cover all code paths including success cases, validation errors, rate limiting, and database synchronization
  3. Edge Case Testing - Includes tests for invalid inputs, missing tokens, and error conditions
  4. Flow Verification - Confirms the proper sequence of database lookup, blockchain verification, and record creation
  5. Helper Functions - Uses makeRequest helper to reduce boilerplate and improve test readability

The tests verify not only what the API returns but also what it doesn’t do. For example, when a token exists in the database, the test confirms that no blockchain queries are made, ensuring optimal performance. This level of detail helps developers understand the expected behavior and catch regressions early in the development process.

Performance Considerations

The Token Status API is designed for high-frequency access, but proper implementation ensures optimal performance:

Caching Strategies

Consider implementing client-side caching to reduce API calls:

class TokenStatusCache { private cache: Map<number, { status: TokenStatus; timestamp: number }> = new Map(); private ttl: number = 30000; // 30 seconds async getStatus(tokenId: number): Promise<TokenStatus | null> { // Check cache first const cached = this.cache.get(tokenId); if (cached && Date.now() - cached.timestamp < this.ttl) { return cached.status; } // Fetch fresh data try { const response = await fetch(`/api/token-status/${tokenId}`); if (response.ok) { const status = await response.json(); // Cache the result this.cache.set(tokenId, { status, timestamp: Date.now() }); // Clear cache for used tokens more frequently if (status.used) { this.ttl = 300000; // 5 minutes for used tokens } return status; } } catch (error) { console.error('Failed to fetch token status:', error); } return null; } invalidate(tokenId: number) { this.cache.delete(tokenId); } clear() { this.cache.clear(); } }

Optimizing Polling Intervals

Balance responsiveness with resource usage:

  • Initial load: Check immediately when component mounts
  • Active tokens: Poll every 5 seconds for unused tokens
  • Used tokens: Reduce polling to every 30 seconds or stop entirely
  • Background tabs: Pause polling when tab is not visible
document.addEventListener('visibilitychange', () => { if (document.hidden) { poller.pause(); } else { poller.resume(); } });

Summary

The Token Status API provides a critical bridge between on-chain token existence and off-chain usage tracking in the RitoSwap ecosystem. By implementing intelligent caching, respecting rate limits, and leveraging the database synchronization features, developers can create responsive interfaces that accurately reflect token states in real-time.

The API’s design philosophy prioritizes performance for frequent polling while maintaining data consistency through automatic synchronization. Whether you’re building status displays, implementing gating logic, or creating portfolio views, this endpoint provides the reliable, real-time data foundation your dApp needs to deliver an exceptional user experience.