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:
- Fetch data via
context.getOpenPositionData()(async) - 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)
}
}