Skip to content
Agent API · Beta

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.

Give this to your AI (a taste)
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

  1. Open Settings and find the API keys section (you need beta access - see request access if you do not have it yet).
  2. 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.
  3. 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.

Give this to your AI

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/reset wipes 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. A pk_live_ key reaches only /me and /health today.
  • 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. markPrice and unrealizedPnL come back null when the feed did not price a symbol on that read. Skip it and re-read; never treat null as 0.
  • Do not blindly retry a write. There is no idempotency key yet. If POST /sandbox/orders times out, read /sandbox/positions first 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:

PitfallWhat you seeFix
Wrong key environment403 wrong_environmentUse a pk_test_ key on /sandbox/*. A pk_live_ key cannot trade.
Header dropped on redirect401 missing_api_keyCall the /v1 base directly. Some clients drop X-API-Key when following the /api/v1 redirect.
Symbol not tradeable400 bad_symbolUse a symbol from /sandbox/markets. Pass the plain symbol (BTC), not a decorated form.
Order too large for the caps400 cap_exceeded (or a specific cap code)Lower size, or read the account floors and size within them.
Treating a null mark as 0Nonsense PnL, phantom liquidationsSkip a null markPrice and re-read; the feed self-heals.
Blind retry after a timeoutA doubled positionRead /sandbox/positions before resending a write. No idempotency yet.
Give this to your AI

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.