Build a trading agent on Perps Fund
A REST API for the Perps Fund sandbox: your own simulated paper account, priced off real marks, the same one the demo terminal runs. Built to be handed to an AI agent - paste a context block into Cursor or Claude Code and let it write the integration.
Written to be handed to an AI
Most people building trading agents today follow a tutorial and paste context into an assistant rather than write every line by hand. So the reference is built around that: every section carries a compact, self-contained context block with the base URL, your auth, the relevant endpoints, and the rules. Copy one into your AI and it has everything it needs for that task - no synthesizing from scattered docs.
I am building a trading agent on the Perps Fund sandbox API.
Base URL: https://api.perpsfund.com/v1
Auth: send my key in the header X-API-Key: pk_test_...
Quick check: GET /me -> { userId, environment, keyId }
Place an order: POST /sandbox/orders { "symbol": "BTC", "side": "long", "size": 0.01, "leverage": 5 }
It is a simulated paper account - no real money. Help me write a client for this API.Sandbox-first, real marks
Everything runs against a simulated paper account priced off real marks. Nothing here moves real money, so it is the right place to build, break, and rehearse a strategy - and to let an autonomous agent trade a few hundred times before you ever consider real capital.
Plain REST, honest about scope
One header authenticates you, requests and responses are JSON, and errors are stable machine-readable codes. Today the surface is the sandbox only: there is no live funded-account route, WebSocket, or SDK yet. We document what exists and mark what does not, rather than overpromise.
What you can do
Read your account
Balance, equity, unrealized PnL, the challenge floors and target, and lock status.
Place and close orders
Market and marketable-limit orders, partial closes, all at a fresh mark.
Take-profit and stop-loss
Set, edit, or clear trigger levels on any open position.
Live market data
The tradeable universe, firm leverage caps, and current marks.
Stable error codes
Every failure is a JSON body with a machine-readable code and a matching status.
OpenAPI 3.0.3 spec
A machine-readable spec for code generators, Postman, or a Swagger viewer.
Everything here runs against a sandbox: your own simulated paper account, the same one the in-app demo terminal drives, priced off real marks. Nothing here can move real money. That is the point - build and rehearse safely, then graduate to a funded account later.
Start here
The fastest path to a working call is three steps: get a key, check it, place an order. Under five minutes.
1. Get a key
- Open Settings and find the API keys section (you need beta access - see request access if you do not have it yet).
- Create a sandbox key. It starts with
pk_test_and is shown once, right after creation. Copy it then; the server only stores a hash and can never show it again. - Put it in an environment variable, never in your code or a commit:
export PERPSFUND_API_KEY="pk_test_your_key_here"
Treat the key like a password
Anyone holding the key can act as your agent. Keep it in an environment variable or a secrets manager, never in source control. You can revoke a key any time from Settings, and a new one takes its place.
2. Make your first call
GET /me resolves your key to its owner and environment. It is the quickest "is my key working?" check.
curl https://api.perpsfund.com/v1/me \
-H "X-API-Key: $PERPSFUND_API_KEY"
{ "userId": "clx0abc123", "environment": "sandbox", "keyId": "clx0def456" }
3. Place your first order
Open a small long on Bitcoin. The order fills at a fresh mark and returns the resulting position and account.
curl -X POST https://api.perpsfund.com/v1/sandbox/orders \
-H "X-API-Key: $PERPSFUND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"symbol":"BTC","side":"long","size":0.01,"leverage":5}'
That is the whole loop: authenticate with a header, send JSON, read JSON back. The full endpoint reference has every route in detail.
Test safely
The sandbox is the whole surface today, and that is a feature. Every key, every order, every position lives in a simulated paper account that mirrors the in-app demo terminal, priced off the same live marks the real markets use. You can be wrong, blow up, and try again with zero real risk.
- Nothing moves real money. There is no live funded-account trading endpoint yet - it is a deliberate future build with its own safeguards. Until then,
/sandbox/*is where trading happens. - Start clean whenever you want.
POST /sandbox/resetwipes your demo account back to its opening balance, so an experiment gone wrong is one call away from a fresh start. - The engine is real. Fills, fees, leverage caps, take-profit and stop-loss triggers, and liquidation all run the same logic a funded account would. What you learn here transfers.
A safe place to let an agent loose
Because the sandbox cannot touch real funds, it is the right place to hand the controls to an autonomous agent and watch how it behaves over a few hundred trades before you ever consider real capital.
Build an agent
An agent is a loop: read state, decide, act, repeat. Here is a minimal one, a reusable client to grow it from, the rules that keep it out of trouble, and the mistakes to skip.
A minimal agent
A complete, runnable skeleton. It resets the account, opens a small position with a stop-loss, then polls until the position closes. Read the comments; swap in your own logic where marked.
import os, time, requests
BASE = "https://api.perpsfund.com/v1"
KEY = os.environ["PERPSFUND_API_KEY"] # sandbox key, from an env var
H = {"X-API-Key": KEY, "Content-Type": "application/json"}
def get(path): return requests.get(BASE + path, headers=H).json()
def post(path, body=None): return requests.post(BASE + path, headers=H, json=body or {}).json()
post("/sandbox/reset") # start from a clean paper account
acct = get("/sandbox/account")["account"]
print("equity:", acct["equity"])
price = get("/sandbox/prices?symbol=BTC")["price"]
stop = round(price * 0.98, 2) # a 2% protective stop below entry
# --- your decision goes here: what, which side, how big ---
res = post("/sandbox/orders", {
"symbol": "BTC", "side": "long", "size": 0.01,
"leverage": 5, "stopLossPrice": stop,
})
if not res.get("ok"):
print("order rejected:", res.get("error"), res.get("reason"))
else:
print("filled at:", res["fill"]["price"])
# poll until the position is gone (a stop or take-profit closed it)
while True:
positions = get("/sandbox/positions")["positions"]
if not any(p["symbol"] == "BTC" for p in positions):
print("position closed")
break
time.sleep(5) # poll a few seconds apart - reads are a warm cache
A reusable client
A small wrapper so the rest of your agent reads cleanly. It raises on a non-ok trading response so a rejected order never passes silently.
import os, requests
class PerpsFund:
def __init__(self, key=None, base="https://api.perpsfund.com/v1"):
self.base = base
self.h = {"X-API-Key": key or os.environ["PERPSFUND_API_KEY"],
"Content-Type": "application/json"}
def _get(self, path):
return requests.get(self.base + path, headers=self.h, timeout=15).json()
def _post(self, path, body=None):
r = requests.post(self.base + path, headers=self.h, json=body or {}, timeout=15).json()
if body is not None and r.get("ok") is False:
raise RuntimeError(f"{r.get('error')}: {r.get('reason', '')}")
return r
# reads
def account(self): return self._get("/sandbox/account")["account"]
def positions(self): return self._get("/sandbox/positions")["positions"]
def markets(self): return self._get("/sandbox/markets")["markets"]
def price(self, symbol): return self._get(f"/sandbox/prices?symbol={symbol}")["price"]
# actions
def order(self, symbol, side, size, **opts):
return self._post("/sandbox/orders", {"symbol": symbol, "side": side, "size": size, **opts})
def close(self, symbol, pct=100):
return self._post("/sandbox/positions/close", {"symbol": symbol, "pct": pct})
def set_tpsl(self, symbol, take_profit=None, stop_loss=None):
return self._post("/sandbox/positions/tpsl",
{"symbol": symbol, "takeProfitPrice": take_profit, "stopLossPrice": stop_loss})
def reset(self): return self._post("/sandbox/reset")
Key rules for AI agents
Follow these and most integration bugs never happen:
- Use a
pk_test_key. Trading only works with a sandbox key on the/sandbox/*routes. Apk_live_key reaches only/meand/healthtoday. - Trust the fill, not the read. Price and position reads come from a warm cache and can lag a few seconds. Every order re-fetches the symbol fresh, so the fill price is the authoritative one.
- Poll, do not stream. There is no WebSocket yet. Read the REST endpoints on an interval (a few seconds is plenty); do not hammer them in a tight loop.
- A missing mark is not zero.
markPriceandunrealizedPnLcome backnullwhen the feed did not price a symbol on that read. Skip it and re-read; never treatnullas0. - Do not blindly retry a write. There is no idempotency key yet. If
POST /sandbox/orderstimes out, read/sandbox/positionsfirst to see whether it applied before you resend, or you may double-place. - Leverage is clamped for you. Ask for more than a market's cap and it is lowered to the cap, not rejected. Read the cap from
/sandbox/markets. - Limit orders must be immediately fillable. A limit price that is not marketable against the current mark returns
order_not_marketable; resting limit orders are not held.
Common pitfalls
The failures we see most, and the fix for each:
| Pitfall | What you see | Fix |
|---|---|---|
| Wrong key environment | 403 wrong_environment | Use a pk_test_ key on /sandbox/*. A pk_live_ key cannot trade. |
| Header dropped on redirect | 401 missing_api_key | Call the /v1 base directly. Some clients drop X-API-Key when following the /api/v1 redirect. |
| Symbol not tradeable | 400 bad_symbol | Use a symbol from /sandbox/markets. Pass the plain symbol (BTC), not a decorated form. |
| Order too large for the caps | 400 cap_exceeded (or a specific cap code) | Lower size, or read the account floors and size within them. |
Treating a null mark as 0 | Nonsense PnL, phantom liquidations | Skip a null markPrice and re-read; the feed self-heals. |
| Blind retry after a timeout | A doubled position | Read /sandbox/positions before resending a write. No idempotency yet. |
API access is in a controlled beta
Keys and the full endpoint reference are rolling out to a limited group. Once your account is enabled, the reference unlocks and you can create keys in Settings. Create an account, then request access.