# @4657/derive

> derive

- Skill: `starchild-ai-agent/4657-derive` (Agent Skill)
- Install (CLI): `npx skillmds@latest add starchild-ai-agent/4657-derive`
- Raw SKILL.md: https://api.skillmd.com/api/skills/starchild-ai-agent/4657-derive/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: starchild-ai-agent (https://skillmd.com/u/starchild-ai-agent)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/starchild-ai-agent/4657-derive

---


# Derive Skill

Derive is a self-custodial onchain options & perpetuals DEX built on the OP Stack (Derive Chain / Lyra Finance). It supports options, perpetuals, and spot across BTC, ETH, SOL, and more.

## API Endpoints

| | REST | WebSocket |
|---|---|---|
| **Mainnet** | `https://api.lyra.finance` | `wss://api.lyra.finance/ws` |
| **Testnet** | `https://api-demo.lyra.finance` | `wss://api-demo.lyra.finance/ws` |

All endpoints speak JSON-RPC 2.0. Public endpoints need no auth. Private endpoints require a signed login.

## ⚠️ Geo-restriction (US block)

The Lyra/Derive API blocks `Region US`. Starchild containers exit the US by default, so **every request must be routed through `sc-vpn` (Malaysia or other non-US region)**.

```python
PROXY_R = {"http": "http://my:x@sc-vpn.internal:8080", "https": "http://my:x@sc-vpn.internal:8080"}
PROXY_W = dict(http_proxy_host="sc-vpn.internal", http_proxy_port=8080,
               http_proxy_auth=("my","x"), proxy_type="http")

requests.post(URL, json=..., proxies=PROXY_R)           # REST
websocket.create_connection(WS_URL, **PROXY_W)          # WebSocket
```

Without the proxy you get `code 403 "You are in a restricted region that violates our terms of service."`

## Protocol Constants

⚠️ **WARNING:** The official `derive_action_signing` example ships with TESTNET constants. Using testnet values on mainnet returns `code 14014 "Signature does not match data"` with no other hint.

### MAINNET (verified from on-chain Matching contract `0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD`)

```
DOMAIN_SEPARATOR     = "0xd96e5f90797da7ec8dc4e276260c7f3f87fedf68775fbe1ef116e996fc60441b"
ACTION_TYPEHASH      = "0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17"
TRADE_MODULE_ADDRESS = "0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b"
MATCHING_ADDRESS     = "0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD"
DEPOSIT_MODULE       = "0x9B3FE5E5a3bcEa5df4E08c41Ce89C4e3Ff01Ace3"
WITHDRAW_MODULE      = ""  # query via explorer when needed
```

### TESTNET (from official example, do NOT use on mainnet)

```
DOMAIN_SEPARATOR     = "0x9bcf4dc06df5d8bf23af818d5716491b995020f377d3b7b64c29ed14e3dd1105"
ACTION_TYPEHASH      = "0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17"
TRADE_MODULE_ADDRESS = "0x87F2863866D85E3192a35A73b388BD625D83f2be"
```

### How to re-derive any mainnet contract

```python
import requests
# Find by name on the Lyra explorer
r = requests.get("https://explorer.lyra.finance/api/v2/search?q=TradeModule")
# Pull DOMAIN_SEPARATOR via JSON-RPC eth_call (selector for `domainSeparator()` = 0xf698da25)
import requests
payload = {"jsonrpc":"2.0","method":"eth_call","id":1,
  "params":[{"to":"0xeB8d770ec18DB98Db922E9D83260A585b9F0DeAD","data":"0xf698da25"},"latest"]}
r = requests.post("https://rpc.lyra.finance", json=payload)
print(r.json()["result"])  # the canonical mainnet DOMAIN_SEPARATOR
```

## Authentication Flow

1. **Smart contract wallet** (NOT your EOA): In the Derive UI go to Home → Developers → "Derive Wallet" to find it
2. **Session key**: A temporary EOA you authorize to sign on your behalf
3. **WS login**: Sign a timestamp with your session key private key, send as `public/login`

```python
from derive_action_signing import utils
from web3 import Web3

web3 = Web3()
login_params = utils.sign_ws_login(web3, DERIVE_WALLET, SESSION_KEY_PRIVATE_KEY)
# sends: { timestamp, wallet, signature }
```

## Execution: cost of crossing spread on illiquid options

Derive options often have very wide bid/ask spreads (30%+ of mark) because the book is mostly market-maker quotes, not organic flow. Crossing the spread (taking the ask on a buy) eats the full MM markup PLUS the taker fee.

**Rule of thumb for any order with premium > $5:**

1. **Default: ALO (post-only) limit at MID price** (`(bid + ask) / 2` or near mark). Pays maker fee (0.0001) instead of taker (0.0003). Won't fill instantly but saves real money on illiquid strikes.
2. **Only cross the spread when you need the fill NOW** — e.g. urgent hedge with hours to expiry, or thesis needs immediate protection.
3. **Estimate cost before you submit:** spread cost = (ask - mark) × contracts. If that's >5% of premium, post mid instead.

Example post-only order:
```python
payload = {
    "instrument_name": "ETH-20260521-2000-P",
    "direction": "buy",
    "order_type": "limit",
    "mmp": False,
    "time_in_force": "gtc",
    "is_atomic_signing": False,
    # Note: post-only is controlled via order params, check Derive docs for current field name
    **action.to_json(),
}
```

## Instrument Naming

```
Options:  {CURRENCY}-{YYYYMMDD}-{STRIKE}-{C|P}   e.g. ETH-20250131-3000-C
Perps:    {CURRENCY}-PERP                          e.g. BTC-PERP
Spot/ERC: {SYMBOL}                                 e.g. ETH
```

## Key Public Endpoints (no auth)

```python
import requests, json

BASE = "https://api.lyra.finance"

# All live options for ETH
r = requests.post(f"{BASE}/public/get_instruments",
    json={"expired": False, "instrument_type": "option", "currency": "ETH"},
    headers={"accept": "application/json", "content-type": "application/json"})

# Ticker with Greeks
r = requests.post(f"{BASE}/public/get_ticker",
    json={"instrument_name": "ETH-PERP"},
    headers={"accept": "application/json", "content-type": "application/json"})

# Order book
r = requests.post(f"{BASE}/public/get_orderbook",
    json={"instrument_name": "ETH-20250131-3000-C", "depth": 10},
    headers={"accept": "application/json", "content-type": "application/json"})

# All tickers for a currency
r = requests.post(f"{BASE}/public/get_tickers",
    json={"currency": "ETH", "instrument_type": "option"},
    headers={"accept": "application/json", "content-type": "application/json"})
```

## Key Private Endpoints (auth required via WS)

After `public/login` over WebSocket:

```python
# Positions
{"method": "private/get_positions", "params": {"subaccount_id": <id>, "currency": "ETH"}}

# Collaterals
{"method": "private/get_collaterals", "params": {"subaccount_id": <id>}}

# Open orders
{"method": "private/get_open_orders", "params": {"subaccount_id": <id>}}

# Order history
{"method": "private/get_order_history", "params": {"subaccount_id": <id>}}

# My trades
{"method": "private/get_trades", "params": {"subaccount_id": <id>}}

# Subaccounts
{"method": "private/get_subaccounts", "params": {"wallet": DERIVE_WALLET}}
```

## Placing an Order (Full Flow)

Requires: `derive_action_signing`, `web3`, `websocket-client` (`pip install derive_action_signing web3 websocket-client`)

```python
import json, requests
from decimal import Decimal
from websocket import create_connection
from web3 import Web3
from derive_action_signing import SignedAction, TradeModuleData, utils

# --- Config ---
DERIVE_WALLET       = "0xYOUR_SMART_CONTRACT_WALLET"
SESSION_KEY_PRIV    = "0xYOUR_SESSION_KEY_PRIVATE_KEY"
SUBACCOUNT_ID       = 12345

# --- Protocol constants (MAINNET — verified on-chain) ---
DOMAIN_SEPARATOR    = "0xd96e5f90797da7ec8dc4e276260c7f3f87fedf68775fbe1ef116e996fc60441b"
ACTION_TYPEHASH     = "0x4d7a9f27c403ff9c0f19bce61d76d82f9aa29f8d6d4b0c5474607d9770d1af17"
TRADE_MODULE_ADDRESS = "0xB8D20c2B7a1Ad2EE33Bc50eF10876eD3035b5e7b"
WS_URL              = "wss://api.lyra.finance/ws"
REST                = "https://api.lyra.finance"

web3 = Web3()
session_wallet = web3.eth.account.from_key(SESSION_KEY_PRIV)

# 1. Get instrument details
resp = requests.post(f"{REST}/public/get_instruments",
    json={"expired": False, "instrument_type": "option", "currency": "ETH"},
    headers={"content-type": "application/json"})
instrument = resp.json()["result"][0]   # pick the first live option

# 2. Build + sign action
action = SignedAction(
    subaccount_id=SUBACCOUNT_ID,
    owner=DERIVE_WALLET,
    signer=session_wallet.address,
    signature_expiry_sec=utils.MAX_INT_32,
    nonce=utils.get_action_nonce(),
    module_address=TRADE_MODULE_ADDRESS,
    module_data=TradeModuleData(
        asset_address=instrument["base_asset_address"],
        sub_id=int(instrument["base_asset_sub_id"]),
        limit_price=Decimal("100"),    # USD price for option
        amount=Decimal("1"),           # contracts
        max_fee=Decimal("1000"),
        recipient_id=SUBACCOUNT_ID,
        is_bid=True,                   # True=buy, False=sell
    ),
    DOMAIN_SEPARATOR=DOMAIN_SEPARATOR,
    ACTION_TYPEHASH=ACTION_TYPEHASH,
)
action.sign(session_wallet.key)

# 3. Connect, login, submit
ws = create_connection(WS_URL)

def ws_rpc(ws, method, params, id_str):
    ws.send(json.dumps({"method": method, "params": params, "id": id_str}))
    while True:
        msg = json.loads(ws.recv())
        if msg["id"] == id_str:
            return msg

login_resp = ws_rpc(ws, "public/login",
    utils.sign_ws_login(web3, DERIVE_WALLET, SESSION_KEY_PRIV),
    "login")
assert "result" in login_resp, f"Login failed: {login_resp}"

order_resp = ws_rpc(ws, "private/order", {
    "instrument_name": instrument["instrument_name"],
    "direction": "buy",
    "order_type": "limit",
    "mmp": False,
    "time_in_force": "gtc",
    **action.to_json(),
}, "order1")
print(json.dumps(order_resp["result"]["order"], indent=2))
ws.close()
```

## Required Credentials (save to workspace/.env)

```
DERIVE_WALLET=0x...          # Smart contract wallet on Derive Chain (NOT your EOA)
DERIVE_SESSION_KEY=0x...     # Private key of your session key EOA
DERIVE_SUBACCOUNT_ID=12345   # Integer subaccount ID
```

To find your wallet: derive.xyz → Home → Developers → "Derive Wallet"
To create a session key: derive.xyz → Settings → Session Keys → Create

## Env Check Pattern

```python
import os
from dotenv import load_dotenv
load_dotenv("/data/workspace/.env")

DERIVE_WALLET    = os.environ["DERIVE_WALLET"]
SESSION_KEY_PRIV = os.environ["DERIVE_SESSION_KEY"]
SUBACCOUNT_ID    = int(os.environ["DERIVE_SUBACCOUNT_ID"])
```

## Available Currencies

BTC, ETH, SOL, HYPE, and others. Use `public/get_currencies` to get the live list.

## Greeks (Options)

The `get_ticker` and `get_tickers` responses include for options:
- `delta`, `gamma`, `vega`, `theta`, `rho` — standard BSM Greeks
- `iv` — implied volatility
- `mark_price`, `index_price`, `best_bid_price`, `best_ask_price`

## Rate Limits

Per account TPS limits — check with `private/get_account`. WebSocket is preferred for trading; REST for reading.

## Testnet Setup

Use `https://api-demo.lyra.finance` (REST) and `wss://api-demo.lyra.finance/ws` (WS) for testing. The testnet has different protocol constants — look them up at docs.derive.xyz/reference/protocol-constants.

## Scripts Location

Working scripts go in `skills/derive/scripts/`. Share output in `output/derive/`.

