@splitmarkets/sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # @splitmarkets/sdk — leverage your users can't get liquidated out of
2
+
3
+ Oracle-free, **non-liquidatable** ETH options on Base & Arbitrum, as a drop-in SDK. Every option is
4
+ physically backed 1:1, so a buyer's **max loss is the premium they paid** — no liquidation, no margin calls,
5
+ no oracle to game. Long = bet ETH up. Short = bet ETH down. Add it to your app in ~5 lines.
6
+
7
+ ```
8
+ ┌──────────────┐ getMarkets / getQuote / buy / close ┌─────────────────────┐
9
+ │ YOUR APP │ ─────────────────────────────────────▶ │ Split on Base/Arb │
10
+ │ + user wallet│ @splitmarkets/sdk (this package) │ no-liquidation opts│
11
+ └──────────────┘ ◀───────────────────────────────────── └─────────────────────┘
12
+ users click "Buy" you earn referral fees on the volume you route
13
+ ```
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ npm install @splitmarkets/sdk viem
19
+ # react is only needed if you use the optional <SplitTradeWidget/>
20
+ ```
21
+
22
+ ## Quickstart — the widget (the whole flow in one component)
23
+
24
+ ```tsx
25
+ import { SplitTradeWidget } from "@splitmarkets/sdk/widget";
26
+
27
+ // give it a connected viem wallet (wagmi: useWalletClient() + useAccount())
28
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base" />
29
+ ```
30
+
31
+ That's pick-side → pick-leverage → buy → position → close, fully working. Restyle to taste.
32
+
33
+ ## Gasless (recommended)
34
+
35
+ By default the widget trades **gasless**: the user signs **once** (a single typed-data signature to authorize
36
+ the USDC spend), there is **no separate `approve` transaction**, and **no ETH for gas** — the trade is
37
+ submitted for them. Their contracts (N) and any close proceeds settle in their **Split trading account**, a
38
+ smart account deterministically derived from their wallet, so the same wallet always maps to the same trading
39
+ account. One signature, no approval, gas-free.
40
+
41
+ ```tsx
42
+ import { SplitTradeWidget } from "@splitmarkets/sdk/widget";
43
+
44
+ // gasless is the default — same props as the connected flow
45
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base" mode="gasless" />
46
+ ```
47
+
48
+ Why one signature: gasless buys pull USDC with a single EIP-3009 `TransferWithAuthorization` (the one popup
49
+ the user sees) and batch the approve + `buyN` into one sponsored call — so there is no on-chain `approve`
50
+ step and the user spends no gas. Closes work the same way (sell N back, USDC settles in the trading account).
51
+
52
+ > Per-chain availability mirrors trading: gasless settlement runs on Base (8453) and Arbitrum (42161).
53
+
54
+ ### Connected fallback
55
+
56
+ `mode="connected"` keeps the original model: the user trades from their **own EOA**, signing each step and
57
+ paying their own gas (the first trade per token is two txs — `approve`, then `buyN`/`sellN`; later trades
58
+ skip the approve once allowance is set). N lands directly in their EOA. Use this when the host app prefers
59
+ the user's wallet to hold positions, or where gasless isn't available.
60
+
61
+ ```tsx
62
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base" mode="connected" />
63
+ ```
64
+
65
+ ## Appearance
66
+
67
+ Theme the widget with the `appearance` prop. Anything you omit falls back to the built-in dark theme.
68
+
69
+ ```tsx
70
+ <SplitTradeWidget
71
+ walletClient={walletClient}
72
+ account={address}
73
+ chain="base"
74
+ appearance={{
75
+ accent: "#7c5cff", // buttons + selected tile
76
+ background: "#0e0e12",
77
+ text: "#e8e8ee",
78
+ radius: 16, // corner radius (px)
79
+ }}
80
+ />
81
+ ```
82
+
83
+ The widget ships with zero-CSS inline styles, so it drops in unstyled-but-polished and you can override only
84
+ the tokens you care about.
85
+
86
+ ## Or the data layer (5 functions, headless, any framework)
87
+
88
+ ```ts
89
+ import { getMarkets, getQuote, buy, getPosition, close } from "@splitmarkets/sdk";
90
+
91
+ const [m] = await getMarkets("base", "long"); // live leverage tiles
92
+ const quote = await getQuote("base", "long", m.seriesId); // pricer-signed quote
93
+ await buy({ chain: "base", side: "long", seriesId: m.seriesId, qN: 0.1, walletClient, account });
94
+ const pos = await getPosition({ chain: "base", side: "long", seriesId: m.seriesId, account });
95
+ await close({ chain: "base", side: "long", seriesId: m.seriesId, qN: pos.qN, walletClient, account });
96
+ ```
97
+
98
+ ## How it works (read this once)
99
+
100
+ 1. **Discover** — `getMarkets` reads the on-chain pools for the live series (strike, expiry, leverage).
101
+ 2. **Price** — `getQuote` fetches a **pricer-signed** quote from Split's hosted API (ask/bid + a signature).
102
+ You can't forge it; only Split can sign it.
103
+ 3. **Trade** — `buy` sends `approve(USDC) → pool.buyN(seriesId, qN, maxCost, quote, signature)` from the
104
+ user's **own wallet**. The contract **verifies the signature on-chain** — a stale or tampered quote reverts.
105
+ 4. **Settle** — physical settlement reads **no price feed**. The buyer's downside is capped at the premium;
106
+ there is no liquidation. `close` sells back at the signed bid.
107
+
108
+ In **gasless** mode the same buyN/sellN run inside a single sponsored call from the user's Split trading
109
+ account, gated by the same on-chain signature check — so the trust model is identical, just with one
110
+ signature and no gas. In the **connected** fallback the user signs each step and pays their own gas. Either
111
+ way Split never holds their keys.
112
+
113
+ **Advanced architecture (click to expand) — what's public vs internal**
114
+
115
+ ```
116
+ ┌────────────────────── YOUR APP ──────────────────────┐
117
+ │ <SplitTradeWidget/> OR getMarkets/getQuote/buy/... │
118
+ │ user's connected wallet (their keys, their gas) │
119
+ └───────────────┬───────────────────────────┬──────────┘
120
+ │ read pools (public RPC) │ GET /pool-quote
121
+ ▼ ▼
122
+ ┌── @splitmarkets/sdk (this package) ──┐ ┌── Split quote-api (Split-hosted) ──┐
123
+ │ viem client • ABIs • addresses │ │ prices the option (vol model) and │
124
+ │ NO secrets, NO keys — all public │ │ SIGNS the quote with Split's key │
125
+ └───────────────┬──────────────────────┘ └──────────────┬─────────────────────┘
126
+ │ approve + buyN/sellN(quote, signature) │ ▲ signer key NEVER
127
+ ▼ │ │ leaves Split's server
128
+ ┌──────────────── Split contracts (Base / Arbitrum) ───────┴───┴────────────────┐
129
+ │ buyN/sellN VERIFY the signature on-chain → mint/burn N to the user's wallet │
130
+ │ physically-settled, oracle-free, 1:1 collateralized → no liquidation possible │
131
+ └───────────────────────────────────────────────────────────────────────────────┘
132
+
133
+ PUBLIC (in this SDK): pool addresses, ABIs, the quote-api URL, public RPCs.
134
+ INTERNAL (never shipped): the quote-signer private key, the pricing model, the keeper,
135
+ Split's gasless facilitator/paymaster, deploy keys.
136
+ ```
137
+
138
+ **Security:** the only secret is the quote-signer key, and it stays on Split's server. Every trade you route
139
+ is gated by a Split-signed quote (you can't forge prices), verified on-chain, and funded by the user's own
140
+ wallet — so an integrator can't mint free options or move prices. Nothing in this package is sensitive.
141
+
142
+
143
+
144
+ ## You earn referral fees
145
+
146
+ Your integration earns a share of protocol fees on the volume your app routes. Email
147
+ **[mayur@vistara.dev](mailto:mayur@vistara.dev)** with your integration's address to set your referral.
148
+
149
+ ## Verify it works (don't take our word for it)
150
+
151
+ ```bash
152
+ npm test # unit: chain config, pool resolution, quote encoding (offline, deterministic)
153
+ npm run test:integration # live, read-only: getMarkets + getQuote hit the real pools & quote-api
154
+ ```
155
+
156
+ Unit tests are hermetic (no network). Integration tests prove discovery + signed pricing work against
157
+ production with zero funds and zero writes. End-to-end buy/close (needs a funded wallet) is covered by the
158
+ runnable example in the repo.
159
+
160
+ ## Requirements
161
+
162
+ - `viem` ^2 (peer dep). `react` ≥18 only for the optional widget.
163
+ - A connected EVM wallet on Base (8453) or Arbitrum (42161).
164
+
165
+ ## For coding agents
166
+
167
+ Point your agent at `**SKILL.md**` in this package — it's a one-pass integration brief.
168
+
169
+ ## Support
170
+
171
+ [mayur@vistara.dev](mailto:mayur@vistara.dev) — referral setup, questions, or anything that doesn't work first try.
package/SKILL.md ADDED
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: split-options
3
+ description: Add "Trade leverage on Split" to a React + viem app — oracle-free, non-liquidatable ETH options (long calls / short puts) on Base and Arbitrum. Use when someone wants to let users trade leverage, options, or a perps-alternative that can't be liquidated, or asks to integrate Split / @splitmarkets/sdk.
4
+ ---
5
+
6
+ # Integrate Split leverage trading (one pass)
7
+
8
+ Split is oracle-free, non-liquidatable ETH options. Each option is physically backed 1:1 — the buyer's max
9
+ loss is the premium, and there is no liquidation. Long = call (ETH up), short = put (ETH down). Contracts are
10
+ deployed; pricing is a hosted, signed quote API. Everything you need is the npm package.
11
+
12
+ ## Install
13
+ ```bash
14
+ npm install @splitmarkets/sdk viem # react ≥18 too, only if you use the widget
15
+ ```
16
+
17
+ ## Fastest path — the widget
18
+ ```tsx
19
+ import { SplitTradeWidget } from "@splitmarkets/sdk/widget";
20
+ // wagmi: const { data: walletClient } = useWalletClient(); const { address } = useAccount();
21
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base" />
22
+ ```
23
+ That's pick-side → pick-leverage → amount → Buy → position → Close, end to end. Match the wallet's chain to
24
+ the `chain` prop (`"base"` | `"arbitrum"`).
25
+
26
+ ## Gasless (recommended, default)
27
+ The widget defaults to `mode="gasless"`: **one signature**, **no `approve` tx**, **no gas**. The user signs a
28
+ single typed-data authorization for the USDC spend; the approve + buyN are batched into one sponsored call.
29
+ Contracts (N) and close proceeds settle in the user's **Split trading account** — a smart account
30
+ deterministically derived from their wallet (same wallet → same account). Available on Base + Arbitrum.
31
+ ```tsx
32
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base" mode="gasless" />
33
+ ```
34
+ **Connected fallback** (`mode="connected"`): trades from the user's own EOA — they sign each step and pay
35
+ their own gas (first trade per token is approve + buyN; later trades skip the approve). N lands in their EOA.
36
+
37
+ ## Appearance / theming
38
+ Pass `appearance` to theme the widget; omitted tokens fall back to the dark default.
39
+ ```tsx
40
+ <SplitTradeWidget walletClient={walletClient} account={address} chain="base"
41
+ appearance={{ accent: "#7c5cff", background: "#0e0e12", text: "#e8e8ee", radius: 16 }} />
42
+ ```
43
+
44
+ ## Headless — the five functions
45
+ ```ts
46
+ import { getMarkets, getQuote, buy, getPosition, close } from "@splitmarkets/sdk";
47
+ ```
48
+ 1. **Markets.** `getMarkets(chain, side)` → leverage tiles `{ seriesId, vault, strike, leverage, spot, premium, label }`. Leverage = spot / premium.
49
+ 2. **Quote.** `getQuote(chain, side, seriesId)` → pricer-signed `{ askPerN, bidPerN, validUntil, nonce, signature, ... }`. Quotes live ~3 min; the trade fns fetch fresh automatically.
50
+ 3. **Buy.** `buy({ chain, side, seriesId, qN, walletClient, account })` → approves USDC, calls `buyN` with the signed quote; N mints to the wallet. Returns `{ txHash, qN }`.
51
+ 4. **Position.** `getPosition({ chain, side, seriesId, account })` → `{ qN, markUsd, intrinsicUsd }` (mark at mid).
52
+ 5. **Close.** `close({ chain, side, seriesId, walletClient, account })` → approves N, calls `sellN`; USDC to the wallet. Omit `qN` to close the full balance.
53
+
54
+ These five functions are the **connected** model: the user signs with their own wallet and pays their own gas
55
+ (N lands in their EOA). The widget's default **gasless** mode instead takes one signature, charges no gas, and
56
+ settles N in the user's Split trading account — see the Gasless section above. No backend, no keys either way.
57
+
58
+ ## Units & gotchas
59
+ - `qN` is human contracts (e.g. `0.1`); the client scales to 18dp. USDC is 6dp; the client converts.
60
+ - First trade per token is two txs (approve, then buyN/sellN); the client skips the approve when allowance is set.
61
+ - The signed quote is verified on-chain in buyN/sellN — a stale/tampered quote reverts. You never sign prices.
62
+ - Honest copy to show users: **"can't be liquidated · max loss = premium."**
63
+
64
+ ## Verify
65
+ `npm test` (offline unit) and `npm run test:integration` (live read-only) ship with the package.
66
+
67
+ ## Deployed addresses (mainnet, constants in the package)
68
+ - Quote API: `https://quote-api-production-b5b9.up.railway.app`
69
+ - Base (8453): long/calls `0xe310f78e2721a2a10faabf8df830140756fcb579`, short/puts `0x8a176e1a7103cccbd6551eef65bc00da641a1ecb`, WETH `0x4200000000000000000000000000000000000006`, USDC `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`.
70
+ - Arbitrum (42161): long `0xa1b161ff5ddd2437ba9e359397e868ef4cb6df2f`, WETH `0x82aF49447D8a07e3bd95BD0d56f35241523fBab1`, USDC `0xaf88d065e77c8cC2239327C5EDb3A432268e5831`. (Calls only for now — `getMarkets("arbitrum","short")` throws.)
71
+
72
+ ## Earn referral fees
73
+ Volume you route earns a share of protocol fees. Email mayur@vistara.dev with your integration's address.