Docs
SDK

How to Close Positions

This guide shows you how to close both taker and maker positions using the PerpCity SDK.

Closing Taker Positions

To close a taker position (long or short):

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

await closePosition(
  context,
  '0x...', // perpId (Hex)
  123n,    // positionId (bigint)
  {
    minAmt0Out: 0,     // Minimum token0 output (slippage protection)
    minAmt1Out: 0,     // Minimum token1 output
    maxAmt1In: 1000000 // Maximum token1 input
  }
)

That's it! The SDK handles:

  • Calculating your PnL
  • Settling funding payments
  • Returning your margin (if profitable)
  • Closing the position on-chain

Closing Maker Positions

Closing a maker position is the same:

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

await closePosition(
  context,
  '0x...', // perpId (Hex)
  456n,    // positionId (bigint)
  {
    minAmt0Out: 0,
    minAmt1Out: 0,
    maxAmt1In: 1000000
  }
)

This withdraws your liquidity and any earned fees.

Complete Example

import { PerpCityContext, closePosition } from '@strobelabs/perpcity-sdk'
import { createWalletClient, http } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'

async function closeMyPosition() {
  // Setup wallet
  const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
  const walletClient = createWalletClient({
    account,
    chain: base,
    transport: http(process.env.RPC_URL)
  })

  // Initialize context
  const context = new PerpCityContext({
    walletClient,
    rpcUrl: process.env.RPC_URL!,
    deployments: {
      perpManager: process.env.PERP_MANAGER_ADDRESS as `0x${string}`,
      usdc: process.env.USDC_ADDRESS as `0x${string}`
    }
  })

  // Close position
  console.log('Closing position...')

  await closePosition(
    context,
    '0x...', // perpId
    123n,    // positionId
    {
      minAmt0Out: 0,
      minAmt1Out: 0,
      maxAmt1In: 1000000
    }
  )

  console.log('Position closed successfully!')
}

closeMyPosition()

Understanding Close Settlement

When you close a position:

For Taker Positions

Final Balance = Initial Margin + PnL - Funding Payments - Fees

For Maker Positions

Final Balance = Initial Liquidity + Fee Earnings + Funding Received
  • You receive your initial liquidity back
  • Plus any earned trading fees
  • Plus/minus funding payments based on market skew

Lockup Periods (Maker Positions)

Maker positions may have minimum lockup periods:

const config = await context.getPerpConfig('0x...')
const lockupModule = config.lockupPeriod

console.log('Lockup period module:', lockupModule)

If you try to close during lockup:

Error: Position still locked

Solution: Wait until the lockup period expires.

Checking Position Age

To check if lockup has expired:

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

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

// Get position open event
const logs = await publicClient.getContractEvents({
  address: perpManagerAddress,
  abi: perpManagerAbi,
  eventName: 'PositionOpened',
  args: {
    positionId: 123n
  }
})

const openBlock = logs[0].blockNumber
const openBlockData = await publicClient.getBlock({ blockNumber: openBlock })
const openTimestamp = openBlockData.timestamp

const currentBlock = await publicClient.getBlock()
const currentTimestamp = currentBlock.timestamp

const positionAge = Number(currentTimestamp - openTimestamp)
const lockupPeriod = config.lockupPeriod

if (positionAge >= lockupPeriod) {
  console.log('Position can be closed')
} else {
  const waitTime = lockupPeriod - positionAge
  console.log(`Wait ${waitTime} more seconds`)
}

Handling Liquidations

If your position is liquidatable, it may be liquidated by a third party before you can close it:

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

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

// Step 2: Check if liquidatable
const isLiquidatable = getPositionIsLiquidatable(positionData)

if (isLiquidatable) {
  console.warn('WARNING: Position is liquidatable!')
  // Close immediately or add margin
}

Common Errors

Position Not Found

Error: Position does not exist

Solution: Verify position ID and perpId are correct. Position may already be closed.

Position Already Closed

Error: Position already closed

Solution: Check if you already closed this position or if it was liquidated.

Still in Lockup

Error: Position still locked

Solution: Wait for lockup period to expire (maker positions only).

Liquidated Position

Error: Position has been liquidated

Solution: Position was liquidated due to insufficient margin. Cannot close.

Transaction Failed Errors

If the transaction fails:

  1. Check gas: Ensure you have enough ETH for gas
  2. Check network: Verify you're on the correct network (Base)
  3. Check RPC: Try a different RPC endpoint if timeout occurs

Batch Closing Multiple Positions

To close multiple positions efficiently:

// Close positions sequentially
const positionIds = [123n, 124n, 125n]
const perpId = '0x...'

for (const positionId of positionIds) {
  await closePosition(context, perpId, positionId, {
    minAmt0Out: 0,
    minAmt1Out: 0,
    maxAmt1In: 1000000
  })
  console.log(`Closed position ${positionId}`)
}

// Or in parallel (riskier, may hit rate limits)
await Promise.all(
  positionIds.map(positionId =>
    closePosition(context, perpId, positionId, {
      minAmt0Out: 0,
      minAmt1Out: 0,
      maxAmt1In: 1000000
    })
  )
)

Next Steps