Launch DB-Backed Campaigns
Verify if a user has swapped your custom token, how many times they swapped, and how much volume they traded. Backed by your own self-hosted real-time WebSocket indexer and MongoDB database. Faster and more reliable than subgraphs.
How it works under the hood
This API endpoint reads directly from your private MongoDB database which is kept in sync with the blockchain in real-time by a background indexer daemon:
- Your background daemon (
scripts/event-indexer.mjs) connects to the RPC node via WebSockets. - It listens to the
AGGFlowSwapevent from the aggregator router contract (0x5E19EB2A6BA30892CCe33f93D7Fb24D498512266). - All swaps are recorded in your MongoDB
transactionsanduser_statscollections immediately. - The API route (
/api/db-verify-swaps) queries MongoDB and returns cumulative swap stats, volumes, and tx hashes in milliseconds without any Goldsky or Graph protocol dependency.
API Endpoint Specs
Access the endpoint directly via HTTP. It supports both GET and POST requests, and has CORS enabled by default.
Verify Swap & Volume Status
https://LitvmSwap.com/api/db-verify-swaps?address=[wallet_address]&token=[your_token_address]&minSwaps=[count]&limit=[limit]&counterToken=[counter_token_address]Request Parameters
| Parameter | Type | Requirement | Description |
|---|---|---|---|
address | String | Required | The user's EVM wallet address to verify (case-insensitive). |
token | String | Required | Your token's deployed contract address on LitVM. Filters user swaps to this specific asset. |
minSwaps | Number | Optional | The swap count threshold for the quest. Default is 1. Returns isEligible: true if cumulative swaps exceed this. |
limit | Number | Optional | Limits the size of the returned transaction history array (proofs.swaps). Default is 1000, hard-capped at 1000. |
counterToken | String | Optional | Pass a contract address to filter user swaps and volume specifically against this counter-token (e.g., pass the wrapped native address to verify swaps made specifically against native zkLTC). |
Checking Volume & DIA Oracle Pricing
To check verification for your own token, replace the token parameter in the HTTP query with your token's deployed contract address.
$0. For custom token campaigns, clients must verify volume based on totalVolumeToken (raw tokens swapped) instead of USD.JSON Response Structure
Below is a typical response payload returned by the endpoint:
{
"success": true,
"address": "0x23d542dcefb00b1f4268e67a0ec1ef4de0a58fe2",
"token": {
"address": "0xdf69970b2fe416339187aa41d39882e864984ce9",
"symbol": "ZKUSDC",
"name": "ZKUSDC",
"decimals": 18
},
"verification": {
"totalSwaps": 16,
"aggregatorSwaps": {
"total": 16,
"swappedIn": 2,
"swappedOut": 14
},
"directPoolSwaps": {
"total": 0,
"v2": 0,
"v3": 0
},
"totalVolumeToken": 10540.23,
"totalVolumeUSD": 10540.23,
"tokenPriceUSD": 1,
"minSwapsRequired": 3,
"isEligible": true,
"remainingSwapsNeeded": 0
},
"proofs": {
"swapCount": 16,
"swaps": [
{
"id": "64b0f69a53d102e3b2e7a1b4",
"txHash": "0x146402802f52410254d708619404d6cf5adef9ae1d9fbe7db80582a86d83586d",
"type": "sell",
"timestamp": 1778439711,
"dateTime": "2026-05-10T19:01:51.000Z",
"amountIn": "10.0",
"amountInDecimal": 10.0,
"amountOut": "2592.96",
"amountOutDecimal": 0
}
]
},
"timestamp": "2026-07-12T01:42:00.000Z"
}Integration Example
Here is how you can fetch and verify a user's eligibility in your Node.js backend application:
// Example: Verifying quest eligibility in Node.js using Database indexer
const axios = require('axios');
async function verifyQuestWithDB(userAddress, customTokenAddress, requiredSwaps = 3) {
try {
const url = `${window.location.origin}/api/db-verify-swaps`;
const response = await axios.get(url, {
params: {
address: userAddress,
token: customTokenAddress, // Replace with your token contract address
minSwaps: requiredSwaps
}
});
const { success, verification, token } = response.data;
if (success) {
console.log(`Token ${token.symbol} database verification:`);
console.log(`Total swaps user performed: ${verification.totalSwaps}`);
console.log(`Total swap USD volume: $${verification.totalVolumeUSD.toFixed(2)}`);
console.log(`Eligible for Quest: ${verification.isEligible ? 'YES' : 'NO'}`);
return verification.isEligible;
}
return false;
} catch (error) {
console.error('Database quest verification failed:', error.message);
return false;
}
}Frontend Integration (TypeScript & React)
Below is a beginner-friendly TypeScript guide showing how to implement this in a React frontend. The component handles state (loading, data, and errors) and provides the interfaces so you get strong type-checking when integrating with your wallet connection.
import React, { useState } from 'react';
// Define TypeScript Interfaces for the API Response
interface TokenInfo {
address: string;
symbol: string;
name: string;
decimals: number;
}
interface SwapDetails {
id: string;
txHash: string;
type: 'buy' | 'sell';
timestamp: number;
dateTime: string;
amountIn: string;
amountOut: string;
}
interface VerificationStats {
totalSwaps: number;
aggregatorSwaps: {
total: number;
swappedIn: number;
swappedOut: number;
};
directPoolSwaps: {
total: number;
v2: number;
v3: number;
};
totalVolumeToken: number;
totalVolumeUSD: number;
tokenPriceUSD: number;
minSwapsRequired: number;
isEligible: boolean;
remainingSwapsNeeded: number;
}
interface VerifySwapsResponse {
success: boolean;
address: string;
token: TokenInfo;
verification: VerificationStats;
proofs: {
swapCount: number;
swaps: SwapDetails[];
};
timestamp: string;
error?: string;
}
export const DatabaseQuestVerifier: React.FC = () => {
const [userAddress, setUserAddress] = useState<string>('');
const [customToken, setCustomToken] = useState<string>('0xdf69970B2fE416339187aA41D39882e864984CE9');
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<VerifySwapsResponse | null>(null);
const [error, setError] = useState<string | null>(null);
const checkQuestStatus = async () => {
if (!userAddress.startsWith('0x') || userAddress.length !== 42) {
setError('Please enter a valid EVM address starting with 0x.');
return;
}
setLoading(true);
setError(null);
setData(null);
try {
const response = await fetch(
`/api/db-verify-swaps?address=${userAddress}&token=${customToken}&minSwaps=1`
);
const json: VerifySwapsResponse = await response.json();
if (json.success) {
setData(json);
} else {
setError(json.error || 'Failed to verify swaps.');
}
} catch (err: any) {
setError(err.message || 'Network error occurred.');
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: '24px', background: '#0d1117', borderRadius: '12px', border: '1px solid #30363d' }}>
<h3 style={{ color: '#fff', marginTop: 0 }}>DEX Database Swap Verifier</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px', marginBottom: '16px' }}>
<label style={{ color: '#8b949e', fontSize: '14px' }}>User Wallet Address:</label>
<input
type="text"
value={userAddress}
onChange={(e) => setUserAddress(e.target.value)}
placeholder="0x..."
style={{ padding: '8px 12px', background: '#161b22', border: '1px solid #30363d', borderRadius: '6px', color: '#fff' }}
/>
<label style={{ color: '#8b949e', fontSize: '14px' }}>Custom Token Contract Address:</label>
<input
type="text"
value={customToken}
onChange={(e) => setCustomToken(e.target.value)}
placeholder="0x..."
style={{ padding: '8px 12px', background: '#161b22', border: '1px solid #30363d', borderRadius: '6px', color: '#fff' }}
/>
</div>
<button
onClick={checkQuestStatus}
disabled={loading}
style={{ padding: '10px 20px', background: '#238636', border: 'none', borderRadius: '6px', color: '#fff', cursor: 'pointer', fontWeight: 'bold' }}
>
{loading ? 'Verifying on Database...' : 'Verify User Quests'}
</button>
{error && <p style={{ color: '#f85149', marginTop: '16px' }}>⚠️ {error}</p>}
{data && (
<div style={{ marginTop: '20px', borderTop: '1px solid #30363d', paddingTop: '16px' }}>
<h4 style={{ color: '#58a6ff', margin: '0 0 12px 0' }}>Result: {data.verification.totalSwaps > 0 ? '✅ Verified User!' : '❌ Not Swapped Yet'}</h4>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', fontSize: '14px', color: '#c9d1d9' }}>
<div>Total Swaps: <strong>{data.verification.totalSwaps} swaps</strong></div>
<div>Raw Token Volume: <strong>{data.verification.totalVolumeToken.toFixed(2)} {data.token.symbol}</strong></div>
<div>Estimated USD Volume: <strong>${data.verification.totalVolumeUSD.toFixed(2)}</strong></div>
<div>Oracle Price: <strong>${data.verification.tokenPriceUSD.toFixed(4)}</strong></div>
</div>
{data.proofs.swaps.length > 0 && (
<div style={{ marginTop: '16px' }}>
<h5 style={{ color: '#fff', margin: '0 0 8px 0' }}>Transaction Proofs:</h5>
<ul style={{ margin: 0, paddingLeft: '20px', color: '#8b949e', fontSize: '13px' }}>
{data.proofs.swaps.map((tx, idx) => (
<li key={idx} style={{ marginBottom: '6px' }}>
<a
href={`https://liteforge.explorer.caldera.xyz/tx/${tx.txHash}`}
target="_blank"
rel="noreferrer"
style={{ color: '#58a6ff', textDecoration: 'none' }}
>
{tx.txHash.slice(0, 10)}...{tx.txHash.slice(-8)}
</a> (${tx.type === 'buy' ? 'Bought' : 'Sold'} on ${tx.dateTime ? tx.dateTime.slice(0, 10) : 'unknown'})
</li>
))}
</ul>
</div>
)}
</div>
)}
</div>
);
};