PearPear
API IntegrationImportant Terms

Instrument ID

Every symbol field in the API takes an exchange-specific instrument ID, not the ticker. How to resolve one, where they appear, and how they fail.

An instrument ID is the exchange's own identifier for one market. Every symbol field the gateway accepts takes an instrument ID, and every instrument the gateway returns is named by one. It is not the ticker.

On Hyperliquid, BTC is "0". On Lighter, "0" is ETH. The same string names a different asset on a different venue, so an instrument ID means nothing without the connector it belongs to.

Resolve tickers to IDs per connector with GET /instruments/symbol-to-id-map, cache the result, and never hardcode an ID or carry one across venues.

What the ID looks like on each venue

ConnectorThe ID isQuote assets listedBTCETHSOL
hyperliquidThe asset index, as a decimal string.USDC"0""1""5"
lighterThe market ID, as a decimal string.USDC"1""0""2"
binanceThe exchange symbol.USDT, USDC"BTCUSDT""ETHUSDT""SOLUSDT"
bybitThe exchange symbol.USDT, USDC"BTCUSDT""ETHUSDT""SOLUSDT"
okxThe exchange instrument ID.USDT"BTC-USDT-SWAP""ETH-USDT-SWAP""SOL-USDT-SWAP"

Pear lists only the quote assets in that column. An OKX market therefore always ends -USDT-SWAP, and a Hyperliquid or Lighter market always settles in USDC.

Compare the first two rows. "0" is BTC on Hyperliquid and ETH on Lighter, and "1" is the reverse. An ID copied from one venue to another does not fail loudly. It trades the wrong asset.

The connector is never on the leg. It comes from the trade account in x-trade-account-id, so the IDs in a request body must all belong to that account's exchange.

Three rules for handling the value:

  • It is always a JSON string, even when it reads as a number. "0", not 0.
  • Treat it as opaque. Do not parse it as an integer, do arithmetic on it, or sort by it.
  • It is 1 to 64 characters and holds no interior whitespace. Surrounding whitespace is trimmed off before the length check rather than rejected, so " 0 " is accepted as "0" and "0 1" is rejected.

Why Hyperliquid IDs are numbers

Hyperliquid names its markets by position in a universe list, not by ticker, so Pear carries that index through as the ID. BTC sits at index 0.

Hyperliquid also hosts builder-deployed markets (HIP-3), each on its own perp dex with its own index list. Pear offsets those so they cannot collide with the main perps: the main dex counts from 0, and each builder dex starts at 110000, 120000, 130000, and so on. That is why a HIP-3 market looks like "120009".

Builder markets are namespaced by their dex in the base symbol too, so flx:BTC is a different market from BTC, with its own index, price, and funding rate. Pear lists a builder market only where it settles in USDC, the same as the main perps:

{ "BTC": "0", "flx:BTC": "120009" }

Read the numbers from the API. Never compute one.

Resolve a ticker to an ID

GET /instruments/symbol-to-id-map returns one entry per market, keyed by base symbol:

curl "https://pro-gateway.pearprotocol.io/instruments/symbol-to-id-map?connector=hyperliquid&scope=tradable"
{ "data": { "BTC": "0", "ETH": "1", "SOL": "5", "HYPE": "159", "flx:BTC": "120009" } }

scope is tradable, reduce_only, not_tradable, or all. It defaults to all, which includes markets you cannot open, so pass scope=tradable when you are building a list a user picks from.

connector is required. Only scope has a default.

Resolve once at startup and cache the map, per connector. Refresh it on a schedule: venues list new markets, and a market can stop being tradable.

The map is keyed by base symbol, so it is lossy. Where a connector lists two markets on the same base, the map holds one of them and drops the other silently. Read GET /instruments instead when you need every market.

The full instrument record

GET /instruments returns everything the gateway knows about each market:

{
  "id": "0",
  "base": "BTC",
  "quote": "USDC",
  "connector": "hyperliquid",
  "type": "perp",
  "leverage": 40,
  "marginMode": "any",
  "precision": {
    "quantity": { "decimals": 5, "step": 0.00001, "contractSize": 1 },
    "price": { "decimals": 1, "sigFigs": 5 },
    "notional": { "min": 10 }
  },
  "tradeability": { "status": "tradable", "reason": null }
}
FieldWhat it tells you
idThe instrument ID to send as symbol.
base, quoteWhat to show a user, and what the market settles in.
typeperp, or hip3 for a builder-deployed Hyperliquid market. spot is reserved: no connector Pear ships emits it today, and a SELL leg on a spot market would be rejected.
leverageThe venue's maximum for this market. Set your own with PUT /leverage.
marginModeThe modes the market offers, not the mode an account selected. any means the user chooses.
precisionQuantity step, price decimals, and the minimum notional the venue accepts.
tradeabilitytradable, reduce_only, or not_tradable, with a reason. A listed instrument is not always an openable one.

GET /instruments/lite returns the same list trimmed to the ID, base, margin mode, and contract size.

All three instrument endpoints require connector. There is no default and no cross-venue listing.

Where you send an instrument ID

You send it asIn
legs[].symbolPOST /trade/open, adjust, close, and reverse. See Basket Trade.
condition.data.symbolprice and funding_rate trigger conditions. A funding_rate condition names its own connector beside it.
condition.data.symbol_a, symbol_bratio conditions, numerator first.
condition.data.basket[].symbolweighted_ratio conditions.
legs[].symbolTWAP schedules, at the top level of the body.
payload.legs[].symbolLadders. A ladder nests its legs under payload, and the body rejects any key it does not recognise, so a top-level legs is a 400, not an ignored field.
weightings[].symbolManual rebalance.
symbolPUT /leverage.
instrumentIdsWebSocket subscriptions to prices, funding, and trades.
assetThe GET /statistics/leaderboard filter. It matches instrument IDs, uppercased, so asset=0 narrows a Hyperliquid board to BTC and asset=BTC returns nothing.

Where you get one back

It comes back asIn
orders[].symbolExecution.
symbolFills.
longAssets, shortAssets, legs[].assetClosed positions only. An open position carries none of the three.
The keys of exposure, initialExposureUSD, entryPrices, totalFundingPaymentGET /positions. This is how an open position names its instruments.
The keys of the mid and mark mapsGET /prices.
The keys of the funding mapGET /funding. That response holds funding and nothing else; there are no mid or mark maps on it.
instrumentId, beside a displaySymbolGET /markets.
legs[].id, beside the ticker in legs[].symbolGET /markets/baskets. See Trade Idea.

Keep the reverse map, ID to ticker, for anything a person reads. A position that reports "0": "0.0052" means 0.0052 BTC, and only your map says so.

When the ID is wrong

Where the mistake surfaces depends on what you called:

  • Triggers and ladders reject it at creation with 422, naming each unknown ID: Unknown instrument id(s) for hyperliquid: 99999. The check covers the condition. A bad symbol in the trigger's own payload legs is not caught here.
  • Trades do not check it up front. POST /trade/open returns 202 Accepted first, and the execution then fails with Instrument not available for asset: 99999. It arrives over the WebSocket as execution.failed, never in the HTTP response. Read GET /executions/{id} to confirm. See Error Handling.
  • A valid ID for the wrong venue passes both checks if that ID also exists on the account's exchange. Nothing errors. You trade the wrong asset.

That last case is why the map is per connector, and why an ID is never worth reusing.

Market data SDKs use the venue's own name

@pear-protocol/market-sdk and @pear-protocol/exchanges-sdk talk to the exchanges directly, so they take the venue's own market name, not the gateway's instrument ID. On Binance, Bybit, OKX, and Lighter those are the same string. On Hyperliquid they differ: the market SDK takes the coin name BTC, where a gateway trade leg takes "0".

Convert at the boundary. base from GET /instruments is the name the market SDKs want.

On this page