Filters & Pagination

Master advanced querying patterns including filtering, pagination, and ordering.

Table of Contents


Filter Types

Sai Keeper supports various filter types for different data types.

String Filters

StringFilter supports equality and pattern matching.

input StringFilter {
  eq: String      # Exact match
  like: String    # Pattern match
}

Example - Exact Match:

query SearchToken {
  oracle {
    tokens(where: { name: { eq: "Bitcoin" } }) {
      id
      symbol
      name
    }
  }
}

Example - Pattern Match:

query SearchByPattern {
  oracle {
    tokens(where: { name: { like: "%coin%" } }) {
      symbol
      name
    }
  }
}

Usage in JavaScript:

// Exact match
const { data } = await client.query({
  query: gql`
    query SearchToken($name: String!) {
      oracle {
        tokens(where: { name: $name }) {
          symbol
          name
        }
      }
    }
  `,
  variables: { name: "Bitcoin" }
})

// Pattern search
const searchResults = await client.query({
  query: gql`
    query SearchTokens($pattern: String!) {
      oracle {
        tokens(where: { name: $pattern }) {
          symbol
          name
        }
      }
    }
  `,
  variables: { pattern: "%BTC%" }
})

Integer Filters

IntFilter supports comparison operations.

input IntFilter {
  eq: Int   # Equal to
  gt: Int   # Greater than
  gte: Int  # Greater than or equal
  lt: Int   # Less than
  lte: Int  # Less than or equal
}

Example - Fee Amount Range:

query GetLargeFees {
  fee {
    feeTransactions(
      filter: {
        minAmount: 1000000    # >= 1,000,000
        maxAmount: 10000000   # <= 10,000,000
      }
    ) {
      id
      totalFeeCharged
      traderAddress
    }
  }
}

Usage in JavaScript:

const { data } = await client.query({
  query: gql`
    query GetFeesInRange($min: Int!, $max: Int!) {
      fee {
        feeTransactions(
          filter: {
            minAmount: $min
            maxAmount: $max
          }
        ) {
          id
          totalFeeCharged
        }
      }
    }
  `,
  variables: {
    min: 1000000,
    max: 10000000
  }
})

Float Filters

FloatFilter for floating-point comparisons.

input FloatFilter {
  eq: Float   # Equal to
  gt: Float   # Greater than
  gte: Float  # Greater than or equal
  lt: Float   # Less than
  lte: Float  # Less than or equal
}

Example - Filter by Leverage:

// Note: Direct float filters not used in current schema,
// but filter objects use similar patterns
const highLeverageTrades = await client.query({
  query: gql`
    query GetHighLeverageTrades($trader: String!) {
      perp {
        trades(where: { trader: $trader }) {
          id
          leverage
          isOpen
        }
      }
    }
  `,
  variables: { trader: "nibi1abc..." }
})

// Filter client-side
const filtered = highLeverageTrades.data.perp.trades.filter(
  trade => trade.leverage > 10
)

Time Filters

TimeFilter for date/time range queries.

input TimeFilter {
  eq: Time   # Exact time
  gt: Time   # After
  gte: Time  # After or at
  lt: Time   # Before
  lte: Time  # Before or at
}

Example - Filter by Date Range:

query GetFeesLastMonth {
  fee {
    feeTransactions(
      filter: {
        fromDate: "2024-10-01T00:00:00Z"
        toDate: "2024-10-31T23:59:59Z"
      }
      limit: 100
    ) {
      id
      totalFeeCharged
      blockTime
    }
  }
}

Usage in JavaScript:

// Get fees for last 30 days
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)

const { data } = await client.query({
  query: gql`
    query GetRecentFees($fromDate: Time!, $toDate: Time!) {
      fee {
        feeTransactions(
          filter: {
            fromDate: $fromDate
            toDate: $toDate
          }
          limit: 100
        ) {
          id
          totalFeeCharged
          blockTime
        }
      }
    }
  `,
  variables: {
    fromDate: thirtyDaysAgo.toISOString(),
    toDate: new Date().toISOString()
  }
})

Helper Function:

function getDateRange(days) {
  const toDate = new Date()
  const fromDate = new Date()
  fromDate.setDate(fromDate.getDate() - days)
  
  return {
    fromDate: fromDate.toISOString(),
    toDate: toDate.toISOString()
  }
}

// Usage
const last7Days = getDateRange(7)
const last30Days = getDateRange(30)
const last90Days = getDateRange(90)

Enum Filters

Filter by enum values.

Example - Fee Type Filter:

query GetOpeningFees($trader: String!) {
  fee {
    feeTransactions(
      filter: {
        traderAddress: $trader
        feeType: OPENING
      }
      limit: 50
    ) {
      id
      feeType
      totalFeeCharged
    }
  }
}

Example - Trade Type Filter:

// Get only limit orders
const { data } = await client.query({
  query: gql`
    query GetLimitOrders($trader: String!) {
      perp {
        tradeHistory(where: { trader: $trader }, limit: 100) {
          id
          tradeChangeType
          trade {
            id
            tradeType
          }
        }
      }
    }
  `,
  variables: { trader: "nibi1abc..." }
})

// Filter for limit_order_created events
const limitOrders = data.perp.tradeHistory.filter(
  h => h.tradeChangeType === 'limit_order_created'
)

Pagination

Basic Pagination

Use limit and offset for pagination.

query GetTradesPaginated($trader: String!, $limit: Int!, $offset: Int!) {
  perp {
    trades(
      where: { trader: $trader }
      limit: $limit
      offset: $offset
    ) {
      id
      isOpen
      leverage
    }
  }
}

JavaScript Implementation:

async function fetchPage(trader, page = 0, pageSize = 20) {
  const { data } = await client.query({
    query: GET_TRADES_PAGINATED,
    variables: {
      trader,
      limit: pageSize,
      offset: page * pageSize
    }
  })
  
  return data.perp.trades
}

// Get first page
const firstPage = await fetchPage("nibi1abc...", 0, 20)

// Get second page
const secondPage = await fetchPage("nibi1abc...", 1, 20)

// Get third page
const thirdPage = await fetchPage("nibi1abc...", 2, 20)

Fetch All Pattern

Fetch all results across multiple pages.

async function fetchAllTrades(trader, pageSize = 100) {
  let allTrades = []
  let page = 0
  let hasMore = true
  
  while (hasMore) {
    const { data } = await client.query({
      query: gql`
        query GetTrades($trader: String!, $limit: Int!, $offset: Int!) {
          perp {
            trades(
              where: { trader: $trader }
              limit: $limit
              offset: $offset
            ) {
              id
              isOpen
              leverage
            }
          }
        }
      `,
      variables: {
        trader,
        limit: pageSize,
        offset: page * pageSize
      }
    })
    
    const trades = data.perp.trades
    allTrades = allTrades.concat(trades)
    
    hasMore = trades.length === pageSize
    page++
    
    console.log(`Fetched page ${page}, total: ${allTrades.length}`)
  }
  
  return allTrades
}

// Usage
const allTrades = await fetchAllTrades("nibi1abc...")
console.log(`Total trades: ${allTrades.length}`)

Cursor-Based Pagination Alternative

While Sai Keeper uses offset pagination, you can implement cursor-based logic:

async function fetchTradesAfterBlock(trader, afterBlock = 0, limit = 100) {
  const { data } = await client.query({
    query: gql`
      query GetTrades($trader: String!, $limit: Int!) {
        perp {
          trades(
            where: { trader: $trader }
            limit: $limit
            order_desc: false
          ) {
            id
            openBlock {
              block
            }
          }
        }
      }
    `,
    variables: { trader, limit }
  })
  
  // Filter trades after cursor block
  const filtered = data.perp.trades.filter(
    trade => trade.openBlock.block > afterBlock
  )
  
  return filtered
}

// Get first batch
const firstBatch = await fetchTradesAfterBlock("nibi1abc...", 0)
const lastBlock = firstBatch[firstBatch.length - 1].openBlock.block

// Get next batch
const nextBatch = await fetchTradesAfterBlock("nibi1abc...", lastBlock)

Infinite Scroll Pattern

Implement infinite scroll for UI:

class TradesPaginator {
  constructor(client, trader, pageSize = 20) {
    this.client = client
    this.trader = trader
    this.pageSize = pageSize
    this.currentPage = 0
    this.allTrades = []
    this.hasMore = true
  }
  
  async loadMore() {
    if (!this.hasMore) {
      return []
    }
    
    const { data } = await this.client.query({
      query: gql`
        query GetTrades($trader: String!, $limit: Int!, $offset: Int!) {
          perp {
            trades(
              where: { trader: $trader }
              limit: $limit
              offset: $offset
              order_desc: true
            ) {
              id
              isOpen
              leverage
            }
          }
        }
      `,
      variables: {
        trader: this.trader,
        limit: this.pageSize,
        offset: this.currentPage * this.pageSize
      }
    })
    
    const trades = data.perp.trades
    this.allTrades = this.allTrades.concat(trades)
    this.hasMore = trades.length === this.pageSize
    this.currentPage++
    
    return trades
  }
  
  reset() {
    this.currentPage = 0
    this.allTrades = []
    this.hasMore = true
  }
  
  getAll() {
    return this.allTrades
  }
}

// Usage
const paginator = new TradesPaginator(client, "nibi1abc...", 20)

// Load first page
const firstPage = await paginator.loadMore()

// Load more on scroll
window.addEventListener('scroll', async () => {
  if (isNearBottom() && paginator.hasMore) {
    const nextPage = await paginator.loadMore()
    appendToUI(nextPage)
  }
})

Ordering

Order Direction

Most queries support order_by and order_desc parameters.

query GetTradesOrdered($trader: String!) {
  perp {
    trades(
      where: { trader: $trader }
      order_by: sequence
      order_desc: true  # Descending (newest first)
    ) {
      id
      openBlock { block_ts }
    }
  }
}

Order Options by Domain:

Perp Trades:

  • sequence - Order by sequence number

Trade History:

  • sequence - Order by sequence

  • trade_id - Order by trade ID

LP Deposits:

  • depositor - Order by depositor address

  • vault - Order by vault address

LP Deposit History:

  • depositor - Order by depositor

  • sequence - Order by sequence

  • vault - Order by vault

LP Withdraw Requests:

  • depositor - Order by depositor

  • unlock_epoch - Order by unlock epoch

  • vault - Order by vault

Oracle Tokens:

  • id - Order by token ID

  • name - Order by token name

  • permission_group - Order by permission group

Fee Transactions:

  • No explicit ordering parameter, results ordered by ID


Sorting Examples

Newest First:

const { data } = await client.query({
  query: gql`
    query GetRecentTrades($trader: String!) {
      perp {
        trades(
          where: { trader: $trader }
          order_desc: true
          limit: 20
        ) {
          id
          openBlock { block_ts }
        }
      }
    }
  `,
  variables: { trader: "nibi1abc..." }
})

Oldest First:

const { data } = await client.query({
  query: gql`
    query GetOldestTrades($trader: String!) {
      perp {
        trades(
          where: { trader: $trader }
          order_desc: false
          limit: 20
        ) {
          id
          openBlock { block_ts }
        }
      }
    }
  `,
  variables: { trader: "nibi1abc..." }
})

By Token Name:

const { data } = await client.query({
  query: gql`
    query GetTokensAlphabetically {
      oracle {
        tokens(
          order_by: name
          order_desc: false
        ) {
          symbol
          name
        }
      }
    }
  `
})

Advanced Patterns

Combined Filters

Combine multiple filters for precise queries.

const { data } = await client.query({
  query: gql`
    query GetFilteredFees(
      $trader: String!
      $feeType: FeeType!
      $fromDate: Time!
      $toDate: Time!
      $minAmount: Int!
    ) {
      fee {
        feeTransactions(
          filter: {
            traderAddress: $trader
            feeType: $feeType
            fromDate: $fromDate
            toDate: $toDate
            minAmount: $minAmount
          }
          limit: 100
        ) {
          id
          totalFeeCharged
          blockTime
        }
      }
    }
  `,
  variables: {
    trader: "nibi1abc...",
    feeType: "OPENING",
    fromDate: "2024-01-01T00:00:00Z",
    toDate: "2024-12-31T23:59:59Z",
    minAmount: 1000000
  }
})

Client-Side Post-Filtering

When server-side filters aren't available, filter client-side:

// Fetch all trades
const { data } = await client.query({
  query: gql`
    query GetAllTrades($trader: String!) {
      perp {
        trades(where: { trader: $trader }, limit: 1000) {
          id
          leverage
          isLong
          isOpen
          collateralAmount
          state {
            pnlPct
          }
          perpBorrowing {
            baseToken { symbol }
          }
        }
      }
    }
  `,
  variables: { trader: "nibi1abc..." }
})

// Filter for high-leverage profitable longs
const filtered = data.perp.trades.filter(trade => 
  trade.isLong &&
  trade.leverage > 10 &&
  trade.state.pnlPct > 0.1 && // 10% profit
  trade.isOpen
)

// Group by token
const byToken = filtered.reduce((acc, trade) => {
  const symbol = trade.perpBorrowing.baseToken.symbol
  if (!acc[symbol]) acc[symbol] = []
  acc[symbol].push(trade)
  return acc
}, {})

console.log('Profitable high-leverage longs by token:', byToken)

Search Implementation

Build a search feature:

async function searchTrades(trader, searchTerm) {
  // Fetch all trades
  const { data } = await client.query({
    query: gql`
      query GetTrades($trader: String!) {
        perp {
          trades(where: { trader: $trader }) {
            id
            isLong
            leverage
            perpBorrowing {
              baseToken {
                symbol
                name
              }
              marketId
            }
          }
        }
      }
    `,
    variables: { trader }
  })
  
  // Search by token symbol or name
  const searchLower = searchTerm.toLowerCase()
  return data.perp.trades.filter(trade => {
    const symbol = trade.perpBorrowing.baseToken.symbol.toLowerCase()
    const name = trade.perpBorrowing.baseToken.name.toLowerCase()
    return symbol.includes(searchLower) || name.includes(searchLower)
  })
}

// Usage
const btcTrades = await searchTrades("nibi1abc...", "bitcoin")
const ethTrades = await searchTrades("nibi1abc...", "eth")

Best Practices

1. Use Appropriate Page Sizes

// Good: Reasonable page size
const PAGE_SIZE = 50

// Bad: Too large, slow queries
const PAGE_SIZE = 10000

// Bad: Too small, too many requests
const PAGE_SIZE = 5

Recommended page sizes:

  • Trades: 20-100

  • Fee transactions: 50-100

  • Trade history: 50-200

  • Tokens: 100-500


2. Cache Paginated Results

class CachedPaginator {
  constructor(client, query, variables) {
    this.client = client
    this.query = query
    this.variables = variables
    this.cache = new Map()
  }
  
  async getPage(page, pageSize = 20) {
    const cacheKey = `${page}-${pageSize}`
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)
    }
    
    const { data } = await this.client.query({
      query: this.query,
      variables: {
        ...this.variables,
        limit: pageSize,
        offset: page * pageSize
      }
    })
    
    this.cache.set(cacheKey, data)
    return data
  }
  
  clearCache() {
    this.cache.clear()
  }
}

3. Handle Empty Results

async function fetchTradesWithEmptyCheck(trader, page = 0) {
  const { data } = await client.query({
    query: GET_TRADES,
    variables: {
      trader,
      limit: 20,
      offset: page * 20
    }
  })
  
  const trades = data.perp.trades
  
  if (trades.length === 0) {
    if (page === 0) {
      console.log('No trades found for this trader')
    } else {
      console.log('No more trades to load')
    }
  }
  
  return trades
}

4. Debounce Filter Changes

import { debounce } from 'lodash'

const debouncedSearch = debounce(async (searchTerm) => {
  const results = await searchTrades(trader, searchTerm)
  updateUI(results)
}, 300) // Wait 300ms after user stops typing

// Usage in React
function SearchInput() {
  const handleChange = (e) => {
    debouncedSearch(e.target.value)
  }
  
  return <input onChange={handleChange} />
}

5. Show Loading States

async function loadTradesWithUI(trader, page) {
  try {
    showLoadingSpinner()
    
    const trades = await fetchPage(trader, page)
    
    if (trades.length === 0) {
      showEmptyState()
    } else {
      renderTrades(trades)
    }
  } catch (error) {
    showErrorState(error)
  } finally {
    hideLoadingSpinner()
  }
}

6. Validate Filter Inputs

function validateDateRange(fromDate, toDate) {
  const from = new Date(fromDate)
  const to = new Date(toDate)
  
  if (isNaN(from.getTime()) || isNaN(to.getTime())) {
    throw new Error('Invalid date format')
  }
  
  if (from > to) {
    throw new Error('From date must be before to date')
  }
  
  const maxRange = 90 * 24 * 60 * 60 * 1000 // 90 days
  if (to - from > maxRange) {
    throw new Error('Date range too large (max 90 days)')
  }
  
  return { fromDate, toDate }
}

// Usage
try {
  const { fromDate, toDate } = validateDateRange(
    userInputFrom,
    userInputTo
  )
  
  const { data } = await client.query({
    query: GET_FEES,
    variables: { fromDate, toDate }
  })
} catch (error) {
  showError(error.message)
}

Previous | Back to Introduction

Last updated