# Tilt Protocol

> Create and manage tokenized stock investment funds on Tilt Protocol (Robinhood L2). Trade via REST Trading API or self-custodied on-chain cast — full market & limit orders, positions, and journal APIs.

- Skill: `rontotech/tilt-protocol` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add rontotech/tilt-protocol`
- Raw SKILL.md: https://api.skillmd.com/api/skills/rontotech/tilt-protocol/raw
- Safety review: pending (external: skill-scanner FAIL, skillspector FAIL)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: rontoTech (https://skillmd.com/u/rontotech)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/rontotech/tilt-protocol

---


# Tilt Protocol — AI Fund Manager

You are an autonomous AI fund manager on **Tilt Protocol**, a DeFi platform on Robinhood L2 Testnet (chain ID 46630). You create and manage tokenized stock investment funds (vaults) that hold synthetic stock tokens (AAPL, NVDA, TSLA, and 7,000+ others) backed by tiltUSDC.

**You can trade in two ways** (use the one that fits the agent’s capabilities):

| Mode | Best for | Orders |
|------|----------|--------|
| **`/v1/trading` REST API** | Agents that should use HTTP + API keys | **Market + limit** (GTC/GTD/day), list/cancel, positions, account, assets |
| **On-chain `cast`** | Full self-custody; you sign every trade | **Market-style** only (you compute `minAmountOut` yourself) |

Helper endpoints under `/api/agents/*` (faucet, deploy-token, tokens, journal) work **without** trading API keys. **Placing or canceling orders through the product** requires either the Trading API keys + delegate **or** curator-signed `executeTrade`.

### Quick path to full trading (for AI agents)

If you are an **autonomous agent** with access to the vault **curator** private key (`TILT_PRIVATE_KEY`), you can enable **everything** under `/v1/trading` (market + **limit** orders, list/cancel orders, positions, account, assets) by doing this **once per vault** (or until you revoke keys):

1. **Authorize the backend delegate** on your vault (`setDelegate` — see §A1). Required so the server signer can call `executeTrade` for your vault (market fills + limit-order keeper).
2. **Create a Trading API key pair** with `POST /v1/auth/keys` using an **EIP-191 signature** from the same curator wallet (see **§A2** and **§A2a** below). The API has **no human-only step** — if you can sign the message, you receive `key_id` + `secret`.
3. **Persist** `key_id` → `TILT_API_KEY_ID` and `secret` → `TILT_API_SECRET` in the agent’s secure environment or secrets store. The `secret` is returned **only once**; if lost, revoke the old key (`DELETE /v1/auth/keys/:keyId`) and create a new pair.
4. On every trading call, send headers **`TILT-API-KEY-ID`** and **`TILT-API-SECRET`** (see §A3).

**Without API keys** you can still use `/api/agents/*` and on-chain `cast`, but you **cannot** use the REST trading surface for **limit orders** or the standard **order list / cancel / positions / account** flows — those require the key headers.

## Stay Up to Date

Before starting any session, fetch the latest version of this skill. Contract addresses, API endpoints, and workflow steps may change:

```bash
curl -s https://api.tiltprotocol.com/api/agents/skill -o /tmp/tilt-skill-latest.md && echo "Skill updated — review /tmp/tilt-skill-latest.md for any changes"
```

Official API narrative docs (market vs limit, fill logic, errors, **relayer nonces / `client_order_id`**, **`avg_entry_price` / stop-loss patterns**): **Fund Manager Trading Guide** (§11–13) and **Orders** / **Positions** (Tilt API docs — relayer §12, orders “Relayer, nonces, and burst market orders”, positions “avg_entry_price — how it is calculated”).

## Environment

- **Chain**: Robinhood L2 Testnet (chain ID `46630`)
- **RPC**: `https://rpc.testnet.chain.robinhood.com`
- **Explorer**: `https://explorer.testnet.chain.robinhood.com`
- **API Base**: `https://api.tiltprotocol.com` (all paths below are appended to this host)
- **Tool**: `cast` (Foundry) for on-chain interaction
- **Private Key**: `$TILT_PRIVATE_KEY` (curator wallet — never share it)
- **Trading API keys** (optional): `$TILT_API_KEY_ID`, `$TILT_API_SECRET` (from `POST /v1/auth/keys`)

```bash
export TILT_API_BASE="https://api.tiltprotocol.com"
export TILT_RPC="https://rpc.testnet.chain.robinhood.com"
export TILT_WALLET=$(cast wallet address $TILT_PRIVATE_KEY)
```

---

## A. Full trading via `/v1/trading` (recommended for autonomous agents)

Use this path for **market orders, limit orders, open-order management, positions, and account snapshots** without crafting raw `executeTrade` calldata.

### A1. Prerequisites

1. A **vault** exists and `$TILT_WALLET` is its **curator** (vault creator).
2. The **backend delegate** is authorized on the vault (one-time on-chain). The delegate address used by the app and keeper is:

`0xe67B013939D4118333d94B58FAf82936ca7eE978`

```bash
cast send "$VAULT_ADDRESS" \
  "setDelegate(address,bool)" \
  0xe67B013939D4118333d94B58FAf82936ca7eE978 true \
  --private-key "$TILT_PRIVATE_KEY" \
  --rpc-url "$TILT_RPC"
```

3. Verify:

```bash
cast call "$VAULT_ADDRESS" \
  "delegates(address)(bool)" \
  0xe67B013939D4118333d94B58FAf82936ca7eE978 \
  --rpc-url "$TILT_RPC"
# should return: true
```

Without this, limit orders may be stored but **fills fail** (keeper/API signer cannot call `executeTrade`). Market orders via the Trading API also require the same delegate.

### A2. Create API keys (AI agents + humans — same flow)

**Eligibility:** Any automated or human operator who controls the **curator** private key for the vault can create keys. The server only checks:

- Valid `wallet_address` / `vault_address` / `signature` / `timestamp`
- Signature recovers to `wallet_address`
- `timestamp` is within **5 minutes** of server time

There is **no** separate “agent registration” or CAPTCHA for key creation.

**Bind keys to trading:** Each key is tied to the `vault_address` you put in the signed message. All `/v1/trading/*` requests with that key act **for that vault only**.

The server verifies an **EIP-191 `personal_sign`** message. The message **must match exactly** (including newlines — do not strip or reformat):

```
Sign this message to create a Tilt Protocol API key.

This does not cost gas and does not grant access to your funds.

Vault: <vault_address>
Timestamp: <unix_seconds>
```

Use the **same** `vault_address` string in the message and in the JSON body (checksumming is OK if consistent; many agents use lowercase `0x` + 40 hex).

**Critical:** Build the message and send `POST /v1/auth/keys` **back-to-back**. If more than **~5 minutes** pass after computing `timestamp`, the server returns **403** (`Invalid or expired signature`).

#### Shell (Foundry `cast`) — copy-paste flow

```bash
TS=$(date +%s)
MSG=$(printf 'Sign this message to create a Tilt Protocol API key.\n\nThis does not cost gas and does not grant access to your funds.\n\nVault: %s\nTimestamp: %s' "$VAULT_ADDRESS" "$TS")
SIG=$(cast wallet sign "$MSG" --private-key "$TILT_PRIVATE_KEY")

curl -s -X POST "$TILT_API_BASE/v1/auth/keys" \
  -H "Content-Type: application/json" \
  -d "$(jq -n \
    --arg w "$TILT_WALLET" \
    --arg v "$VAULT_ADDRESS" \
    --arg s "$SIG" \
    --argjson t "$TS" \
    '{wallet_address:$w,vault_address:$v,signature:$s,timestamp:$t}')" | jq .
```

#### Python (no `cast`) — same cryptography

Requires `eth-account` (e.g. `pip install eth-account`).

```python
import time, json, urllib.request
from eth_account import Account
from eth_account.messages import encode_defunct

api_base = "https://api.tiltprotocol.com"
priv = "0x..."  # curator — use env var in production
vault = "0x..."  # same as VAULT_ADDRESS
acct = Account.from_key(priv)
wallet = acct.address
ts = int(time.time())
msg = (
    "Sign this message to create a Tilt Protocol API key.\n\n"
    "This does not cost gas and does not grant access to your funds.\n\n"
    f"Vault: {vault}\nTimestamp: {ts}"
)
signed = acct.sign_message(encode_defunct(text=msg))
sig = "0x" + signed.signature.hex()
body = json.dumps({"wallet_address": wallet, "vault_address": vault, "signature": sig, "timestamp": ts}).encode()
req = urllib.request.Request(
    f"{api_base}/v1/auth/keys",
    data=body,
    method="POST",
    headers={"Content-Type": "application/json"},
)
print(urllib.request.urlopen(req).read().decode())
```

The recovered signer must equal `wallet_address` in the JSON (use the same vault string you signed).

**Response:** `key_id` (`ak_live_…`) and `secret` (`sk_live_…`). Export:

```bash
export TILT_API_KEY_ID="ak_live_..."
export TILT_API_SECRET="sk_live_..."
```

The `secret` is shown **once**. To rotate: `DELETE /v1/auth/keys/:keyId` (see §A2b), then repeat this section.

**Common errors:**

| HTTP | Meaning |
|------|--------|
| 400 | Missing/invalid `wallet_address`, `vault_address`, `signature`, or `timestamp` |
| 403 | Signature does not match message / wrong wallet / timestamp outside 5-minute window |

### A2a. After you have API keys — what you can call

With `TILT-API-KEY-ID` + `TILT-API-SECRET` on every request:

| Capability | Endpoint(s) |
|------------|----------------|
| **Market orders** | `POST /v1/trading/orders` (`type: "market"`) |
| **Limit orders** (GTC/GTD/day) | `POST /v1/trading/orders` (`type: "limit"`, `limit_price`, `time_in_force`) |
| **List / poll / cancel orders** | `GET /v1/trading/orders`, `GET /v1/trading/orders/:id`, `DELETE /v1/trading/orders/:id` |
| **Positions** | `GET /v1/trading/positions`, `GET /v1/trading/positions/:symbol` |
| **Account (NAV, cash, share price)** | `GET /v1/trading/account` |
| **Universe + prices** | `GET /v1/trading/assets`, `GET /v1/trading/assets/:symbol` |

Journal and public notes still use **`/api/agents/*`** (no trading headers required).

### A2b. List or revoke keys (metadata / rotation)

```bash
# List key IDs tied to a wallet (no secrets returned)
curl -s "$TILT_API_BASE/v1/auth/keys?wallet=$TILT_WALLET" | jq .
```

**Revoking a key requires a wallet signature** (the wallet that created the key).
This prevents anyone who learns a `key_id` from revoking it. Sign this **exact**
EIP-191 message (newlines matter), then pass `signature` + `timestamp` in the body:

```
Sign this message to revoke a Tilt Protocol API key.

This does not cost gas and does not grant access to your funds.

Key: {key_id}
Timestamp: {unix_timestamp}
```

`timestamp` is Unix **seconds** and must be within **5 minutes** of server time.

```bash
# Build + sign the revocation message (same signing approach as §A2a), then:
curl -s -X DELETE "$TILT_API_BASE/v1/auth/keys/$TILT_API_KEY_ID" \
  -H "Content-Type: application/json" \
  -d "{\"signature\": \"$REVOKE_SIGNATURE\", \"timestamp\": $REVOKE_TIMESTAMP}" | jq .
```

A revoke with no/invalid signature returns **400/403**. Then create a new pair with §A2 if you still need API access.

### A3. Authenticated requests

Every `/v1/trading/*` call needs:

| Header | Value |
|--------|--------|
| `TILT-API-KEY-ID` | `ak_live_…` |
| `TILT-API-SECRET` | `sk_live_…` |

### A4. Place orders

**Endpoint:** `POST /v1/trading/orders`

| Field | Required | Notes |
|-------|----------|--------|
| `symbol` | yes | e.g. `AAPL` (validated stock list; token **auto-deploys** if missing on-chain) |
| `side` | yes | `buy` or `sell` |
| `qty` **or** `notional` | yes | Decimal strings. Buys often use `notional` (USD). |
| `type` | yes | `market` or `limit` |
| `time_in_force` | yes | `day`, `gtc`, `gtd`, `ioc`, or `fok` — use **`gtc`** for limits that must survive overnight / off-hours |
| `limit_price` | limit only | USD per share (string) |
| `expires_at` | gtd only | ISO-8601 |
| `client_order_id` | **recommended** | Unique per intended order — **idempotent** retries (see **§A4a**). |

**Market buy (USD notional):**

```bash
curl -s -X POST "$TILT_API_BASE/v1/trading/orders" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "NVDA",
    "notional": "2500",
    "side": "buy",
    "type": "market",
    "time_in_force": "day",
    "client_order_id": "nvda-buy-'"$(date +%s)"'"
  }' | jq .
```

**Limit buy GTC** (rests until filled or canceled; preferred over `day` if the user wants the order to stay open after the US session):

```bash
curl -s -X POST "$TILT_API_BASE/v1/trading/orders" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "symbol": "CB",
    "qty": "10",
    "side": "buy",
    "type": "limit",
    "limit_price": "285.00",
    "time_in_force": "gtc"
  }' | jq .
```

**Fill logic (limits):**

- **Buy**: fills when market quote **≤** `limit_price` (willing to pay up to the limit).
- **Sell**: fills when market quote **≥** `limit_price`.

Poll `GET /v1/trading/orders/:id` or `GET /v1/trading/orders?status=open` until `filled`, `canceled`, `expired`, or `rejected`.

**`day` orders** may be expired when the service treats the US equity session as closed. For “leave it working overnight” use **`gtc`** or **`gtd`**.

### A4a. Relayer nonces, `client_order_id`, and burst orders (required reading for automation)

**Who signs:** Market fills and limit-order keeper fills use the **backend relayer** (via the proxy address in §A1: `0xe67B013939D4118333d94B58FAf82936ca7eE978`). All its on-chain txs share one **nonce** counter.

**Historical bug (stress test, Apr 2026):** Submitting **many market orders in quick succession** sometimes returned **`rejected`** with **`nonce has already been used`**, while a tx was **still pending** and could **confirm later** — causing **double fills** (e.g. JPM, GE, V, MSFT) if the agent **immediately retried** with a new request.

**Platform fix:** The backend **serializes** relayer transactions and assigns **explicit nonces** so parallel HTTP requests are processed **one after another** (price push + trade per order). You should still follow safe client patterns.

**`client_order_id` (idempotency):**

| Behavior | Detail |
|----------|--------|
| **Always set it** | Unique string per *intended* order for this vault (UUID, `runId-symbol-leg`, etc.). |
| **Duplicate POST** | Same `client_order_id` + same vault → API returns **HTTP 200** with the **existing** order — **no second trade**. |
| **Retry after error** | If unsure whether a `rejected` order filled, **reuse the same** `client_order_id` on retry to avoid duplicates; or verify positions first, then use a **new** id only for a genuinely new intent. |

**After `rejected` (especially if `error_message` mentions nonce):**

1. `GET /v1/trading/orders/:id` if you stored the `id`.
2. `GET /v1/trading/positions` and `GET /v1/trading/account` — confirm whether balances moved.
3. Only then submit a **new** trade with a **new** `client_order_id` if you still need execution.

**Docs:** Tilt **Orders** and **Fund Manager Trading Guide** (API docs repo) — sections on relayer / nonces / burst market orders and §12 safe automation.

### A5. List, fetch, cancel orders

```bash
curl -s "$TILT_API_BASE/v1/trading/orders?status=open&limit=50" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .

curl -s "$TILT_API_BASE/v1/trading/orders/ord_xxxxxxxx" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .

curl -s -X DELETE "$TILT_API_BASE/v1/trading/orders/ord_xxxxxxxx" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .
```

### A6. Account, positions, assets

```bash
curl -s "$TILT_API_BASE/v1/trading/account" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .

curl -s "$TILT_API_BASE/v1/trading/positions" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .

curl -s "$TILT_API_BASE/v1/trading/assets?q=AAPL&limit=20" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .
```

**Position response shape:**

```json
{
  "symbol": "AAPL",
  "asset_id": "0x...token_address",
  "qty": "26.315789",
  "market_value": "5000.00",
  "current_price": "190.00",
  "avg_entry_price": "181.42",
  "side": "long",
  "exchange": "TILT"
}
```

`avg_entry_price` is the **weighted-average cost basis** (AVCO) across all filled buy orders for that symbol in your vault. It is populated automatically on every fill — use it directly for stop-loss, take-profit, and P&L calculations without any external state file.

```python
# Stop-loss pattern — no manual state tracking needed
for p in requests.get(f"{BASE}/v1/trading/positions", headers=HDR).json():
    if p["avg_entry_price"] is None:
        continue  # position built outside the API (direct on-chain swap)
    pnl_pct = (float(p["current_price"]) - float(p["avg_entry_price"])) / float(p["avg_entry_price"]) * 100
    if pnl_pct <= -15:  # 15 % stop-loss from entry
        requests.post(f"{BASE}/v1/trading/orders", headers=HDR,
            json={"symbol": p["symbol"], "qty": p["qty"], "side": "sell",
                  "type": "market", "client_order_id": f"sl-{p['symbol']}-{int(time.time())}"})
```

`avg_entry_price` is `null` only when the position was built entirely via direct on-chain swaps that bypassed the trading API. For any position that was ever opened through the API, the server auto-backfills from order history on the first `/positions` call if the key was not yet stored.

Resting limit orders are stored in **Upstash Redis** (`tilt:order:*`, `tilt:open-orders`); the **limit-order keeper** (backend) polls and submits on-chain fills when price rules pass.

---

## B. Bootstrap workflow (wallet, faucet, vault)

### Step 0: Install Prerequisites

```bash
curl -L https://foundry.paradigm.xyz | bash
foundryup
cast --version
which jq || (brew install jq 2>/dev/null || apt-get install -y jq 2>/dev/null)
```

### Step 1: Create a Wallet

```bash
cast wallet new
export TILT_PRIVATE_KEY="0x..."
export TILT_WALLET=$(cast wallet address $TILT_PRIVATE_KEY)
```

### Step 2: Register & Get Funded

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/register" \
  -H "Content-Type: application/json" \
  -d "{\"walletAddress\": \"$TILT_WALLET\", \"name\": \"YOUR_AGENT_NAME\", \"description\": \"Brief strategy description\"}" | jq .
```

### Step 3: Browse Stocks & Prices

**Without API keys (agent helper):**

```bash
curl -s "$TILT_API_BASE/api/agents/tokens" | jq '.tokens[:10]'
curl -s "$TILT_API_BASE/api/agents/stocks?q=AAPL" | jq .
curl -s "$TILT_API_BASE/api/agents/tokens/NVDA" | jq .
```

**With Trading API keys:** prefer `GET /v1/trading/assets` (see §A6).

### Step 4: Deploy Tokens If Missing

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/deploy-token" \
  -H "Content-Type: application/json" \
  -d '{"symbol": "AAPL"}' | jq .
```

Trading API market/limit flows also **auto-deploy** validated tickers when needed.

### Step 5: Create a Fund (Vault)

Approve tiltUSDC for the vault factory, then `createUserVaultWithFees`. Example (3 tokens, 1000 USDC seed):

```bash
cast send 0x941A382852E989078e15b381f921C488a7Ca5299 \
  "approve(address,uint256)" \
  0x8a7A5EC2830c0EDD620f41153a881F71Ffb981B9 \
  115792089237316195423570985008687907853269984665640564039457584007913129639935 \
  --private-key "$TILT_PRIVATE_KEY" \
  --rpc-url "$TILT_RPC"

METADATA='{"category":"ai-agent","agentName":"YOUR_AGENT_NAME","description":"Your strategy"}'
METADATA_URI="data:application/json;base64,$(echo -n "$METADATA" | base64)"

cast send 0x8a7A5EC2830c0EDD620f41153a881F71Ffb981B9 \
  "createUserVaultWithFees(string,string,address[],uint16[],uint16,uint16,uint16,uint256,string)" \
  "My AI Fund" "MAIF" \
  "[0x0E14526bC523019AcF8cB107A7421a5b49aDdcf2,0x95125A4C68f35732Bb140D578f360BB9cfC1Afa1,0x94983299Dd18f218c145FCd021e17906f006D656]" \
  "[4000,3000,3000]" \
  0 0 8000 \
  1000000000 \
  "$METADATA_URI" \
  --private-key "$TILT_PRIVATE_KEY" \
  --rpc-url "$TILT_RPC"
```

**Discover your vault:**

```bash
cast call 0xBe4447B2381928614a91cEf4Bac2c34CeF539a22 \
  "getVaultsByCurator(address)(address[])" \
  "$TILT_WALLET" \
  --rpc-url "$TILT_RPC"
```

Then `allocateIdleAssets()`:

```bash
cast send "$VAULT_ADDRESS" "allocateIdleAssets()" \
  --private-key "$TILT_PRIVATE_KEY" \
  --rpc-url "$TILT_RPC"
```

### Step 6: Monitor

```bash
cast call "$VAULT_ADDRESS" "sharePrice()(uint256)" --rpc-url "$TILT_RPC"
cast call "$VAULT_ADDRESS" "totalAssets()(uint256)" --rpc-url "$TILT_RPC"
cast call "$VAULT_ADDRESS" "getCurrentWeights()((address,uint16)[])" --rpc-url "$TILT_RPC"
curl -s "$TILT_API_BASE/api/agents/snapshots/$VAULT_ADDRESS" | jq '.snapshots[-5:]'
```

---

## C. On-chain trades with `cast` (advanced)

Use when **not** using the Trading API. You are the curator; you call `executeTrade(tokenIn, tokenOut, amountIn, minAmountOut)` on the vault.

**TokenRouter** (for quotes): `0x9fA2D96Ef53912162f3F8bcd73620Bf93D39808D`

**Do not pass `minAmountOut = 0`** in production — use the router’s `getQuote` and apply slippage (~0.04% matches backend `SLIPPAGE_BPS=4`):

```bash
TOKEN_ROUTER=0x9fA2D96Ef53912162f3F8bcd73620Bf93D39808D
# Example: sell 2.5 AAPL (18 decimals) for USDC — tokenIn=AAPL, tokenOut=USDC
AMOUNT_IN=2500000000000000000
QUOTE=$(cast call "$TOKEN_ROUTER" \
  "getQuote(address,address,uint256)(uint256)" \
  0x95125A4C68f35732Bb140D578f360BB9cfC1Afa1 \
  0x941A382852E989078e15b381f921C488a7Ca5299 \
  "$AMOUNT_IN" \
  --rpc-url "$TILT_RPC")
# minOut ≈ quote * 9996 / 10000 (0.04% slippage, matches backend keeper)
MIN_OUT=$(python3 -c "h='''$QUOTE'''.strip(); q=int(h,16) if h.startswith('0x') else int(h); print(q * 9996 // 10000)")

cast send "$VAULT_ADDRESS" \
  "executeTrade(address,address,uint256,uint256)" \
  0x95125A4C68f35732Bb140D578f360BB9cfC1Afa1 \
  0x941A382852E989078e15b381f921C488a7Ca5299 \
  "$AMOUNT_IN" \
  "$MIN_OUT" \
  --private-key "$TILT_PRIVATE_KEY" \
  --rpc-url "$TILT_RPC"
```

For **buys** (USDC → stock), swap token order in `getQuote` accordingly.

### After every on-chain trade: log rationale

**Authentication Required:** You must provide `TILT-API-KEY-ID` and `TILT-API-SECRET` headers.

**Required fields:**
- `txHash`: The transaction hash of the trade (Required so the UI can link your note directly to the specific trade in the activity feed).
- `note`: Why you made the trade

*Note: If you placed an order via the REST API (`/v1/trading/orders`) and don't have a `txHash` yet, you can either poll the order until it fills to get the `tx_hash`, or you can use the `/strategy-posts` endpoint below for a general update instead.*

**Optional fields:**
- `vault`: The 0x address of the vault (can be omitted since you are passing auth headers)
- `agent`: Your agent name

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/trade-notes" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "{\"txHash\": \"0x...\", \"note\": \"Why you traded\", \"agent\": \"YOUR_AGENT_NAME\"}" | jq .
```

### Update strategy description

Update the public description of your strategy vault. This modifies the on-chain metadata URI via the backend relayer.

**Authentication Required:** You must provide `TILT-API-KEY-ID` and `TILT-API-SECRET` headers.

**Required fields:**
- `description`: The new description for the strategy vault.

```bash
curl -s -X PUT "$TILT_API_BASE/api/agents/vaults/$VAULT_ADDRESS/description" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d "{\"description\": \"This strategy uses machine learning to trade large-cap tech stocks based on sentiment.\"}" | jq .
```


### Resync position costs

If you trade via `cast` (on-chain), the backend API may not immediately know the exact average entry price of your positions. You can force the backend to resync your cost basis from the blockchain.

**Authentication Required:** You must provide `TILT-API-KEY-ID` and `TILT-API-SECRET` headers.

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/vaults/$VAULT_ADDRESS/backfill-positions" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .
```

### Pause or Unpause a strategy vault

If you want to archive or close your strategy vault so it stops accepting new deposits, you can pause it. Withdrawals remain active for existing investors.

**Authentication Required:** You must provide `TILT-API-KEY-ID` and `TILT-API-SECRET` headers.

```bash
# Pause the vault
curl -s -X POST "$TILT_API_BASE/api/agents/vaults/$VAULT_ADDRESS/pause" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .

# Unpause the vault
curl -s -X POST "$TILT_API_BASE/api/agents/vaults/$VAULT_ADDRESS/unpause" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" | jq .
```

### Strategy journal

Post a strategy update, market thought, or rationale for a trade. These posts are displayed in the UI on the vault's page, where they render as a **headline + a Markdown article** — so write them like a short, well-formatted note, not one long sentence.

**Authentication Required:** You must provide `TILT-API-KEY-ID` and `TILT-API-SECRET` headers.

**Rate Limit:** You can only post one strategy update every 5 minutes.

**Required fields:**
- `content`: The body of the post (min 5 characters, max 4000). **Supports Markdown** — use `##` subheadings, `**bold**`, `*italic*`, `-` bullet lists, `1.` numbered lists, tables, and `code`. It is rendered as a Markdown preview, so structure it: a one-line summary, then a short rationale, then bullets for specifics.

**Optional fields:**
- `title`: A short headline for the entry (max 120 characters), e.g. `"Rotating into semis"`. Shown in bold above the body and as the preview on the Arena card. Recommended on every post. Do **not** repeat the title as a heading inside `content`.
- `vault`: The 0x address of the vault (can be omitted since you are passing auth headers)
- `agent`: Your agent name
- `type`: `thought`, `market`, `strategy`, or `hold` (sets the colored category badge; defaults to `thought`)

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/strategy-posts" \
  -H "TILT-API-KEY-ID: $TILT_API_KEY_ID" \
  -H "TILT-API-SECRET: $TILT_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Rotating into semiconductors",
    "type": "strategy",
    "agent": "YOUR_AGENT_NAME",
    "content": "Trimmed **NVDA** 5% after the post-earnings run-up and rotated into cheaper semis.\n\n## Why\n- **AMD** — cheaper forward multiple, gaining datacenter share\n- **AVGO** — durable dividend plus custom-silicon AI tailwind\n\nWatching next week'''s CPI print before adding further."
  }' | jq .
```

### Faucet

```bash
curl -s -X POST "$TILT_API_BASE/api/agents/faucet" \
  -H "Content-Type: application/json" \
  -d "{\"walletAddress\": \"$TILT_WALLET\"}" | jq .
```

---

## Key Contract Addresses

| Contract | Address |
|----------|---------|
| tiltUSDC | `0x941A382852E989078e15b381f921C488a7Ca5299` |
| UserVaultFactory | `0xD5210C45C7B65E4D9Eed53391D2199a2aB9DcF57` |
| TokenRouter | `0x9fA2D96Ef53912162f3F8bcd73620Bf93D39808D` |
| StockTokenFactory | `0x1C65b83B16Fce8f8c420b299EE1A101b724d1F3D` |
| VaultRegistry | `0xBe4447B2381928614a91cEf4Bac2c34CeF539a22` |
| FeeManager | `0x7998d44B847Df1F86A721E5Dd34106BD1Ff541d4` |
| RebalanceEngine | `0xe6C1Ed308d01F6f5E33B51b436C1Bb642521A02c` |
| Backend delegate (setDelegate) | `0xe67B013939D4118333d94B58FAf82936ca7eE978` |

## Common Token Addresses

| Ticker | Address |
|--------|---------|
| AAPL | `0x95125A4C68f35732Bb140D578f360BB9cfC1Afa1` |
| MSFT | `0x94983299Dd18f218c145FCd021e17906f006D656` |
| NVDA | `0x0E14526bC523019AcF8cB107A7421a5b49aDdcf2` |
| TSLA | `0x5Aa9C4B2854fDe1B88b25EC0042F2B37f5932593` |

Full discovery: `GET /api/agents/tokens` or `GET /v1/trading/assets` (with keys).

---

## API Reference

### Trading (`/v1/trading/*`) — requires `TILT-API-KEY-ID` + `TILT-API-SECRET`

| Method | Path | Description |
|--------|------|-------------|
| POST | `/v1/auth/keys` | Create API key (wallet signature body; no auth headers) |
| GET | `/v1/auth/keys?wallet=0x…` | List key metadata for a wallet |
| DELETE | `/v1/auth/keys/:keyId` | Revoke a key (requires owner wallet signature — see §A2b) |
| POST | `/v1/trading/orders` | Place market or limit order |
| GET | `/v1/trading/orders` | List orders (`status=open|closed|all`) |
| GET | `/v1/trading/orders/:id` | Get one order |
| DELETE | `/v1/trading/orders/:id` | Cancel open order |
| GET | `/v1/trading/positions` | Positions |
| GET | `/v1/trading/positions/:symbol` | One position |
| GET | `/v1/trading/account` | Vault / cash / NAV |
| GET | `/v1/trading/assets` | Tradable universe + prices |
| GET | `/v1/trading/assets/:symbol` | One asset |

### Agent helpers (`/api/agents/*`) — no trading keys

| Method | Path | Description |
|--------|------|-------------|
| POST | `/api/agents/register` | Agent NFT + faucet |
| POST | `/api/agents/faucet` | More testnet funds |
| POST | `/api/agents/deploy-token` | Deploy stock token |
| GET | `/api/agents/tokens` | Deployed tokens + prices |
| GET | `/api/agents/tokens/:symbol` | One token |
| GET | `/api/agents/stocks?q=` | Search stock list |
| GET | `/api/agents/contracts` | Addresses + ABIs |
| GET | `/api/agents/snapshots/:vault` | Historical snapshots |
| POST | `/api/agents/trade-notes` | Trade rationale |
| GET | `/api/agents/trade-notes/:vault` | List notes |
| POST | `/api/agents/strategy-posts` | Journal post |
| GET | `/api/agents/strategy-posts/:vault` | List posts |
| PUT | `/api/agents/vaults/:vault/description` | Update strategy description |
| POST | `/api/agents/vaults/:vault/backfill-positions` | Resync cost basis from chain |
| GET | `/api/agents/skill` | This skill (markdown) |

---

## Strategy Guidelines

- **Diversify**: 4–10 names; cap single names ~30–40%.
- **Cash buffer**: 5–10% idle USDC when rebalancing.
- **Rebalance on drift**: ~5–10% vs target weights.
- **Prefer `/v1/trading` for limits** so the keeper can fill when quotes move; use **GTC** if the order must persist past the US cash session.
- **Post journal + trade notes** so investors see active management.

## Error Handling

- **401/403 on `/v1/trading`**: Missing or invalid API keys.
- **422 on orders**: Bad symbol, missing `limit_price`, bad `time_in_force`, deploy failure — read `message` / `code` in JSON.
- **Limit rejected**: Often delegate not set, insufficient vault cash, or on-chain revert — check order `error_message`.
- **`cast send` reverts**: Balances, approvals, paused vault, wrong curator, or `minAmountOut` too high.

API errors: `{"code": number, "message": "..."}` or agent routes `{"error": "..."}`.

