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.
- Python
Python 3.10+, requests and eth-account
- Test
- python -m unittest discover tests
- TypeScript
Node 20+ or a browser, and viem
- Test
- npm test
- Rust
alloy for EIP-712, reqwest and tokio
- Test
- cargo test
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 PrismPerpQuickstart
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 carrieserrorandcode. - 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 thevenueblock 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
Sessionper 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/accountfor 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
202with 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.
| Struct | Fields, in order | Signed to |
|---|---|---|
| Session | address trader, address sessionKey, uint32 scope, uint64 epoch, uint64 issuedAt, uint64 expiresAt | signed by the wallet: delegates to a session key |
| Order | address trader, uint8 market, bool isLong, address collateral, uint256 size, uint256 margin, int256 limitPrice, uint64 leverage, uint64 nonce, uint64 deadline | open a position |
| ClosePosition | address trader, uint256 positionId, uint64 nonce, uint64 deadline | close a whole position |
| CancelIntent | address trader, uint256 intentId, uint64 nonce, uint64 deadline | cancel a resting order |
| ReducePosition | address trader, uint256 positionId, uint256 size, uint64 nonce, uint64 deadline | partial close |
| AddMargin | address trader, uint256 positionId, uint256 amount, uint64 nonce, uint64 deadline | add margin |
| TriggerClose | address trader, uint256 positionId, uint256 size, int256 triggerPrice, bool above, uint64 nonce, uint64 deadline | stop-loss or take-profit |
| CancelTrigger | address trader, uint256 triggerId, uint64 nonce, uint64 deadline | cancel a stop or target |
| PaperCollateral | address trader, string token, uint256 amount, bool isDeposit, uint64 nonce, uint64 deadline | paper deposit or withdrawal |
| RevokeSession | address trader, address sessionKey, uint64 nonce, uint64 deadline | end a session |
Market data
Public reads. Nothing to sign, no key, no account. Every figure carries its provenance: live, modelled, simulated, seeded or historical.
/api/snapshotPublicSnapshot
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.
{ "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/api/streamPublicLive 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"])/api/marketsPublicMarkets
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"])/api/candlesPublicCandles
OHLC for one market at one timeframe, oldest first. Each bar says where it came from, and `coverage` counts bars by source.
| market * | CVIX30 | FRBASIS-BTC | FRBASIS-ETH | |
| interval | seconds | 60, 300, 900, 3600, 14400 or 86400; default 60 |
| limit | integer | 10 to 1000; default 300 |
{ "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]/api/fundingPublicFunding
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"]])/api/oraclesPublicOracle 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"])/api/historyPublicHistory
Stored history the snapshot does not carry: open-interest samples, oracle events, funding settlement windows, and row counts by provenance.
| kind * | oi | events | settlements | provenance | |
| market | market id | required for oi; filters settlements |
| limit | integer | 1 to 500; default 120 |
oi = pp.history("oi", market="CVIX30", limit=200)["rows"]/api/healthPublicHealth
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.
/api/accountPublicAccount
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.
| trader * | address | the wallet the account belongs to |
{ "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"])/api/ordersPublicOrders
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.
| trader * | address | the wallet the account belongs to |
intents = pp.orders(address)["intents"]/api/positions/triggersPublicStops 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.
| trader * | address | the wallet the account belongs to |
resting = [t for t in pp.triggers(address)["triggers"] if t["status"] == "pending"]/api/noncePublicOrder nonce
The next unused nonce for an order. The SDKs ask before every order; account actions use their own.
| trader * | address | the wallet the account belongs to |
n = pp.nonce(address)/api/account/exportPublicCSV 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.
| trader * | address | the wallet the account belongs to |
| kind | fills | closes | ledger | intents | triggers | default 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.
/api/sessionPublicEpoch, 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.
| trader * | address | the wallet the account belongs to |
| key | address | a session key, to ask whether it is live |
epoch = pp.get("/api/session", trader=address)["epoch"]Open a session
Register a grant the wallet has just signed. Scope is a bitmask: 1 open, 2 close, 4 cancel, 8 paper collateral.
| trader * | address | the wallet the account belongs to |
| sessionKey * | address | the key that will sign; not the wallet itself |
| scope * | uint32 | bitmask; 15 is everything |
| epoch * | integer | from GET /api/session |
| issuedAt * | unix seconds | |
| expiresAt * | unix seconds | at most 7 days after issuedAt |
| signature * | hex | by the wallet, over the Session struct |
trader = pp.connect(wallet_key, ttl_seconds=12 * 3600) # signs and posts the grant
print(trader.signer_address)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.
| trader * | address | the wallet the account belongs to |
| sessionKey * | address | |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 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.
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.
| trader * | address | the wallet the account belongs to |
| market * | market id | the id as a string; the signed struct carries its index (0, 1, 2) |
| isLong * | bool | |
| collateral * | address | the token address from the venue block, as signed |
| size * | uint256 | notional, 1e18 fixed point; margin × leverage |
| margin * | uint256 | 1e18 fixed point |
| limitPrice * | int256 | worst acceptable level; 0 fills at the index |
| leverage * | integer | up to 10× on C-VIX, 20× on FR-BASIS |
| nonce * | integer | from GET /api/nonce |
| deadline * | unix seconds | at most an hour out |
| signature * | hex | over the Order struct |
{ "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)Cancel an order
Withdraw a resting order and release the margin and fee reserved against it.
| trader * | address | the wallet the account belongs to |
| intentId * | integer | the order's id |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 bytes, r‖s‖v, over the struct named above |
trader.cancel_order(intent_id)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.
| trader * | address | the wallet the account belongs to |
| positionId * | integer | |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 bytes, r‖s‖v, over the struct named above |
closed = trader.close_position(position_id)
print(closed["exitPrice"], closed["net"])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.
| trader * | address | the wallet the account belongs to |
| positionId * | integer | |
| size * | uint256 | notional to close, 1e18 fixed point |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 bytes, r‖s‖v, over the struct named above |
{ "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"])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.
| trader * | address | the wallet the account belongs to |
| positionId * | integer | |
| amount * | uint256 | in the position's collateral, 1e18 fixed point |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 bytes, r‖s‖v, over the struct named above |
m = trader.add_margin(position_id, 25)
print(m["margin"], m["leverage"])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`.
| trader * | address | the wallet the account belongs to |
| positionId * | integer | |
| size * | uint256 | notional to close when it fires; 0 closes what is left |
| triggerPrice * | int256 | the level, 1e18 fixed point; bps on FR-BASIS, may be negative |
| above * | bool | fire at or above (true) or at or below (false) |
| nonce * | integer | |
| deadline * | unix seconds | its expiry, at most 30 days out |
| signature * | hex | over 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 halfCancel 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.
| trader * | address | the wallet the account belongs to |
| triggerId * | uint256 | the trigger's id, or on the chain venue its digest as a decimal integer |
| nonce * | integer | unique per action; the replay guard is the signed hash, so the time in ms is fine |
| deadline * | unix seconds | at most 10 minutes out |
| signature * | hex | 65 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.
| Code | Status | Meaning |
|---|---|---|
| unauthorised | 401 | The signature recovers to a key with no live session for this wallet, or without the scope the action needs. |
| nonce_used | 409 | An order with this nonce already exists for this wallet. Ask GET /api/nonce again. |
| replayed | 409 | This exact signed action has been applied already. |
| stale_oracle | 503 | The market's feed has stopped updating; nothing opens or settles against an old price. |
| insufficient_balance | 422 | The free balance does not cover margin plus the opening fee, or the margin to add. |
| crossed | 422 | The index is already past the stop or target's level; close now instead. |
| open_interest_cap | 422 | This side of the market is at its open-interest cap, the market registry's figure on both venues. |
| account_cap | 422 | The order would take this account past its open-interest limit in the market. |
| refused | 422 | Chain venue: the contract refused the position tool when the relayer simulated it; the message carries its reason. |
| not_found | 404 | No such open position, or not on this account. |
| rate_limited | 429 | Too many requests in the last minute from this IP or this address. |
