Skip to main content
The DeriverseTradeService fetches and parses on-chain trade history from Solana transactions, converting blockchain data into structured Trade objects with PnL, fees, and prices.

Overview

This service uses the @deriverse/kit Engine to decode transaction logs from the Deriverse program, extracting spot and perpetual trade fills with all relevant details.

Architecture

DeriverseTradeService

fetchTradesForWallet

Fetch and parse all Deriverse trades for a wallet address.
async fetchTradesForWallet(
  connection: Connection,
  walletAddress: string
): Promise<Trade[]>
connection
Connection
required
Solana web3.js Connection instance
walletAddress
string
required
Solana wallet address to fetch trades for
result
Trade[]
Array of parsed trades sorted by closedAt descending (most recent first)

Configuration Parameters

The method uses several configuration constants:
ParameterValueDescription
limit1000Maximum transaction signatures to fetch
batchSize5Number of transactions per parallel batch
maxTxs100Maximum transactions to process
delayMs600Delay between batches (rate limiting)

Trade Parsing Logic

Spot Trades
Detected when log message contains: orderId, qty, price, crncy, rebates, side
{
  id: `${txSignature}-${orderId}-spot`,
  symbol: INSTRUMENT_ID_TO_SYMBOL[instrId] ?? `Instr-${instrId}`,
  quoteCurrency: 'USDC',
  side: side === 0 ? 'buy' : 'sell',
  orderType: 'limit',
  quantity: qty / ASSET_DECIMALS,
  price: price / PRICE_DECIMALS,
  notional: (qty / ASSET_DECIMALS) * (price / PRICE_DECIMALS),
  pnl: 0,
  fee: -rebates / QUOTE_DECIMALS,
  feeCurrency: 'quote',
  openedAt: closedAt,
  closedAt: closedAt,
  durationSeconds: 0,
  isWin: false,
  txSignature
}
Perpetual Trades
Detected when log message contains: orderId, perps, price, crncy, rebates, side
{
  id: `${txSignature}-${orderId}-perp`,
  symbol: INSTRUMENT_ID_TO_SYMBOL[instrId] ?? `Instr-${instrId}`,
  quoteCurrency: 'USDC',
  side: side === 0 ? 'long' : 'short',
  orderType: 'limit',
  quantity: Math.abs(perps) / ASSET_DECIMALS,
  price: price / PRICE_DECIMALS,
  notional: (Math.abs(perps) / ASSET_DECIMALS) * (price / PRICE_DECIMALS),
  pnl: crncy / QUOTE_DECIMALS,
  fee: -rebates / QUOTE_DECIMALS,
  feeCurrency: 'quote',
  openedAt: closedAt,
  closedAt: closedAt,
  durationSeconds: 0,
  isWin: (crncy / QUOTE_DECIMALS) > 0,
  txSignature
}

Trade Object Structure

Trade
object
Parsed trade object
id
string
Unique identifier: {txSignature}-{orderId}-{spot|perp}
symbol
string
Trading pair symbol (e.g., ‘BTC-PERP’, ‘SOL-PERP’)
quoteCurrency
string
Quote currency (always ‘USDC’ for Deriverse)
side
'buy' | 'sell' | 'long' | 'short'
Trade side (buy/sell for spot, long/short for perps)
orderType
string
Order type (always ‘limit’ currently)
quantity
number
Trade quantity in base asset units
price
number
Execution price
notional
number
Trade value (quantity * price)
pnl
number
Profit and loss (0 for spot, calculated for perps)
fee
number
Trading fee paid (negative rebates)
feeCurrency
string
Fee currency (always ‘quote’)
openedAt
Date
Position opened timestamp (same as closedAt for now)
closedAt
Date
Position closed timestamp (transaction block time)
durationSeconds
number
Position duration (0 for immediate fills)
isWin
boolean
Whether trade was profitable (false for spot, pnl > 0 for perps)
txSignature
string
Solana transaction signature

Example Usage

Basic Trade Fetching

import { Connection } from '@solana/web3.js';
import { DeriverseTradeService } from '@/services/DeriverseTradeService';

const connection = new Connection(
  process.env.NEXT_PUBLIC_RPC_HTTP!,
  'confirmed'
);

const tradeService = new DeriverseTradeService();
const walletAddress = '7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU';

const trades = await tradeService.fetchTradesForWallet(
  connection,
  walletAddress
);

console.log(`Found ${trades.length} trades`);

// Calculate statistics
const totalPnl = trades.reduce((sum, t) => sum + t.pnl, 0);
const totalFees = trades.reduce((sum, t) => sum + t.fee, 0);
const winningTrades = trades.filter(t => t.isWin).length;
const winRate = (winningTrades / trades.length) * 100;

console.log(`Total PnL: ${totalPnl.toFixed(2)} USDC`);
console.log(`Total Fees: ${totalFees.toFixed(2)} USDC`);
console.log(`Win Rate: ${winRate.toFixed(1)}%`);

Filtering by Symbol

const trades = await tradeService.fetchTradesForWallet(connection, walletAddress);

// Get only BTC perpetual trades
const btcTrades = trades.filter(t => t.symbol === 'BTC-PERP');

// Get only spot trades
const spotTrades = trades.filter(t => t.side === 'buy' || t.side === 'sell');

// Get only perpetual trades
const perpTrades = trades.filter(t => t.side === 'long' || t.side === 'short');

Persisting to Database

import { DeriverseTradeService } from '@/services/DeriverseTradeService';
import { SupabaseTradeService } from '@/services/SupabaseTradeService';

const tradeService = new DeriverseTradeService();
const supabaseService = new SupabaseTradeService();

async function syncTrades(walletAddress: string) {
  // Fetch trades from blockchain
  const trades = await tradeService.fetchTradesForWallet(
    connection,
    walletAddress
  );
  
  // Save to database
  const result = await supabaseService.saveTrades(walletAddress, trades);
  
  console.log(`Saved ${result.saved} trades to database`);
  
  return result;
}

Real-time Dashboard Integration

import { useEffect, useState } from 'react';
import { DeriverseTradeService } from '@/services/DeriverseTradeService';
import { getRpcConnection } from '@/lib/utils';

function TradesPage() {
  const [trades, setTrades] = useState<Trade[]>([]);
  const [loading, setLoading] = useState(true);
  const walletAddress = 'your-wallet-address';
  
  useEffect(() => {
    const fetchTrades = async () => {
      try {
        const connection = getRpcConnection();
        const service = new DeriverseTradeService();
        const result = await service.fetchTradesForWallet(
          connection,
          walletAddress
        );
        setTrades(result);
      } catch (error) {
        console.error('Error fetching trades:', error);
      } finally {
        setLoading(false);
      }
    };
    
    fetchTrades();
  }, [walletAddress]);
  
  if (loading) return <div>Loading trades...</div>;
  
  return (
    <div>
      <h1>Trade History</h1>
      <table>
        <thead>
          <tr>
            <th>Time</th>
            <th>Symbol</th>
            <th>Side</th>
            <th>Quantity</th>
            <th>Price</th>
            <th>PnL</th>
            <th>Fee</th>
          </tr>
        </thead>
        <tbody>
          {trades.map(trade => (
            <tr key={trade.id}>
              <td>{trade.closedAt.toLocaleString()}</td>
              <td>{trade.symbol}</td>
              <td>{trade.side}</td>
              <td>{trade.quantity.toFixed(4)}</td>
              <td>${trade.price.toFixed(2)}</td>
              <td className={trade.pnl >= 0 ? 'positive' : 'negative'}>
                ${trade.pnl.toFixed(2)}
              </td>
              <td>${trade.fee.toFixed(2)}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Constants and Configuration

The service uses these constants from ../lib/constants:
export const PROGRAM_ID = 'deriverse_program_id';
export const DERIVERSE_VERSION = 1;
export const RPC_HTTP = process.env.NEXT_PUBLIC_RPC_HTTP!;

// Decimal scaling factors
export const PRICE_DECIMALS = 1e6;      // 6 decimals
export const ASSET_DECIMALS = 1e9;      // 9 decimals
export const QUOTE_DECIMALS = 1e6;      // 6 decimals (USDC)

// Instrument ID to symbol mapping
export const INSTRUMENT_ID_TO_SYMBOL: Record<number, string> = {
  0: 'BTC-PERP',
  1: 'ETH-PERP',
  2: 'SOL-PERP',
  // ... more mappings
};

Performance Optimization

Batch Processing

Transactions are fetched in batches of 5 with 600ms delays to avoid rate limits:
for (let i = 0; i < Math.min(signatures.length, maxTxs); i += batchSize) {
  if (i > 0) await new Promise(r => setTimeout(r, delayMs));
  const batch = signatures.slice(i, i + batchSize);
  // Process batch in parallel
}

Efficient Filtering

Only processes transactions with program data logs:
if (!tx?.meta?.logMessages?.some(isProgramLog)) continue;

Engine Singleton

Engine is initialized once and reused:
let enginePromise: Promise<Engine> | null = null;

async function getEngine(): Promise<Engine> {
  if (enginePromise) return enginePromise;
  enginePromise = (async () => {
    // Initialize engine
  })();
  return enginePromise;
}

Error Handling

The service handles errors gracefully:
  • Invalid wallet addresses throw errors early
  • Failed transaction fetches are logged and skipped
  • Log decode failures are caught per-transaction
  • Successful trades are returned even if some transactions fail

Build docs developers (and LLMs) love