API and SDKs

Market data, the live stream, accounts, sessions and signed trading over plain HTTP — every call in Python, TypeScript and Rust, and a client in each to download and install.

Examples in
API contents

Download the SDKs

Each is the full source, zipped when this site was built. Each test suite signs every struct the venue verifies and checks the digest and the signature against vectors the venue’s own code generated, so a client that passes signs exactly what the server accepts.

Install

From the download to an import. pip builds the Python package straight from its link; the TypeScript package builds once and installs as a local dependency; the Rust crate joins your Cargo.toml as a path dependency.

# pip builds the package straight from the download
pip install https://prismperp.trade/sdk/prismperp-python.zip

# then: from prismperp import PrismPerp

Quickstart

Read the live index, sign in once, and trade. Collateral is USDG in PrismPerpVault, deposited from your wallet first: the terminal’s Deposit dialog does it in two transactions. Orders and the position tools settle on-chain, so each call answers at once and the account shows the result a few seconds later; wait_for_order and wait_for poll for it. Each SDK’s examples/quickstart runs a position’s whole life — open, add margin, reduce, a stop-loss, close, export — against a live server.

pip install ./prismperp-python          # requests + eth-account

import os
from prismperp import PrismPerp

pp = PrismPerp("https://prismperp.trade")
print(pp.snapshot()["index"]["value"])   # C-VIX, live

# One wallet signature, gas-free; a fresh session key signs everything after it.
# The wallet deposits USDG into PrismPerpVault first (the terminal's Deposit).
trader = pp.connect(os.environ["WALLET_KEY"])
order = trader.place_order("CVIX30", is_long=True, margin=100, leverage=5)
order = trader.wait_for_order(order["id"])   # settled on-chain in seconds
print(order["status"], order["fillPrice"], order["positionId"])

trader.close_position(order["positionId"])
trader.revoke()

Conventions

  • Base URL. https://prismperp.trade. JSON in, JSON out; every answer says "ok" and a refusal carries error and code.
  • The signing domain. EIP-712, domain PrismPerp / 1 / the venue’s chain id / its settlement contract — the zero address on the paper venue. Read all of it from the venue block of /api/snapshot, along with the address each collateral is signed as. The SDKs do; nothing here should be hard-coded.
  • Amounts. 1e18 fixed-point integers, sent as decimal strings, so no reader rounds one through a double. 100 USDG is "100000000000000000000". Levels are the same: a C-VIX of 46.07 or a spread of −1.5 bps.
  • Keys. The wallet signs one Session per session; a fresh key signs everything else, so bots never hold the wallet key in memory for longer than a login. A signature by the wallet itself is also accepted.
  • Nonces and deadlines. An order takes the next nonce from /api/nonce and a deadline up to an hour out. Account actions take any unique nonce and a deadline up to ten minutes out; a stop or target’s deadline is its expiry, up to thirty days. The same signed action is applied once.
  • Streaming. Server-sent events on /api/stream, the snapshot every few seconds. Poll /api/account for an account’s own changes.
  • Both venues. Partial close, add margin, stops and targets work on paper and on-chain. On the chain venue PrismPositionManager applies each: the call answers 202 with its transaction, and a stop or target fires on the contract’s own reading of the oracle.

Signed structs

The field order and types are the type hash, so they are exactly these. Order, Session and ClosePosition are also what PrismPerpSettlement verifies on-chain.

StructFields, in orderSigned to
Sessionaddress trader, address sessionKey, uint32 scope, uint64 epoch, uint64 issuedAt, uint64 expiresAtsigned by the wallet: delegates to a session key
Orderaddress trader, uint8 market, bool isLong, address collateral, uint256 size, uint256 margin, int256 limitPrice, uint64 leverage, uint64 nonce, uint64 deadlineopen a position
ClosePositionaddress trader, uint256 positionId, uint64 nonce, uint64 deadlineclose a whole position
CancelIntentaddress trader, uint256 intentId, uint64 nonce, uint64 deadlinecancel a resting order
ReducePositionaddress trader, uint256 positionId, uint256 size, uint64 nonce, uint64 deadlinepartial close
AddMarginaddress trader, uint256 positionId, uint256 amount, uint64 nonce, uint64 deadlineadd margin
TriggerCloseaddress trader, uint256 positionId, uint256 size, int256 triggerPrice, bool above, uint64 nonce, uint64 deadlinestop-loss or take-profit
CancelTriggeraddress trader, uint256 triggerId, uint64 nonce, uint64 deadlinecancel a stop or target
PaperCollateraladdress trader, string token, uint256 amount, bool isDeposit, uint64 nonce, uint64 deadlinepaper deposit or withdrawal
RevokeSessionaddress trader, address sessionKey, uint64 nonce, uint64 deadlineend a session

Market data

Public reads. Nothing to sign, no key, no account. Every figure carries its provenance: live, modelled, simulated, seeded or historical.

GET/api/snapshotPublic

Snapshot

Everything the terminal shows, in one read: each market's state, the C-VIX index and its contributors, the FR-BASIS spreads, venue funding, oracle health, the book, the vault, and the venue block that names the signing domain.

Response, abridged
{ "at": 1790226449442,
  "index": { "value": 46.07, "raw": 46.07, "dampened": false, "contributors": [ … ] },
  "markets": [ { "market": "CVIX30", "last": 46.07, "oiLong": 1.2e6, "borrowRate": 0.0018, … } ],
  "basis": { "BTC": { "spread": 0.39 }, "ETH": { "spread": -0.02 } },
  "venue": { "mode": "chain", "chainId": 46630, "settlement": "0xd1FB…60d9",
             "collateral": { "USDG": "0x827d…4336" }, "positionTools": true } }
snap = pp.snapshot()
print(snap["index"]["value"], snap["basis"]["BTC"]["spread"])
venue = pp.venue()   # chainId, settlement, collateral: what orders are signed against
GET/api/streamPublic

Live stream

The snapshot again, pushed as server-sent events every few seconds. The connection ends after five minutes; reconnect when it does. An `event: error` frame means one snapshot could not be built, not that the stream is over.

for snap in pp.stream():          # a generator; break to stop
    print(snap["at"], snap["index"]["value"])
GET/api/marketsPublic

Markets

Each market's state row: last level, 24-hour open, high and low, open interest by side, the borrow rate and 24-hour notional.

for m in pp.markets()["markets"]:
    print(m["market"], m["last"], m["borrowRate"])
GET/api/candlesPublic

Candles

OHLC for one market at one timeframe, oldest first. Each bar says where it came from, and `coverage` counts bars by source.

Query parameters
market *CVIX30 | FRBASIS-BTC | FRBASIS-ETH
intervalseconds60, 300, 900, 3600, 14400 or 86400; default 60
limitinteger10 to 1000; default 300
Response, abridged
{ "ok": true, "market": "CVIX30", "interval": 3600,
  "candles": [ { "t": "2026-09-24T04:00:00.000Z", "o": 45.9, "h": 46.2, "l": 45.8, "c": 46.07, "v": 0, "n": 0, "source": "live" } ],
  "coverage": { "live": 48 } }
bars = pp.candles("CVIX30", interval=3600, limit=48)["candles"]
closes = [b["c"] for b in bars]
GET/api/fundingPublic

Funding

The latest funding rate at each venue, normalised to bps per 8 hours, the current BTC and ETH basis, and recent BTC spread history. `nativeIsModelled` says whether the native leg is a print or a model.

f = pp.funding()
print(f["basis"]["BTC"], [r["venue"] for r in f["rates"]])
GET/api/oraclesPublic

Oracle health

Each source venue's last answer: whether it answered, how fast, and what it said.

for o in pp.oracles()["oracles"]:
    print(o["venue"], o["ok"], o["latencyMs"])
GET/api/historyPublic

History

Stored history the snapshot does not carry: open-interest samples, oracle events, funding settlement windows, and row counts by provenance.

Query parameters
kind *oi | events | settlements | provenance
marketmarket idrequired for oi; filters settlements
limitinteger1 to 500; default 120
oi = pp.history("oi", market="CVIX30", limit=200)["rows"]
GET/api/healthPublic

Health

The venue's own checks: index and basis freshness, the relayer's heartbeat, orders stuck in the queue, and on the chain venue gas and solvency. Answers 503 when a critical one fails.

print(pp.health())

Accounts

Readable by address, without a signature, on purpose: on-chain every one of these numbers is public, and a venue that hid them would be rehearsing a privacy it cannot have. Writing is what needs a key.

GET/api/accountPublic

Account

One wallet's balances, open positions valued at the mark (PnL, liquidation level, health), resting stops and targets, closed positions, orders, fills, ledger and totals.

Query parameters
trader *addressthe wallet the account belongs to
Response, abridged
{ "ok": true, "trader": "0x…", "venue": "chain",
  "balances": [ { "token": "USDG", "free": 874.94, "locked": 125, … } ],
  "positions": [ { "id": 1000000001, "market": "CVIX30", "isLong": true, "size": 500, "margin": 125,
                   "mark": 46.07, "value": { "net": 1.2, "liquidationPrice": 42.3, "health": 1.01, … } } ],
  "triggers": [ … ], "closed": [ … ], "intents": [ … ], "fills": [ … ], "ledger": [ … ], "totals": { … } }
acct = pp.account(address)
for p in acct["positions"]:
    print(p["id"], p["value"]["net"], p["value"]["liquidationPrice"])
GET/api/ordersPublic

Orders

The wallet's last fifty orders, any status: pending, submitted, settled (on-chain) or matched (paper), expired, rejected or cancelled, with fill price and position.

Query parameters
trader *addressthe wallet the account belongs to
intents = pp.orders(address)["intents"]
GET/api/positions/triggersPublic

Stops and targets

The wallet's stop-losses and take-profits, resting and past, with the level each fires at and what it realised if it did.

Query parameters
trader *addressthe wallet the account belongs to
resting = [t for t in pp.triggers(address)["triggers"] if t["status"] == "pending"]
GET/api/noncePublic

Order nonce

The next unused nonce for an order. The SDKs ask before every order; account actions use their own.

Query parameters
trader *addressthe wallet the account belongs to
n = pp.nonce(address)
GET/api/account/exportPublic

CSV export

An account's whole history as CSV, for bookkeeping: the same overview the portfolio page reads, over every row. Times are UTC, ISO 8601; amounts are in each row's own collateral. 20 a minute per IP.

Query parameters
trader *addressthe wallet the account belongs to
kindfills | closes | ledger | intents | triggersdefault fills
csv = pp.export_csv(address, "ledger")
open("ledger.csv", "w").write(csv)

Sessions

The wallet signs exactly one thing per session: a grant naming a fresh key, a scope, its revocation epoch and an expiry of up to seven days. Everything after is signed by that key, so trading never prompts the wallet and never costs gas. The SDKs' connect does all three steps below.

GET/api/sessionPublic

Epoch, or whether a key is live

With `trader` alone: the wallet's current session epoch, which a new grant must name. With `key` too: whether that key is live for that wallet, its scope and expiry.

Query parameters
trader *addressthe wallet the account belongs to
keyaddressa session key, to ask whether it is live
epoch = pp.get("/api/session", trader=address)["epoch"]
POST/api/session Signs Session

Open a session

Register a grant the wallet has just signed. Scope is a bitmask: 1 open, 2 close, 4 cancel, 8 paper collateral.

JSON body
trader *addressthe wallet the account belongs to
sessionKey *addressthe key that will sign; not the wallet itself
scope *uint32bitmask; 15 is everything
epoch *integerfrom GET /api/session
issuedAt *unix seconds
expiresAt *unix secondsat most 7 days after issuedAt
signature *hexby the wallet, over the Session struct
trader = pp.connect(wallet_key, ttl_seconds=12 * 3600)   # signs and posts the grant
print(trader.signer_address)
DELETE/api/session Signs RevokeSession

Revoke a session

The venue stops honouring the key, and every stop or target that key signed is cancelled. Signed by the wallet or by the session key itself.

JSON body
trader *addressthe wallet the account belongs to
sessionKey *address
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
trader.revoke()

Trading

Every write carries an EIP-712 signature, and the only question is whether the key behind it is the wallet or one the wallet delegated to — the check PrismPerpSettlement makes too. Amounts are 1e18 fixed-point integer strings. Rate limits: 60 a minute per IP and 30 per address for orders, and again for closes, cancels and the position tools.

POST/api/orders Signs Order

Place an order

Open a position. A market order settles at the index in the relayer's next batch, a few seconds on-chain; a limit rests until the index reaches it or its deadline passes. Size must equal margin × leverage. The answer carries the order's id: poll the account, or wait_for_order, for the fill or why there was none.

JSON body
trader *addressthe wallet the account belongs to
market *market idthe id as a string; the signed struct carries its index (0, 1, 2)
isLong *bool
collateral *addressthe token address from the venue block, as signed
size *uint256notional, 1e18 fixed point; margin × leverage
margin *uint2561e18 fixed point
limitPrice *int256worst acceptable level; 0 fills at the index
leverage *integerup to 10× on C-VIX, 20× on FR-BASIS
nonce *integerfrom GET /api/nonce
deadline *unix secondsat most an hour out
signature *hexover the Order struct
Response, abridged
{ "ok": true, "id": 34, "status": "pending", "fillPrice": null, "positionId": null,
  "signedBy": "session", "rejectReason": null, "txHash": null, "venue": "chain" }
order = trader.place_order("CVIX30", is_long=True, margin=100, leverage=5)
limit = trader.place_order("FRBASIS-BTC", is_long=False, margin=250, leverage=10,
                           limit_price="-1.5", ttl_seconds=3600)
POST/api/orders/cancel Signs CancelIntent

Cancel an order

Withdraw a resting order and release the margin and fee reserved against it.

JSON body
trader *addressthe wallet the account belongs to
intentId *integerthe order's id
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
trader.cancel_order(intent_id)
POST/api/positions/close Signs ClosePosition

Close a position

Close a whole position at the index. Refused against a stale feed: a price from an hour ago is not a settlement price. On the chain venue the signed close goes to closePositionFor, where the contract checks it again.

JSON body
trader *addressthe wallet the account belongs to
positionId *integer
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
closed = trader.close_position(position_id)
print(closed["exitPrice"], closed["net"])
POST/api/positions/reduce Signs ReducePosition

Partial close

Close part of a position at the index. Margin, borrow and carry leave in proportion to size, so what stays open keeps its entry, leverage and liquidation level. A size equal to the whole position closes it. On the chain venue PrismPositionManager applies it: the answer is 202 with the transaction, and the account shows the result in seconds.

JSON body
trader *addressthe wallet the account belongs to
positionId *integer
size *uint256notional to close, 1e18 fixed point
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
Response, abridged
{ "ok": true, "status": "reduced", "closed": 200, "remaining": 300, "exitPrice": 46.07, "net": 1.84, "returned": 51.84 }
// chain venue, 202:
{ "ok": true, "id": 12, "status": "submitted", "txHash": "0x…", "venue": "chain" }
r = trader.reduce_position(position_id, 200)
print(r["closed"], r["remaining"], r["net"])
POST/api/positions/margin Signs AddMargin

Add margin

Move free balance into a position's margin. The liquidation level moves away and the payout cap, eight times margin, rises with it. Margin cannot pass the notional: under 1× there is nothing left to protect. On the chain venue it is a transaction PrismPositionManager applies, answered 202 like a partial close.

JSON body
trader *addressthe wallet the account belongs to
positionId *integer
amount *uint256in the position's collateral, 1e18 fixed point
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
m = trader.add_margin(position_id, 25)
print(m["margin"], m["leverage"])
POST/api/positions/triggers Signs TriggerClose

Stop-loss or take-profit

A close signed in advance: it rests until the index reaches its level, then fires at the index. `above` says which side: for a long, above is a take-profit and below a stop. A level the index is already past is refused with 422 crossed. Rests up to 30 days; revoking the key that signed it cancels it. On the chain venue the relayer fires it through PrismPositionManager, which checks the level against the oracle itself, and the account's trigger carries its EIP-712 digest as `hash`.

JSON body
trader *addressthe wallet the account belongs to
positionId *integer
size *uint256notional to close when it fires; 0 closes what is left
triggerPrice *int256the level, 1e18 fixed point; bps on FR-BASIS, may be negative
above *boolfire at or above (true) or at or below (false)
nonce *integer
deadline *unix secondsits expiry, at most 30 days out
signature *hexover the TriggerClose struct
stop = trader.place_trigger(position_id, level="41.5", above=False)          # stop-loss, closes all
target = trader.place_trigger(position_id, level=52, above=True, size=250)  # take-profit on half
POST/api/positions/triggers/cancel Signs CancelTrigger

Cancel a stop or target

Withdraw a resting stop-loss or take-profit before it fires. On the chain venue, name it by its digest (`hash`, as a decimal integer) and the cancel is also recorded by PrismPositionManager, so the contract refuses to fire it whoever holds the signature; that answer is 202.

JSON body
trader *addressthe wallet the account belongs to
triggerId *uint256the trigger's id, or on the chain venue its digest as a decimal integer
nonce *integerunique per action; the replay guard is the signed hash, so the time in ms is fine
deadline *unix secondsat most 10 minutes out
signature *hex65 bytes, r‖s‖v, over the struct named above
trader.cancel_trigger(stop["id"])

Errors

A refusal is { "ok": false, "error": "…", "code": "…" } with the status below. The SDKs raise it as PrismPerpError (TypeScript, Python) or Error::Api (Rust), carrying both.

CodeStatusMeaning
unauthorised401The signature recovers to a key with no live session for this wallet, or without the scope the action needs.
nonce_used409An order with this nonce already exists for this wallet. Ask GET /api/nonce again.
replayed409This exact signed action has been applied already.
stale_oracle503The market's feed has stopped updating; nothing opens or settles against an old price.
insufficient_balance422The free balance does not cover margin plus the opening fee, or the margin to add.
crossed422The index is already past the stop or target's level; close now instead.
open_interest_cap422This side of the market is at its open-interest cap, the market registry's figure on both venues.
account_cap422The order would take this account past its open-interest limit in the market.
refused422Chain venue: the contract refused the position tool when the relayer simulated it; the message carries its reason.
not_found404No such open position, or not on this account.
rate_limited429Too many requests in the last minute from this IP or this address.