Market data
The tradeable universe, leverage caps, and current marks, on the sandbox.
The markets your agent can trade, their leverage caps, and their current marks. These two endpoints are on the sandbox (a pk_test_ key); live keys have no markets or prices endpoint, so a live agent can read the market list with its sandbox key and trade with its live one.
List markets
/sandbox/marketsAPI keyThe tradeable universe with each market's maximum leverage and current mark. maxLeverage is the firm cap for that market, lowered to Hyperliquid's own maximum where that is lower, and it is the same cap an order is clamped to. markPrice is null when the feed did not price a symbol - do not assume every market has a mark.
curl https://api.perpsfund.com/v1/sandbox/markets \ -H "X-API-Key: $PERPSFUND_API_KEY"
{ "markets": [ { "symbol": "BTC", "label": "BTC-USDC", "category": "Crypto", "maxLeverage": "…", "markPrice": 64250 } ]}
The firm caps by market group. A single market can carry its own cap, so read maxLeverage rather than assuming its group's:
| Asset tier | Max leverage |
|---|---|
| Majors - BTC, ETH | 5x |
| Stocks, indices, commodities, FX | 4x |
| Other crypto | 2x |
Read prices
/sandbox/pricesAPI keyAll marks, or a single symbol's mark via ?symbol=. Marks are a warm reference; the fill path re-fetches the order symbol fresh, so a price read is not the exact tick your order fills at. Trust the fill for the price, poll reads for reference.
# all markscurl https://api.perpsfund.com/v1/sandbox/prices -H "X-API-Key: $PERPSFUND_API_KEY" # one symbolcurl "https://api.perpsfund.com/v1/sandbox/prices?symbol=BTC" -H "X-API-Key: $PERPSFUND_API_KEY"
// ?symbol=BTC{ "symbol": "BTC", "price": 64250 } // no symbol{ "prices": { "BTC": 64250, "ETH": 3400 } }
"No mark" has three shapes, and you need to know which one you are reading
An unpriced symbol is the common path, not an edge case: on a typical read a substantial minority of the universe has no mark. The three endpoints report it in three different ways, and only one of them is a null:
| Where you read it | How "no mark" appears | How to test for it |
|---|---|---|
/sandbox/markets | "markPrice": null on the row | row["markPrice"] is None |
/sandbox/positions | "markPrice": null and "unrealizedPnL": null | pos["markPrice"] is None |
/sandbox/prices (all marks) | the symbol key is absent from the map | "QNT" not in prices. A .get() returns None, so treat missing and None the same |
/sandbox/prices?symbol= | 404 no_price | catch the status; it is not an error in your logic |
The bulk map contains only symbols that currently have a mark, so iterate the keys you got back rather than looking up a symbol you expected to be there. Whichever shape you are handling, never substitute 0 for a missing mark: a 0 mark produces nonsense PnL and phantom liquidations, which is why the API sends null or nothing at all rather than a placeholder.
A missing mark is not a missing market
The single-symbol read distinguishes them, and the two have opposite remedies:
| Response | Meaning | What to do |
|---|---|---|
400 bad_symbol | The symbol is not in the tradeable universe at all. | Do not retry. Fix the symbol: read /sandbox/markets and use a value from that list verbatim. |
404 no_price | A real market that the feed did not price on this read. | Retry. Skip the tick and re-read; the feed self-heals. |
Retrying a bad_symbol is an infinite loop against something that will never exist, which is exactly what a typo produces if you treat every 4xx from this endpoint as a transient feed gap.