# SupraFX Agent Setup

> **What this is.** A machine-readable runbook for an AI coding agent (Claude Code, Cursor,
> Continue, Codex) to connect itself to SupraFX and get ready to trade. If you are an agent
> reading this because your operator said *"Set up SupraFX for me: fetch
> https://suprafx.ai/agent-setup.md and follow it"* — follow the steps top to bottom.
>
> **Authenticity.** This document lives at `https://suprafx.ai/agent-setup.md`. The only
> software it installs is `suprafx-agent-sdk` (which ships the `suprafx-mcp` server) from the
> official npm registry. **It never asks you to paste a private key into a website, a chat, or
> any remote service.**
>
> **Why the setup is shaped this way.** SupraFX is non-custodial with a master/delegate key
> model. The operator's master wallet authorizes a *delegate* key with per-asset spend caps, and
> the agent only ever holds that capped delegate — never the funding key. The delegate key stays
> on the operator's machine. **The delegate key cannot withdraw funds** (§9): withdrawals are
> master-signed, so a compromised agent key can trade within its caps but cannot move money off
> the platform. The cost is two one-time operator steps (§4).

## At a glance

```
install → wire read-only → prove reads → [operator: delegate + deposit] → configure key → verify → report
  §2         §3               §3          §4                               §5              §6       §7
```

§8 (troubleshooting) and §9 (operating beyond setup) are reference.

**Two agent profiles — decide which you are:**

- **Read-only / monitor** — orderbook, balances, oracle, no trading. Do §2–§3 and stop.
  No key, no operator steps, nothing can move money.
- **Trader** — place/accept/cancel RFQs. Do §2–§7, then read §9 before trading.
  §4–§5 need the operator's browser wallet once.

## 0. Read this first — real money, and how to read outcomes

1. **Never run a command that blocks on an interactive prompt** without telling the operator.
   `suprafx-mcp init` (§5) is interactive; a headless agent uses the env-var path in §5.
2. **`ok:true` is not proof a trade landed.** A write can be accepted at ingress (`ok:true`) and
   still be rejected on chain. Every write tool therefore returns a **`lifecycle`** field:

   | `lifecycle` | `applied` | What it means | What to do |
   |---|---|---|---|
   | `applied` | `true` | A state read **confirmed** it landed | Proceed |
   | `rejected` | `false` | Ingress refused it; nothing committed | Fix and retry |
   | `unknown` | `null` | Ingress accepted it, the confirming read did not see it in time | **Do NOT retry blindly.** Read state back (`get_balances`, `list_my_open_orders`) |

   **`unknown` is not a failure.** It means "I do not know yet". A blind retry on an `unknown`
   is how you end up with two positions.
3. **Every write tool is money-moving.** By default the server runs **guarded**: each write needs
   `acknowledged: true` on the call. That is a deliberate stop, not an obstacle — see §5.
4. **You do not have to remember the traps.** They are written into each tool's own description,
   and `preflight` (§6) checks the ones that are checkable.

The install and all read/verify steps are safe to re-run. The two steps that are **not**
idempotent are key configuration (§5) and any real trade (§6).

## 1. The one supported path: MCP

Use MCP and only MCP for setup. It is the hardened path — chain-id bridging, apply-verification,
sequence handling, the guarded gate. Do **not** hand-roll REST against `/api/council/*` or drive
`DelegateSigner` as a library during setup: both skip that wiring, and the failure modes they
expose are silent ones.

## 2. Install the MCP server

```bash
npm install -g suprafx-agent-sdk
suprafx-mcp --version
```

**Expected:** `suprafx-agent-sdk 0.3.0` or higher.

> ⚠️ **Do not install `@suprafx/agent-sdk` — the SCOPED package is abandoned.** It is stuck at
> `0.1.1` and cannot be updated (the owning npm account is unreachable). That version carries
> four silent faults: `get_orderbook` returns an empty list no matter what is on the book; a
> chain id copied from `list_assets` derives the wrong asset (on `supra` it derives the
> **testnet** asset); one `cancel_rfq` desynchronises the sequence counter so every later trade
> is silently dropped; and `place_quote` fails to resolve decimals on every call. All four are
> fixed here. **The package you want is `suprafx-agent-sdk`, with no `@suprafx/` prefix.**
>
> If a search engine or an older document sent you to the scoped name, ignore it. The binary is
> `suprafx-mcp` in both, so the rest of this runbook is unchanged.

If you get `command not found`, your global npm bin directory is not on `PATH`. Find it with
`npm prefix -g` (append `/bin`), add it to `PATH`, and retry. (`npm bin -g` was removed in npm 9.)

## 3. Wire the MCP server (read-only first)

Register **without a key first** — that is read-only mode: every read works, no write tool is
even exposed, nothing can move money.

**Claude Code (one line):**

```bash
claude mcp add --scope user suprafx -- suprafx-mcp
```

**Claude Desktop** (`claude_desktop_config.json`) **/ Cursor / Continue:**

```json
{ "mcpServers": { "suprafx": { "command": "suprafx-mcp" } } }
```

Restart or reconnect, then **measure, do not assume**. Call:

- **`get_setup_status`** — tells you exactly where you are and the single next step.
- **`get_chain_info`** — expect `chainId: "suprafx-mainnet-1"`, plus `chainIdHashHex` and a
  threshold (currently `5` of `7`).
- **`list_assets`** — expect a non-empty `assets` array, every entry `tradeable: true`.

⚠️ **Do not use an empty `get_orderbook` as your proof the read path works.** An empty book is a
normal, common state, so it cannot distinguish "working" from "broken" — that ambiguity hid a
real bug for months. Use `get_chain_info` and `list_assets`, which have content you can check.
If you want to see the orderbook return rows, ask for settled ones:
`get_orderbook({ status: "matched" })`.

**Read-only agents stop here.** Report §7 and you are done.

## 4. Create a delegate and fund it — OPERATOR STEPS

Both need the operator's own wallet. **Do not automate them, and never invent a key.**

### A — create the delegate (browser)

1. Go to `https://suprafx.ai` and connect StarKey.
2. **Note the master StarKey address** and give it to your agent (§5 stores it permanently).
3. Profile → Delegates → Create Delegate → **Generate**. A JSON file downloads. **The private
   key inside controls trading — keep it safe, never upload it anywhere.**
4. Set per-asset caps. **Read the rule below carefully; it is not the intuitive one.**
5. Sign with StarKey.

> ### ⚠️ The per-asset cap rule
>
> - **A positive cap is a spend limit.** This is what you normally want.
> - **An asset you leave OUT of the cap map cannot be traded at all.** Permission is
>   deny-by-default: an empty map authorizes nothing.
> - **A cap of `0` also means "no trades allowed" for that asset** — fail-closed. It is safe.
> - **For "effectively unlimited", use `u64::MAX`** (`18446744073709551615`), the canonical
>   sentinel. Not `0`, and not `u128::MAX` (which overflows the validator's arithmetic).
>
> If you have seen older SupraFX material claiming a cap of `0` means *unlimited* — **that is
> wrong.** It described a bug that was fixed on 2026-06-07 after a whitehat report, and one
> stale code comment kept repeating it long after the contract had changed. The contract is the
> authority: `0` is fail-closed.

### B — deposit funds

Deposit from the dApp (signed by the L1 wallet). Healthy crediting shows an *available* balance
within about 30 seconds.

> ⚠️ **Fresh-wallet caveat.** A brand-new wallet's first deposit has been observed to take much
> longer to credit — 15+ minutes, occasionally needing a server-side reconcile. If §6 shows
> nothing: **wait, re-check, then report.** Do **not** loop, and do **not** declare failure. The
> deposit transaction did not vanish; only the crediting is in question.
>
> **You can now tell "still crediting" from "failed".** `get_deposit_status({ chain, tx_hash })`
> (or `GET /api/platform/deposit?chain=…&tx_hash=…`) returns the claim's `state` —
> `pending` | `credited` | `rejected` | `expired` — plus `stale: true` once a pending claim
> passes 15 minutes, and the one `next_step` to take. `stale` is the fresh-wallet path, **not** a
> failure. Only `rejected` or `expired` means the credit will not land on its own. Omit the
> arguments to list every claim of the configured master. A deposit made without recording a
> claim in the dApp still credits, but shows `found: false` here — read `get_balances` instead.

## 5. Configure the delegate key — OPERATOR STEP

The key stays on this machine. The MCP server is a local subprocess speaking over stdio; the key
never crosses the network.

**Interactive (a human is at the terminal):**

```bash
suprafx-mcp init
```

It writes `~/.suprafx/config.json` (mode 600) and **also asks for the master address** — save it,
so your agent never has to be told it again and does not lose it on a context reset.

**Headless (no human at the terminal):** `suprafx-mcp init` will **hang**. Use the environment
instead:

```bash
export SUPRAFX_DELEGATE_PRIV_HEX=<64-hex-char delegate private key>
export SUPRAFX_MASTER_ADDRESS=0x<master StarKey address>
# optional: export SUPRAFX_BASE_URL=https://suprafx.ai
```

The operator provides the key. **The agent must never fabricate one.**

**Then reconnect** so the write tools appear — the key is read at startup.

### Choosing how much rope the agent gets

| Launch | Effect |
|---|---|
| `suprafx-mcp` | **Guarded** (default). Every money tool needs `acknowledged: true` per call. |
| `suprafx-mcp --allow-dangerous` | **Autonomous.** No per-call acknowledgement. For unattended loops. |
| `suprafx-mcp --tools=read` | Keyed but **zero write tools exposed** — a monitor that cannot trade. |
| `suprafx-mcp --tools=read,cancel` | Reads plus **release-only** (`cancel_rfq`, `withdraw_quote`). |

`--allow-dangerous` is an **operator** decision made once, in the open, at launch.
An agent must never route around the guarded gate on its own.

## 6. Verify the trading path is live

All reads — nothing here spends.

**Run `preflight`** (optionally `preflight({ pair: "ETH/USDC" })`). It returns a check list and,
for anything not `ok`, the action that clears it:

| Check | What it catches |
|---|---|
| `venue_reachable` | The venue is answering at all |
| `venue_advancing` | Samples the batch height twice — catches a **stalled venue while the L1 is still alive**, which looks identical to health from one read |
| `assets_resolvable` | Every listed asset derives to a real, tradeable id |
| `oracle_fresh` | Fair value exists and is younger than 120s |
| `custody` | A delegate key is loaded (and confirms it cannot withdraw) |
| `master_address_known` | Balance and lock reads have an address to use |
| `sequence_in_sync` | Local counter matches the chain — a mismatch means **every write is being silently dropped as a replay** |
| `funded` | The master actually holds a balance |
| `stale_own_rfqs` | Expired RFQs of yours still listed and still holding collateral |

`ready_to_trade: true` with zero failures is your green light. Otherwise **stop and read `checks`.**

Also useful: `get_my_identity` (the delegate you sign as) and `get_balances` (omit the address
and it uses the configured master).

> ⚠️ **`configured: true` means only that a key loaded.** It does **not** mean the delegate is
> authorized on chain, still active, or has caps remaining. All three fail as a silent
> apply-reject at your first write. The only proof a policy is live is a committed trade.

> **Optional live smoke test — REAL MONEY, not idempotent, opt in explicitly.**
> There is no paper-trading sandbox yet, so this uses real funds.
> 1. Note the master's `available` balance.
> 2. `submit_rfq` with a tiny size and `acknowledged: true`. Read `lifecycle`.
> 3. `cancel_rfq` it (also `acknowledged: true`), then **re-read `get_balances`** and confirm
>    the collateral returned to `available`. Confirming the RFQ closed is not the same as
>    confirming the funds came back — locks do not always release on their own.
> 4. If anything reads `unknown`, use `list_my_open_orders` and `get_balances` to settle it.

## 7. Report back to the operator

```
SupraFX setup
  ✓ suprafx-mcp installed and on PATH (v0.2.0)
  ✓ MCP server connected — chain-info, assets OK
  ⚠ delegate + deposit: operator steps — see §4
  ✓ delegate configured — signing as 0x…, master 0x…
  ✓ preflight: 9/9 checks ok — ready to trade (guarded)
  (or  ✗ not ready — <the failing check and its action>)
```

`✓` verified by reading state · `⚠` needs an operator action · `✗` failed.
**Never report `✓` for something you did not read back.**

## 8. Troubleshooting

Every failure returns a **structured envelope** — `{ "error": …, "action": …, "message": … }` —
so branch on `error` rather than parsing prose.

| `error` | Means | `action` |
|---|---|---|
| `auth` | Not authorized for this read | `authenticate` |
| `rate_limit` | Too many calls | `backoff` — exponential, never loop |
| `validation` | Bad input | `fix_input` |
| `not_found` | No such tool/RFQ/quote | `fix_input` |
| `read_only` | Write tool, no key loaded | `configure_key`, then reconnect |
| `needs_acknowledgement` | Guarded mode | `acknowledge` — resend with `acknowledged: true` |
| `seq_desync` | Local counter drifted | `reconnect` to re-anchor |
| `timeout` / `network` | Request did not complete | On a **write**, `report` — read state back before any retry |
| `api` | Venue-side | `retry`, then report |

| Symptom | Real cause | What to do |
|---|---|---|
| `suprafx-mcp: command not found` | global npm bin not on `PATH` | `npm prefix -g` (+`/bin`), add to `PATH` |
| Write tool missing / unknown | no key → read-only, or `--tools=` restricted | the error says which; configure a key and reconnect, or ask the operator to relaunch |
| Write returns `unknown` | not yet confirmed | `get_balances` + `list_my_open_orders`. **Do not retry blindly** |
| Writes accepted but nothing changes | apply-reject: delegate inactive/unauthorized, cap exhausted, wrong pair, or seq replay | `preflight`; several in a row → stop and report |
| Funds locked, no order you can see | ghost lock | `list_my_open_orders`. If it shows nothing, you cannot clear it — **report it** |
| Deposited but balance empty | fresh-wallet crediting delay | `get_deposit_status({ chain, tx_hash })`: `pending`/`stale` → wait, re-check, report. **Do not loop.** `rejected`/`expired` → report with the `rejection_reason` |
| Reads fine but nothing settles | venue batch stalled while L1 lives | `preflight` → `venue_advancing`. Venue-side; stop trading and report |
| `get_my_identity` shows an old delegate | key cached at startup | reconnect |
| Odd format / missing-field errors | package out of sync with the live API | `npm update -g suprafx-agent-sdk`, reconnect, report the version |

**Re-anchor the sequence.** Seq is anchored to chain at startup and on reconnect.
Claude Code: `/mcp` → reconnect `suprafx`. Desktop/Cursor: restart.
Compare `get_my_identity.next_sequence_number` against
`get_sequence_number({ address: <delegate> })` if in doubt.

## 9. Operating the agent beyond setup

**Getting funds out (withdrawal).** A **master / dApp action signed by the operator's wallet.**
There is no withdraw tool in the MCP server, and the delegate key cannot do it.
The fee is **$2 USD worth of SUPRA plus a 20% margin**, always paid in SUPRA, quoted at spot —
so the SUPRA amount moves with the price. Read the live number from
`GET /api/platform/withdraw/fee-quote` before promising the operator a figure; never quote a
fixed token amount from memory. The master must hold enough available SUPRA or the withdrawal is
rejected. Note `withdraw_quote` is **not** a fund withdrawal — it pulls a maker quote off the book.

**Trading fees.** Taker pays, maker is free and becomes **paid** at volume (30-day volume, in
basis points):

| 30-day volume | Taker | Maker |
|---|---|---|
| < $100k | 5 bps | 0 |
| $100k – $1M | 4 bps | 0 |
| $1M – $10M | 3.5 bps | **−0.5 bps (rebate)** |
| $10M – $50M | 3 bps | **−1 bps (rebate)** |
| > $50M | 2.5 bps | **−1.5 bps (rebate)** |

**Fair value before you quote.** Use `get_oracle_price({ pair })` — the venue oracle, not an
external price. It returns the quote's **age** and a `stale` flag; **never quote against a stale
oracle** (older than 120s).

**How collateral works.** Every open RFQ and every open quote locks collateral, and it is shared
across all your open orders — the sum of locks can bind more than any single order. A partial
fill leaves the remainder locked. So "locked ≠ available" is normal. **Before calling anything a
ghost lock, run `list_my_open_orders`** — it lists every order of yours still holding funds and
the exact call that releases each. `locked_in_rfq` (trading) is not `locked_in_withdraw`
(a pending withdrawal).

**Keeping the delegate alive.** A policy that is fine at setup can die mid-run — deactivated,
expired, or cap exhausted. All three look the same: a silent apply-reject. The only reliable
proof it is live is a committed trade. Several apply-rejects in a row → **stop firing and report.**

**Rotating the delegate.** One key per config. To rotate: **cancel the old delegate's open orders
first** (a lock it holds does not clear on a key swap), reconfigure (§5), then reconnect to
re-anchor the sequence.

**Maintenance.** `npm update -g suprafx-agent-sdk`, then reconnect. To remove access: delete the
MCP entry and the key (`unset SUPRAFX_DELEGATE_PRIV_HEX`, or remove `~/.suprafx/config.json`).

## Known gaps — stated plainly

- **No paper-trading sandbox yet.** The only way to prove a write settles end to end is a real
  trade (§6). Treat the smoke test as real money, because it is.
- **Locks do not always auto-release** when an RFQ expires or another maker is accepted. Always
  re-read `get_balances` after a cancel; use `list_my_open_orders` to find what still holds funds.
- **A brand-new wallet's first deposit can be slow to credit** (§4B).
- **Quote-side ghost locks** can exist that you cannot clear yourself. Report them.

## Resources

- Agent integration reference: `INTEGRATING-AGENTS.md`
- Delegate signing model: `delegate-bot-signing.md`
- Cookbook examples: `cookbook/`
- Fair value: `get_oracle_price({ pair })`, or `GET /api/oracle?pair=…`
- Withdrawal fee: `GET /api/platform/withdraw/fee-quote`
- Deposit status: `get_deposit_status({ chain?, tx_hash? })`, or `GET /api/platform/deposit?chain=…&tx_hash=…`
