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 sequencetrade_id- Order by trade ID
LP Deposits:
depositor- Order by depositor addressvault- Order by vault address
LP Deposit History:
depositor- Order by depositorsequence- Order by sequencevault- Order by vault
LP Withdraw Requests:
depositor- Order by depositorunlock_epoch- Order by unlock epochvault- Order by vault
Oracle Tokens:
id- Order by token IDname- Order by token namepermission_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 = 5Recommended 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)
}Last updated