Docs
SDK

How to Query Position Data

This guide shows you how to query position data, market data, and user balances using the PerpCity SDK.

Quick Position Data

Get all data for a single position:

// Fetch position data via context method
const positionData = await context.getOpenPositionData(
  '0x...', // perpId (Hex)
  123n,    // positionId (bigint)
  true,    // isLong
  false    // isMaker (true for maker, false for taker)
)

console.log({
  pnl: positionData.liveDetails.pnl,
  fundingPayment: positionData.liveDetails.fundingPayment,
  effectiveMargin: positionData.liveDetails.effectiveMargin,
  isLiquidatable: positionData.liveDetails.isLiquidatable
})

Individual Position Metrics

Extract specific metrics using pure functions. The SDK uses a two-step pattern:

  1. Fetch data via context.getOpenPositionData() (async)
  2. Extract values via pure functions (sync)

Profit and Loss (PnL)

import { getPositionPnl } from '@strobelabs/perpcity-sdk'

// Step 1: Fetch position data
const positionData = await context.getOpenPositionData('0x...', 123n, true, false)

// Step 2: Extract PnL
const pnl = getPositionPnl(positionData)
console.log(`PnL: $${pnl.toFixed(2)}`)

Funding Payments

import { getPositionFundingPayment } from '@strobelabs/perpcity-sdk'

const positionData = await context.getOpenPositionData('0x...', 123n, true, false)
const funding = getPositionFundingPayment(positionData)
console.log(`Funding: $${funding.toFixed(2)}`)

Effective Margin

import { getPositionEffectiveMargin } from '@strobelabs/perpcity-sdk'

const positionData = await context.getOpenPositionData('0x...', 123n, true, false)
const margin = getPositionEffectiveMargin(positionData)
console.log(`Effective margin: $${margin.toFixed(2)}`)

Liquidation Status

import { getPositionIsLiquidatable } from '@strobelabs/perpcity-sdk'

const positionData = await context.getOpenPositionData('0x...', 123n, true, false)
const isLiquidatable = getPositionIsLiquidatable(positionData)

if (isLiquidatable) {
  console.warn('WARNING: Position can be liquidated!')
}

User Data (Batch Queries)

Get all positions and balance in one call:

import { getUserUsdcBalance, getUserOpenPositions } from '@strobelabs/perpcity-sdk'

// Fetch user data with all positions
const userData = await context.getUserData(
  '0x...', // User address
  [
    { perpId: '0x...', positionId: 123n, isLong: true, isMaker: false },
    { perpId: '0x...', positionId: 124n, isLong: true, isMaker: false },
    { perpId: '0x...', positionId: 456n, isLong: false, isMaker: true }
  ]
)

// Extract values using pure functions
const balance = getUserUsdcBalance(userData)
const positions = getUserOpenPositions(userData)

console.log(`USDC Balance: $${balance.toFixed(2)}`)

positions.forEach((pos, i) => {
  console.log(`Position ${i + 1}:`, {
    pnl: pos.liveDetails.pnl,
    fundingPayment: pos.liveDetails.fundingPayment,
    effectiveMargin: pos.liveDetails.effectiveMargin,
    isLiquidatable: pos.liveDetails.isLiquidatable
  })
})

This is more efficient than querying each position separately!

Market Data

All market data queries use the two-step pattern:

Current Mark Price

import { getPerpMark } from '@strobelabs/perpcity-sdk'

// Step 1: Fetch perp data
const perpData = await context.getPerpData('0x...')

// Step 2: Extract mark price
const mark = getPerpMark(perpData)
console.log(`Current mark: ${mark}`)

Beacon (Oracle) Address

import { getPerpBeacon } from '@strobelabs/perpcity-sdk'

const perpData = await context.getPerpData('0x...')
const beacon = getPerpBeacon(perpData)
console.log(`Oracle address: ${beacon}`)

Tick Spacing

import { getPerpTickSpacing } from '@strobelabs/perpcity-sdk'

const perpData = await context.getPerpData('0x...')
const tickSpacing = getPerpTickSpacing(perpData)
console.log(`Tick spacing: ${tickSpacing}`)

Bounds

import { getPerpBounds } from '@strobelabs/perpcity-sdk'

const perpData = await context.getPerpData('0x...')
const bounds = getPerpBounds(perpData)
console.log('Bounds:', bounds)

Fee Structure

import { getPerpFees } from '@strobelabs/perpcity-sdk'

const perpData = await context.getPerpData('0x...')
const fees = getPerpFees(perpData)
console.log({
  creatorFee: fees.creatorFee,
  insuranceFee: fees.insuranceFee,
  lpFee: fees.lpFee,
  liquidationFee: fees.liquidationFee
})

Complete Perp Data

Get all market data in one call:

const perpData = await context.getPerpData('0x...')

console.log({
  id: perpData.id,
  mark: perpData.mark,
  beacon: perpData.beacon,
  tickSpacing: perpData.tickSpacing,
  bounds: perpData.bounds,
  fees: perpData.fees
})

User Balance

Get USDC balance using the two-step pattern:

import { getUserUsdcBalance } from '@strobelabs/perpcity-sdk'

// Step 1: Fetch user data
const userData = await context.getUserData(
  '0x...', // User address
  []       // Empty array if no positions to check
)

// Step 2: Extract balance
const balance = getUserUsdcBalance(userData)
console.log(`Balance: $${balance.toFixed(2)}`)

Complete Dashboard Example

Build a trading dashboard with all relevant data:

import {
  PerpCityContext,
  getUserUsdcBalance,
  getUserOpenPositions
} from '@strobelabs/perpcity-sdk'

async function fetchDashboardData(context: PerpCityContext) {
  // Your positions (from your tracking system)
  const myPositions = [
    { perpId: '0x...', positionId: 123n, isLong: true, isMaker: false },
    { perpId: '0x...', positionId: 124n, isLong: true, isMaker: false },
    { perpId: '0x...', positionId: 456n, isLong: false, isMaker: true }
  ]

  // Get user data (balance + all positions)
  const userData = await context.getUserData(
    context.walletClient.account.address,
    myPositions
  )

  // Get market data for each unique perp
  const perpIds = [...new Set(myPositions.map(p => p.perpId))]
  const marketData = await Promise.all(
    perpIds.map(perpId => context.getPerpData(perpId))
  )

  return {
    balance: getUserUsdcBalance(userData),
    positions: getUserOpenPositions(userData),
    markets: marketData
  }
}

// Use in your app
const dashboard = await fetchDashboardData(context)

console.log(`Wallet Balance: $${dashboard.balance.toFixed(2)}`)
console.log(`Open Positions: ${dashboard.positions.length}`)

dashboard.positions.forEach(pos => {
  const pnl = pos.liveDetails.pnl
  console.log(
    `Position ${pos.positionId}: ${pnl >= 0 ? '+' : ''}$${pnl.toFixed(2)}`
  )
})

Polling for Updates

For real-time data, poll on an interval:

import { getPositionIsLiquidatable } from '@strobelabs/perpcity-sdk'

// Update position data every 10 seconds
setInterval(async () => {
  const data = await context.getOpenPositionData('0x...', 123n, true, false)

  console.log('Updated PnL:', data.liveDetails.pnl.toFixed(2))

  if (getPositionIsLiquidatable(data)) {
    console.warn('LIQUIDATION WARNING!')
    // Close position or add margin
  }
}, 10_000)

Be careful with polling frequency. Too frequent polls can hit RPC rate limits. 5-10 second intervals are reasonable.

WebSocket Updates (Advanced)

For real-time updates without polling, watch blockchain events:

import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'

const publicClient = createPublicClient({
  chain: base,
  transport: http(process.env.RPC_URL)
})

// Watch for position updates
publicClient.watchContractEvent({
  address: perpManagerAddress,
  abi: perpManagerAbi,
  eventName: 'PositionUpdated',
  args: {
    positionId: 123n
  },
  onLogs(logs) {
    console.log('Position updated!', logs)
    // Refresh position data
  }
})

Caching Strategies

The SDK caches perp configs automatically, but you should cache other data in your app:

import { getPerpMark } from '@strobelabs/perpcity-sdk'

// Cache mark prices with TTL
const markCache = new Map<string, { value: number; expires: number }>()

async function getCachedMark(
  context: PerpCityContext,
  perpId: string,
  ttl = 10_000
): Promise<number> {
  const cached = markCache.get(perpId)

  if (cached && Date.now() < cached.expires) {
    return cached.value
  }

  const perpData = await context.getPerpData(perpId)
  const mark = getPerpMark(perpData)

  markCache.set(perpId, {
    value: mark,
    expires: Date.now() + ttl
  })

  return mark
}

Error Handling

Always handle potential errors when querying data:

try {
  const data = await context.getOpenPositionData('0x...', 123n, true, false)
  console.log('PnL:', data.liveDetails.pnl)
} catch (error) {
  if (error.message.includes('Position does not exist')) {
    console.log('Position was closed or liquidated')
  } else if (error.message.includes('timeout')) {
    console.log('RPC timeout, try again')
  } else {
    console.error('Unexpected error:', error)
  }
}

Next Steps