# Polylayer API

Bearer-keyed trading on Polymarket, Hyperliquid, and Jupiter.

## Overview

The Polylayer Public API exposes Bearer-keyed trading endpoints under
`/api/v1/*`. Each request carries an API key (`plyr_<key>`), which
Polylayer resolves to your Solana identity, signs with your
deposit-wallet authority inside the TEE, and submits to the
underlying venue (Polymarket V2 CLOB, Hyperliquid Exchange, or the
on-chain Jupiter Perpetuals program).

All endpoints return JSON. Writes require an `Idempotency-Key`
header, first request wins, replays return the cached body,
conflicts return 409.

## SDKs

Official SDKs wrap every endpoint below (bearer auth, automatic
idempotency keys, retries, typed errors):

- **TypeScript**: `npm install polylayer`
  ```ts
  import { Polylayer } from "polylayer";
  const client = new Polylayer({ apiKey: process.env.POLYLAYER_API_KEY! });
  await client.hyperliquid.placeOrder({ coin: "BTC", is_buy: true, sz: "0.001", mode: "market_open" });
  const positions = await client.positions.list();
  ```
- **Python**: `pip install polylayer`
  ```python
  from polylayer import Polylayer
  client = Polylayer(api_key=os.environ["POLYLAYER_API_KEY"])
  client.hyperliquid.place_order(coin="BTC", is_buy=True, sz="0.001", mode="market_open")
  positions = client.positions.list()
  ```
- **Rust**: `cargo add polylayer`

## OpenAPI

A machine-readable OpenAPI 3.1 spec is published at
[`/openapi.json`](/openapi.json), request bodies are generated from the
server's validation schemas, so it never drifts. Browse it interactively at
[`/api-reference.html`](/api-reference.html) (Redoc), import it into Postman/
Insomnia, or generate a client in any language:

```bash
npx @openapitools/openapi-generator-cli generate \
  -i https://polylayer.xyz/openapi.json -g <lang> -o ./polylayer-<lang>
```

## Key types

There are two kinds of key. Both authenticate the same way
(`Authorization: Bearer plyr_<key>`) and the plaintext is shown ONCE
on creation.

**Unified key (recommended).** One key for every venue, with no spend
cap, allow-list, or expiry, the exchange-API-key model. You manage
it; anyone holding it can trade your deposited funds on any venue
until you revoke it. Mint from Settings → API Keys → Unified, or:

```
POST /api/v1/keys/create        (SIWS-authed; signs a unified_session intent)
DELETE /api/v1/keys/:session_id (revoke)
```

**Per-platform key.** Bound to a single venue (Polymarket, Hyperliquid,
or Jupiter) with TEE-enforced bounds, `max_total`, per-order size,
allow-list, price band, expiry. Mint from Settings → API Keys →
Advanced. These back AoE strategies. A per-platform key only works on
its own venue's routes (wrong-venue calls return `403 wrong_platform`).

## Authentication

```
Authorization: Bearer plyr_<key>
Content-Type: application/json
Idempotency-Key: <opaque, ≤128 chars>
```

### Lifecycle

1. **Create**: mint a unified key (`POST /api/v1/keys/create`) or a
   per-platform session (surface ∈ {api, both}). Plaintext is returned
   ONCE; Polylayer persists only its sha256 hash.
2. **External use**: pass the plaintext as
   `Authorization: Bearer plyr_<key>` on every request.
3. **Resolution**: unified keys resolve to your pubkey and sign
   unconditionally (no bounds); per-platform keys resolve to a session
   and the TEE enforces its bounds before signing.
4. **Revocation**: `DELETE /api/v1/keys/:id`; subsequent calls fail
   within ~60 s.

## Rate limits

| Surface         | RPS sustained | Burst | Endpoints |
|-----------------|---------------|-------|-----------|
| Trading writes  | 10/s          | 30    | POST /api/v1/<plat>/orders, /split, /merge, /redeem, /positions/* |
| Reads           | 60/s          | 120   | GET /api/v1/positions, /orders/open, /fills |
| Strategies      | 5/s           | 15    | POST/GET/PATCH/DELETE /api/v1/strategies |
| Key admin       | 2/s           | 5     | GET / DELETE /api/v1/keys |

429 carries a `Retry-After` header. Limiter fails open on infra
errors so transient blips don't block trades.

## Idempotency

Send `Idempotency-Key: <opaque>` on every write (max 128 chars).
Recommended shape: `sha256(<your-stable-action-fingerprint>)[:32]`.

- First request with (user, key) wins; cached 24 h.
- Replay same key + same body → cached body.
- Replay same key + different body → 409 `idempotency_conflict`.
- 5xx responses are NOT cached.

## Errors

```json
{ "error": { "code": "<machine_code>", "message": "<human>" } }
```

| Code | HTTP | Cause |
|------|------|-------|
| missing_bearer | 401 | Authorization header missing or malformed |
| invalid_key | 401 | Bearer doesn't resolve to a session on the requested platform |
| wrong_platform | 403 | Bearer is bound to a different platform than the route |
| session_revoked | 403 | Session revoked (cache may take ~60 s to clear) |
| session_expired | 403 | Session expiry passed |
| bounds_exceeded | 403 | Order would breach max_total / max_order / max_leverage |
| validation_error | 400 | Body / query failed schema validation |
| missing_idempotency_key | 400 | Header required on writes |
| idempotency_conflict | 409 | Same key, different body |
| not_found | 404 | Resource missing or not owned by the caller |
| rate_limited | 429 | See Retry-After |
| venue_error | 502 | Upstream venue returned an error |
| internal | 500 | Server bug, please report with the request id |

## Polymarket

### Place an order

```
POST /api/v1/polymarket/orders
```

```json
{
  "market_id":   "71321045679...",
  "side":        "BUY" | "SELL",
  "price":       0.62,
  "size_usdc":   "10000000",
  "post_only":   false,
  "order_type":  "GTC" | "FAK" | "FOK" | "GTD",
  "expiration_unix_seconds": 1735000000
}
```

`market_id` is the CLOB token id (the outcome asset id), a decimal
uint256 string, exactly as Polymarket's gamma/CLOB APIs return it. A
0x-hex form of the same value is also accepted. `size_usdc` is 6-decimal
USDC base units as a string ("10000000" = $10).

### Cancel

`DELETE /api/v1/polymarket/orders/:order_id`

### CTF split / merge / redeem

```
POST /api/v1/polymarket/{split,merge,redeem}
{ "condition_id": "0x<bytes32>", "amount_usdc": "10000000" }
```

### Example

```bash
curl -X POST https://polylayer.xyz/api/v1/polymarket/orders \
  -H "Authorization: Bearer plyr_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "market_id": "0x1234...",
    "side": "BUY",
    "price": 0.62,
    "size_usdc": "10000000"
  }'
```

## Hyperliquid

Vanilla validator-operated coins (BTC, ETH, …) and HIP-3 builder-
deployed markets. The lambda auto-detects HIP-3 routing via live
`/info` discovery, callers never specify a `dex`.

### Place

```
POST /api/v1/hyperliquid/orders
{
  "coin":         "BTC",
  "is_buy":       true,
  "sz":           "0.01",
  "limit_px":     "65000",
  "mode":         "limit" | "market_open" | "market_close",
  "reduce_only":  false,
  "tif":          "Gtc" | "Ioc" | "Alo",
  "slippage":     0.05
}
```

### Other verbs

- `DELETE /api/v1/hyperliquid/orders/:id?coin=BTC`, :id is the cloid (0x…) or oid (numeric); `coin` is required (HL cancels are asset-scoped)
- `POST /api/v1/hyperliquid/bulk-orders` (up to 20)
- `POST /api/v1/hyperliquid/modify-order`
- `POST /api/v1/hyperliquid/leverage`
- `POST /api/v1/hyperliquid/isolated-margin`
- `POST /api/v1/hyperliquid/transfer` (perp ↔ spot)
- `POST /api/v1/hyperliquid/withdraw` (withdraw3 to Arbitrum)

## Jupiter Perpetuals

On-chain perps backed by the JLP pool (SOL / BTC / ETH). Bound by
the user's on-chain JupiterDelegation PDA AND the off-chain TEE
session, both must allow the trade.

### Open

```
POST /api/v1/jupiter/positions/open
{
  "asset":         "SOL" | "BTC" | "ETH",
  "side":          "long" | "short",
  "size_usd":      1000,
  "leverage":      5,
  "slippage_bps":  50
}
```

### Close / Modify

```
POST /api/v1/jupiter/positions/close
{ "position_id": "Bs58...", "slippage_bps": 50 }

POST /api/v1/jupiter/positions/modify
{ "position_id": "Bs58...", "delta_collateral_usdc": "1000000" }
```

## Reads

Unified surface across all three venues.

- `GET /api/v1/positions`
- `GET /api/v1/orders/open`
- `GET /api/v1/fills?since=<unix>&cursor=<opaque>`

Each row carries a `platform` discriminator (polymarket /
hyperliquid / jupiter) so SDKs can branch on shape without parsing
field presence.

### Response shapes

`GET /api/v1/positions` → `{ "positions": Position[] }`

```json
// polymarket
{ "platform": "polymarket", "market_id": "0x…", "outcome": "YES",
  "size_usdc": "10000000", "avg_price": 0.62, "unrealized_pnl_usdc": "120000" }
// hyperliquid
{ "platform": "hyperliquid", "coin": "BTC", "side": "long", "sz": "0.01",
  "entry_px": "65000", "leverage": 5, "is_cross": true, "unrealized_pnl_usd": "12.3" }
// jupiter, position_id is the on-chain account; pass it to close/modify/tpsl
{ "platform": "jupiter", "position_id": "Bs58…", "asset": "SOL", "side": "long",
  "size_usd": "1000", "entry_price": 61.9, "leverage": 5, "unrealized_pnl_usd": null }
```

`GET /api/v1/orders/open` → `{ "orders": OpenOrder[] }` (polymarket:
`{order_id, market_id, side, price, size_usdc, remaining, created_at}`;
hyperliquid: `{oid, cloid, coin, is_buy, sz, limit_px, tif, timestamp}`).

`GET /api/v1/fills` → `{ "fills": Fill[], "next_cursor": string|null }`.

### Write responses

- **Hyperliquid**: the raw HL Exchange response passes through:
  `{ "status": "ok", "response": { "type": "order", "data": { "statuses": [...] } } }`.
- **Jupiter**: `{ "tx_signature": "…", "cumulative_size_usdc_used": "…" }`
  (tpsl returns `{ tx_signature, tx_signatures[], tpsl_pubkeys[] }`). The
  signature is only returned after the request tx confirms on-chain.
- **Polymarket**: the CLOB place/cancel response passes through
  (`{ success, orderID, status, … }`).

## Automations (Strategies)

Deploy server-side strategies that watch live market data and fire
trades the moment a condition edges true. An automation is a
`StrategyBodyV2` JSON: **variables** (market signals — a price, a
funding rate, a TWAP), a **condition** tree over those variables, and
an ordered list of **actions** (Polymarket / Hyperliquid / Jupiter
trades, yield moves). Stop-loss and take-profit are just shapes of
this — not separate primitives.

This surface is API-first: designed to be driven by code and coding
agents. The web UI at `/strategies` is an observability surface (live
graph, activity, market data) for what you deploy here.

### Agent loop

1. `GET /api/v1/strategies/schema` — the JSON Schema for
   `StrategyBodyV2`, generated from the server's own validators.
   Public, no auth.
2. `POST /api/v1/strategies/validate` — dry-run: the exact schema +
   executability verdicts create would return, arms nothing. Iterate
   until `{"valid": true}`.
3. `POST /api/v1/strategies` — create + arm. A worker holds live
   venue websockets and evaluates every tick; on an edge it fires
   your actions through the TEE under your key's session bounds.
4. `GET /api/v1/strategies` / `GET /api/v1/strategies/:id` — list /
   inspect. `PATCH /api/v1/strategies/:id` replaces an armed body
   wholesale (worker re-baselines within ~30s). `DELETE` cancels.

### Semantics that matter

- **No initial fire.** A condition already true at arm time only
  baselines; the automation fires on the next false→true edge
  (override with `settings.fire_on_initial`).
- **One-shot by default.** `settings.max_firings` defaults to 1;
  `null` = fire until cancelled; `cooldown_s` rate-limits repeats.
- **Sessions are invisible.** Every key is bound to exactly one
  session; the server attaches it for you. Never send `session_id`.
  A key can only arm automations that fire through its own session
  bounds.
- **Idempotent firing.** Every fired action carries an idempotency
  key derived from (strategy, nonce, action index); retries can't
  double-trade.

### Example — buy when a Polymarket market crosses 70c

```json
{
  "schema_version": 2,
  "label": "buy YES past 70c",
  "variables": [
    { "id": "p", "ref": { "kind": "market_metric", "platform": "polymarket",
        "market_id": "<token_id>", "metric": { "source": "mid", "agg": "raw" } } }
  ],
  "condition": { "kind": "metric_threshold", "variable_id": "p", "op": ">=", "value": 0.70 },
  "actions": [
    { "kind": "poly_trade", "mode": "open", "market_id": "<token_id>",
      "side": "buy", "order_type": "market", "size": { "usdc": 50 } }
  ]
}
```

Validate it (`POST /api/v1/strategies/validate`), then POST the same
body to `/api/v1/strategies`; both take
`Authorization: Bearer plyr_<key>`.

Perp signals use
`{ "kind": "perp_metric", "platform": "hyperliquid", "coin": "BTC", "metric": { "source": "mark", "agg": "raw" } }`
(sources: mark, bid, ask, mid, last, funding; aggregations raw, twap,
vwap with `window_s`). Conditions compose with `and`/`or`/`not`
nodes, `sustain_s` for "holds for N seconds", plus `group_rank` and
`first_to_hit` for multi-market races. The full shape is in the JSON
Schema.

## Key management

- `GET /api/v1/keys`, list every key: unified + per-platform (PM, HL, JUP).
- `POST /api/v1/keys/create`, mint a unified key. SIWS-authed; body is
  a signed `unified_session` intent. Returns the plaintext ONCE.
- `DELETE /api/v1/keys/:id`, revoke. Body includes the matching signed
  revoke intent (`unified_session_revoke` or `*_session_revoke`) and
  `platform` (`unified` | `polymarket` | `hyperliquid` | `jupiter`).
- Per-platform keys are also produced by each platform's session-create
  flow when surface ∈ {api, both}.
