QUEST VERIFICATION API

Launch Token Campaigns

Verify if a user has swapped your custom token, how many times they swapped, and how much volume in USD they traded. Perfect for building custom quests on Galxe, Layer3, Zealy, or your own event system.

How it works under the hood

Our DEX records every swap routed through the liquidity aggregator on-chain. When a user trades, the subgraph indexes swaps from two sources:

  1. Core Aggregator swaps: Triggered directly via the Aggregator Entrypoint Contract (0x5E19EB2A6BA30892CCe33f93D7Fb24D498512266), emitting standard AGGFlowSwap events.
  2. Wrapper & Point Strategy swaps: Triggered via the Wrapper Aggregator Contract (0xf664b56933f3cf0d7d69982b5a8ec9101b80059d), which wraps core aggregation and emits dedicated SwapExecuted events.
  3. Our Goldsky Subgraph Indexer monitors both sources, extracts the real user's wallet address from the event parameters, and updates their corresponding trading histories and volumes.
  4. The Quest Verification API queries this subgraph indexer directly for a given userAddress and tokenAddress to return cumulative swap counts, target asset volumes, and transaction proofs instantly.

API Endpoint Specs

Access the endpoint directly via HTTP. It supports both GET and POST requests, and has CORS enabled by default to allow calling it directly from your web applications or external credential engines.

GET

Verify Swap & Volume Status

https://LitvmSwap.com/api/verify-swaps?address=[wallet_address]&token=[your_token_address]&minSwaps=[count]&limit=[limit]&counterToken=[counter_token_address]

Request Parameters

ParameterTypeRequirementDescription
addressStringRequiredThe user's EVM wallet address to verify (case-insensitive).
tokenStringRequiredYour token's deployed contract address on LitVM. Replacement is required to verify your own custom token (e.g. token=0xYourCustomTokenAddress).
minSwapsNumberOptionalThe swap count threshold for the quest. Default is 1. Returns isEligible: true if cumulative swaps exceed this.
limitNumberOptionalLimits the size of the returned transaction history array (proofs.swaps). Default is 1000, hard-capped at 1000.
counterTokenStringOptionalPass a contract address to filter user swaps and volume specifically against this counter-token (e.g., pass the zero address 0x0000000000000000000000000000000000000000 to verify ZKUSDC swaps made specifically against native zkLTC).

How to Use the Limit Parameter

A user can swap at any time and as many times as they want. To keep response times fast and payloads lightweight, you can pass the limit parameter.

Note on Limits: Setting limit=5 tells the API to only return details for the user's last 5 swap transaction proofs. The total cumulative swap counts (e.g., if they swapped 10,000 times) are never truncated and remain 100% accurate in the totalSwaps property.

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.

⚠️ DIA Oracle USD Pricing Limitation: We use official DIA Oracle price feeds on-chain to provide live USD conversions. These price feeds only support major assets (such as zkLTC, wzkLTC, ETH, BTC, ZKUSDC, and ZKUSDT). For newly launched custom tokens, the API cannot fetch a USD price feed, so totalVolumeUSD will default to $0. For custom token campaigns, clients must verify volume based on totalVolumeToken (raw tokens swapped) instead of USD.

Querying Volume Against Specific Tokens: You can verify swap volumes specifically against a target counter-asset (e.g., checking how much volume of Token X was swapped specifically against zkLTC or ZKUSDC) by passing the optional counterToken address. The API will dynamically sum swap transaction proofs on-chain for only that specific pair and return the results.

How to Design a Great Quest

Building a campaign that drives high-quality engagement and shields you from bots requires good quest design. Here are our recommendations:

🛡️ Implement Sybil Resistance (Volume Checks)

Bots can easily script 10 swaps of $0.0001 to pass a simple swap-count quest. To ensure you reward real users, verify that their totalVolumeUSD is at least $10 or $20.

🏆 Create Multi-Tier Quests

Motivate users to keep swapping by offering progressive tier rewards:
  • Bronze Tier: 1 Swap (Entry level)
  • Silver Tier: 5 Swaps OR $50 Volume (Active user)
  • Gold Tier: 15 Swaps AND $200 Volume (Super power user)

🔗 Display On-Chain Proofs to Users

When a user completes a quest, read the proofs.swaps list returned by the API and display their transaction hashes directly on your quest page. Link them to the explorer: https://liteforge.explorer.caldera.xyz/tx/[txHash] to make verification feel rewarding and transparent.

JSON Response Structure

Below is a typical response payload returned by the endpoint:

API JSON Response Example
{
  "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
    },
    "totalVolumeUSD": 2207418.02,
    "minSwapsRequired": 3,
    "isEligible": true,
    "remainingSwapsNeeded": 0
  },
  "proofs": {
    "swapCount": 16,
    "swaps": [
      {
        "id": "0x146402802f52410254d708619404d6cf5adef9ae1d9fbe7db80582a86d83586d-16",
        "txHash": "0x146402802f52410254d708619404d6cf5adef9ae1d9fbe7db80582a86d83586d",
        "type": "sell",
        "timestamp": 1778439711,
        "dateTime": "2026-05-10T19:01:51.000Z",
        "amountIn": "10000000000000000000",
        "amountOut": "2592967744689044163214"
      }
    ]
  },
  "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:

Node.js Integration Code
// Example: Verifying quest eligibility in Node.js
const axios = require('axios');

async function verifyQuest(userAddress, customTokenAddress, requiredSwaps = 3) {
  try {
    const url = `${window.location.origin}/api/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} 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('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 (e.g. using Wagmi or RainbowKit).

How it works on the frontend: The component calls the local /api/verify-swaps route, passing the user's wallet address and your token's address. It parses the returned JSON, checks if the user has swaps (totalSwaps > 0), and lists their transaction proofs as clickable explorer links.

TypeScript React Component (QuestVerifier.tsx)
import React, { useState } from 'react';

// 1. 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;
}

// 2. React + TypeScript Component
export const QuestVerifier: 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/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 Campaign 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 LitVM...' : '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.slice(0, 10)})
                  </li>
                ))}
              </ul>
            </div>
          )}
        </div>
      )}
    </div>
  );
};

Need support or custom indexing rules? Contact the LitvmSwap core dev team.