DocumentationOpen App

Backend Services

Build indexers, analytics pipelines, and data warehouses that track Gearbox protocol state over time.

Overview

Backend services typically need to:

  1. Capture historical snapshots at specific blocks
  2. Index events for efficient state reconstruction
  3. Track rates, values, and utilization over time
  4. Store and query historical data

This guide shows patterns for each requirement.


Historical Snapshots

WHY: Track how protocol state changes over time for analytics, reporting, and historical queries.

What to Snapshot

DataSourceChange Frequency
Pool ratesPoolStateEvery block with activity
Pool liquidityPoolStateEvery deposit/borrow
Quota utilizationQuotaKeeperStateEvery position change
Credit account stateCreditAccountDataEvery account operation
Token pricesPriceOracleExternal feed updates

How to Query at Specific Blocks

Compressors support querying at historical blocks using viem's blockTag or blockNumber:

TypeScript
import { marketCompressorAbi, AP_MARKET_COMPRESSOR, VERSION_RANGE_310, } from '@gearbox-protocol/sdk'; const [compressor] = sdk.addressProvider.mustGetLatest( AP_MARKET_COMPRESSOR, VERSION_RANGE_310 ); // Query at specific block const historicalData = await client.readContract({ address: compressor, abi: marketCompressorAbi, functionName: 'getMarketData', args: [poolAddress], blockNumber: 19000000n, // Specific block }); console.log(`Pool state at block 19000000:`); console.log(` Available liquidity: ${historicalData.pool.availableLiquidity}`); console.log(` Supply rate: ${historicalData.pool.supplyRate}`);

Archive Node Requirements

Historical queries require an archive node. Standard nodes only keep recent state (~128 blocks).

RPC providers with archive access:

  • Alchemy (archive add-on)
  • Infura (archive add-on)
  • QuickNode (archive plans)
  • Self-hosted Erigon/Reth

Snapshot Pattern

TypeScript
interface PoolSnapshot { blockNumber: bigint; timestamp: number; availableLiquidity: bigint; totalAssets: bigint; supplyRate: bigint; borrowRate: bigint; } async function capturePoolSnapshot( blockNumber: bigint ): Promise<PoolSnapshot> { const block = await client.getBlock({ blockNumber }); const marketData = await client.readContract({ address: compressor, abi: marketCompressorAbi, functionName: 'getMarketData', args: [poolAddress], blockNumber, }); return { blockNumber, timestamp: Number(block.timestamp), availableLiquidity: marketData.pool.availableLiquidity, totalAssets: marketData.pool.totalAssets, supplyRate: marketData.pool.supplyRate, borrowRate: marketData.pool.baseInterestRate, }; } // Capture hourly snapshots const BLOCKS_PER_HOUR = 300n; // ~12 second blocks let currentBlock = startBlock; while (currentBlock <= endBlock) { const snapshot = await capturePoolSnapshot(currentBlock); await saveToDatabase(snapshot); currentBlock += BLOCKS_PER_HOUR; }

Event Indexing

WHY: Events provide efficient tracking of specific state changes without polling.

Key Events

Credit Facade emits events for all account operations:

EventWhen EmittedKey Data
OpenCreditAccountAccount openedowner, creditAccount, borrowAmount
CloseCreditAccountAccount closedcreditAccount
LiquidateCreditAccountAccount liquidatedcreditAccount, liquidator, remainingFunds
StartMultiCallMulticall beginscreditAccount
FinishMultiCallMulticall endscreditAccount

Pool emits events for liquidity changes:

EventWhen EmittedKey Data
DepositLP depositssender, owner, assets, shares
WithdrawLP withdrawssender, receiver, assets, shares
BorrowCredit Manager borrowscreditAccount, amount
RepayDebt repaidcreditAccount, amount, profit, loss

Watching Events with viem

TypeScript
import { parseAbiItem } from 'viem'; // Watch for new credit accounts const unwatchOpen = client.watchContractEvent({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'OpenCreditAccount', onLogs: async (logs) => { for (const log of logs) { console.log(`New account: ${log.args.creditAccount}`); console.log(` Owner: ${log.args.owner}`); console.log(` Initial debt: ${log.args.borrowAmount}`); await indexCreditAccount(log); } }, }); // Watch for liquidations const unwatchLiquidate = client.watchContractEvent({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'LiquidateCreditAccount', onLogs: async (logs) => { for (const log of logs) { console.log(`Liquidated: ${log.args.creditAccount}`); console.log(` Liquidator: ${log.args.liquidator}`); await recordLiquidation(log); } }, });

Fetching Historical Events

For backfilling, fetch events in block ranges:

TypeScript
async function fetchHistoricalEvents( fromBlock: bigint, toBlock: bigint ) { // Fetch in chunks to avoid RPC limits const CHUNK_SIZE = 10000n; let current = fromBlock; while (current <= toBlock) { const chunkEnd = current + CHUNK_SIZE > toBlock ? toBlock : current + CHUNK_SIZE; const logs = await client.getContractEvents({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'OpenCreditAccount', fromBlock: current, toBlock: chunkEnd, }); for (const log of logs) { await processEvent(log); } current = chunkEnd + 1n; } }

State Tracking

WHY: Build complete account or pool history over time by combining events and snapshots.

Credit Account Lifecycle

Track an account from open to close:

TypeScript
interface AccountHistory { creditAccount: string; owner: string; openBlock: bigint; closeBlock: bigint | null; operations: AccountOperation[]; } interface AccountOperation { blockNumber: bigint; txHash: string; type: 'open' | 'multicall' | 'liquidate' | 'close'; healthFactorAfter?: bigint; } async function trackAccountLifecycle(creditAccount: string): Promise<AccountHistory> { // Find open event const openEvents = await client.getContractEvents({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'OpenCreditAccount', args: { creditAccount }, fromBlock: 0n, toBlock: 'latest', }); const openEvent = openEvents[0]; // Find all multicall events const multicallEvents = await client.getContractEvents({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'FinishMultiCall', args: { creditAccount }, fromBlock: openEvent.blockNumber, toBlock: 'latest', }); // Find close event (if any) const closeEvents = await client.getContractEvents({ address: creditFacadeAddress, abi: creditFacadeAbi, eventName: 'CloseCreditAccount', args: { creditAccount }, fromBlock: openEvent.blockNumber, toBlock: 'latest', }); return { creditAccount, owner: openEvent.args.owner, openBlock: openEvent.blockNumber, closeBlock: closeEvents[0]?.blockNumber ?? null, operations: [ { blockNumber: openEvent.blockNumber, txHash: openEvent.transactionHash, type: 'open' }, ...multicallEvents.map(e => ({ blockNumber: e.blockNumber, txHash: e.transactionHash, type: 'multicall' as const, })), ...(closeEvents[0] ? [{ blockNumber: closeEvents[0].blockNumber, txHash: closeEvents[0].transactionHash, type: 'close' as const, }] : []), ].sort((a, b) => Number(a.blockNumber - b.blockNumber)), }; }

Combining Events and Snapshots

For complete state reconstruction:

TypeScript
async function reconstructAccountStateAtBlock( creditAccount: string, targetBlock: bigint ): Promise<CreditAccountData | null> { // Check if account existed at this block const history = await trackAccountLifecycle(creditAccount); if (history.openBlock > targetBlock) { return null; // Account didn't exist yet } if (history.closeBlock && history.closeBlock <= targetBlock) { return null; // Account was closed } // Query compressor at target block const [accountData] = await client.readContract({ address: accountCompressor, abi: creditAccountCompressorAbi, functionName: 'getCreditAccountData', args: [creditManagerAddress, creditAccount], blockNumber: targetBlock, }); return accountData; }

Rate History

WHY: Analytics on yield, utilization trends, and rate changes over time.

Rates to Track

RateSourceNotes
Supply APYpool.supplyRateRAY scaled (10^27)
Base borrow APRpool.baseInterestRateRAY scaled
Quota ratesquotaKeeper.tokens[].ratePer-token, RAY scaled
UtilizationCalculated(totalAssets - availableLiquidity) / totalAssets

Polling Pattern

TypeScript
interface RateSnapshot { blockNumber: bigint; timestamp: number; supplyRate: bigint; borrowRate: bigint; utilization: number; quotaRates: Map<string, bigint>; } async function pollRates(): Promise<RateSnapshot> { const block = await client.getBlock({ blockTag: 'latest' }); const marketData = await client.readContract({ address: compressor, abi: marketCompressorAbi, functionName: 'getMarketData', args: [poolAddress], }); const pool = marketData.pool; const borrowed = pool.totalAssets - pool.availableLiquidity; const utilization = pool.totalAssets > 0n ? Number(borrowed * 10000n / pool.totalAssets) / 100 : 0; const quotaRates = new Map<string, bigint>(); for (const token of marketData.quotaKeeper.tokens) { quotaRates.set(token.token, token.rate); } return { blockNumber: block.number, timestamp: Number(block.timestamp), supplyRate: pool.supplyRate, borrowRate: pool.baseInterestRate, utilization, quotaRates, }; } // Poll every minute setInterval(async () => { const snapshot = await pollRates(); await saveRateSnapshot(snapshot); }, 60_000);

Rate Conversion

Convert RAY-scaled rates to annual percentages:

TypeScript
const RAY = 10n ** 27n; function rayToAnnualPercent(rayRate: bigint): number { // rate is per-second, annualize it const SECONDS_PER_YEAR = 365n * 24n * 60n * 60n; const annualRate = rayRate * SECONDS_PER_YEAR; return Number(annualRate * 10000n / RAY) / 100; } const supplyAPY = rayToAnnualPercent(pool.supplyRate); console.log(`Supply APY: ${supplyAPY.toFixed(2)}%`);

Complete Example: Simple Indexer

TypeScript
import { createPublicClient, http } from 'viem'; import { mainnet } from 'viem/chains'; import { GearboxSDK, marketCompressorAbi, creditAccountCompressorAbi, AP_MARKET_COMPRESSOR, VERSION_RANGE_310, } from '@gearbox-protocol/sdk'; interface IndexerState { lastIndexedBlock: bigint; pools: Map<string, PoolData>; accounts: Map<string, AccountData>; } async function runIndexer( startBlock: bigint, poolAddress: `0x${string}`, creditManagerAddress: `0x${string}` ) { const client = createPublicClient({ chain: mainnet, transport: http(process.env.ARCHIVE_RPC_URL), }); const sdk = await GearboxSDK.attach({ client, marketConfigurators: [], }); const [marketCompressor] = sdk.addressProvider.mustGetLatest( AP_MARKET_COMPRESSOR, VERSION_RANGE_310 ); let currentBlock = startBlock; while (true) { const latestBlock = await client.getBlockNumber(); while (currentBlock <= latestBlock) { // Snapshot pool state const marketData = await client.readContract({ address: marketCompressor, abi: marketCompressorAbi, functionName: 'getMarketData', args: [poolAddress], blockNumber: currentBlock, }); await savePoolSnapshot(currentBlock, marketData.pool); // Index events in this block range const events = await client.getContractEvents({ address: creditManagerAddress, abi: creditFacadeAbi, fromBlock: currentBlock, toBlock: currentBlock + 100n, }); for (const event of events) { await processEvent(event); } currentBlock += 100n; } // Wait for new blocks await sleep(12_000); } } function sleep(ms: number): Promise<void> { return new Promise(resolve => setTimeout(resolve, ms)); }

Next Steps