SDK Reference — @swarm-ai-labs/turnkey-extensions
The product API: one object, named after what you want to do.
import {
createSwarm,
markets,
} from "@swarm-ai-labs/turnkey-extensions/runtime";
const swarm = createSwarm({ identity, policy: { capUsd: 500 } });
await swarm.portfolio.holdings(); // what do I own
await swarm.portfolio.positions(); // what is working
await swarm.perps.open({
coin: "ETH",
side: "buy",
notionalUsd: 250,
mode: "live",
});
await markets.funding({ minApr: 0.2 }); // no wallet neededEverything below is a thin binding over the domain modules — it adds no arithmetic and no protocol knowledge of its own. What it adds is one vocabulary, one wallet and one policy, so you do not have to know which of the forty subpaths owns "read my positions". The building blocks are still there and still supported: see CAPABILITIES.md.
Two entry points
| Entry point | Needs a wallet? | What it is |
|---|---|---|
markets | no | Public market data: prices, perps, books, candles, screeners. |
createSwarm({ identity, … }) | yes | Everything bound to one wallet: reads, trades, limits, budget. |
Both live at the ./runtime subpath (and ./runtime/node for the parts that need a filesystem or a subprocess):
import {
createSwarm,
markets,
} from "@swarm-ai-labs/turnkey-extensions/runtime";
import {
fsStore,
turnkeyBinaryIdentity,
} from "@swarm-ai-labs/turnkey-extensions/runtime/node";markets is also reachable as swarm.markets, so a consumer holding a swarm has one place to look. It is the same object — binding a wallet does not change a public read.
Creating a swarm
const swarm = createSwarm({
identity, // who signs — required
store, // where state that outlives the process is kept (default: in-memory)
policy: { capUsd: 500, limits: { maxLeverage: 5 } },
wallet: "trading", // which Turnkey wallet, when the identity holds several
addresses: { evm: "0x…" }, // skip discovery when you already know them
progress, // live status for every write path
venues: ALL_VENUES, // which venues the portfolio reads
adiApiKey, // ADI authenticates by API key
binance: { apiKey, apiSecret }, // Binance USD-M futures
funder, // Polymarket Deposit Wallet override
});Identity backends
An identity is one primitive — signRawPayload — and everything else (the viem account, the EIP-712 signer, the address book) is derived from it.
| Function | Use it when |
|---|---|
turnkeyApiKeyIdentity({ apiPublicKey, … }) | A bot or a server. No human at a terminal. |
turnkeySessionIdentity(session) | An email-OTP session (what swarm login produces). |
turnkeyKitIdentity(client, organizationId) | A browser, over @turnkey/react-wallet-kit. |
turnkeyBinaryIdentity({ keyName, … }) | The local turnkey CLI. ./runtime/node only. |
otpLogin({ initiator, email, … }) | Mint a session from an email OTP, whatever the environment. |
Store
memoryStore() by default — a caller that wants an order journal to survive a restart says so, rather than discovering the package wrote to a directory it did not choose. fsStore() (from ./runtime/node) writes 0600 files under ~/.config/swarm-cli, which is where the CLI keeps the same documents.
Policy
Two levels, and they know different things.
authorize(op)runs while the operation is still an intent, before a key is touched: leverage ceiling, withdrawal whitelist, daily-loss limit.reserve(usd, label)runs where the USD figure finally exists: the spend cap and the per-order notional limit.
Passing the shorthand ({ capUsd, limits }) builds a policy for you, and wires the daily-loss check to read today's PnL from your own venues. Passing a built SwarmPolicy uses it as given. Absent means no cap and no standing limits — a library does not invent a ceiling you never asked for.
A refusal throws PolicyRefusal, carrying rule: "cap" | "limit" so a caller can tell a budget from a standing limit.
Shapes every operation shares
Anything that can move money returns an OpResult<T>:
interface OpResult<T> {
mode: "plan" | "paper" | "live";
data: T; // what is specific to this operation
plan: OpPlan; // what it would do / did, as steps
spend?: PolicySummary; // what the policy reserved
warnings: string[]; // seen, but not grounds to stop
}plan is populated always, not only in plan mode: a live run that reports what it did in the same shape as a preview is a result two consumers can render identically.
mode defaults to "plan" everywhere. Nothing signs until you say "live". That is not a convention a caller has to remember — an agent-facing server is safe because it cannot name live, not because it promises never to.
Failures are typed: OpError (with kind: "user" | "upstream" and an operator-facing hint), OpModeUnsupported, PolicyRefusal.
Reads across venues return a VenueSweep<T>: rows (everything), outcomes (per venue) and problems. "Holds nothing" and "could not be asked" are different answers — a venue that failed appears in problems, never as an empty table.
Function reference
swarm.wallet — identity and addresses
One Turnkey wallet is one key set with a different address format per chain.
| Function | What it does |
|---|---|
wallet.addresses() | One address per chain slot: evm, sol, btc, tron, ton, sui, near. Read once. |
wallet.account(address) | The raw Turnkey account, including the public key TON and SUI cannot be built without. |
wallet.evmAccount(address?) | A viem LocalAccount backed by Turnkey. Defaults to the wallet's EVM address. |
wallet.validAddress(chain, addr) | Would this address be accepted on that chain? Catches wrong-network sends before signing. |
wallet.explorerUrl(chain, hash) | Explorer link for a transaction. |
wallet.organizationId | The organization the identity signs under. |
swarm.addresses() | Shorthand for wallet.addresses(). |
NEAR has no Turnkey address format at all — its implicit account is derived from the Solana key. You never have to know that.
markets — public market data
No wallet, no signing, no configuration.
| Function | What it does |
|---|---|
markets.prices(symbols) | Spot USD price + 24h change, indexer first with a CoinGecko fallback. |
markets.tickers(query, { limit }) | Resolve free text to a token the inventory knows. |
markets.priceGraph(id, timeframe) | Historical price series for a chain:address token id. |
markets.perps({ search }) | Every listed Hyperliquid perp with mark, funding, OI and leverage cap. |
markets.perp(coin) | One perp in detail. null when it is not listed. |
markets.perpAsset(coin) | Tick size, size decimals, leverage cap — what an order is built from. |
markets.book(coin) | L2 order-book snapshot: what is actually available to trade against. |
markets.mid(coin) | Cheap mid-price poll, for a live tick. |
markets.candles(coin, range) | Candle closes over a chart range (LIVE, 1H, 1D, 1W, 1M, 1Y). |
markets.predictions({ search }) | Polymarket markets — trending, or matching a query. |
markets.funding(filter) | Perps ranked by annualised funding, and which side collects it. |
markets.edge(filter) | Prediction markets whose outcome prices do not sum to 1. |
funding ranks by |APR|, not by APR: a deeply negative rate is exactly as tradable from the other side. edge drops a market with an unquoted outcome rather than ranking a blank quote as free money.
swarm.portfolio — holdings, positions, PnL
Four questions that used to need four vocabularies, answered in one.
| Function | What it does |
|---|---|
portfolio.holdings({ view }) | The whole wallet: native coin on every supported chain plus ADI and 0G, read, priced and sorted. |
portfolio.balances(pairs) | Specific chain:SYMBOL pairs — the targeted read, resolved against the inventory. |
portfolio.positions({ sort }) | Open positions across Hyperliquid, Binance, Polymarket and ADI, in one model. |
portfolio.orders() | Resting orders across those same venues. |
portfolio.collateral() | Equity and free collateral per venue — what a new position can draw on. |
portfolio.pnl({ sinceMs }) | Realized, unrealized, fees and funding per venue over a window. |
portfolio.fills({ sinceMs }) | The executed trades behind that PnL, for a report or a CSV export. |
portfolio.cancel(selector) | Pull resting orders: { orderId }, { instrument } or { all: true }. This one signs. |
portfolio.rebalance({ … }) | Where the collateral sits, and the route that would move some to a target venue. |
portfolio.activity(options) | On-chain history across every chain the wallet has an address for. |
portfolio.venueContext() | The context those reads run against — for a venue call this layer does not wrap. |
Every venue read is failure-isolated, and an unknown venue name is refused rather than quietly matching nothing. pnl marks each row windowed: a venue that can only report lifetime figures says so instead of pretending the number is your day.
Holdings never render a failed read as a zero balance: an unreadable row carries an error, and errorCount says how many.
swarm.perps — perpetual futures
Reads take the address, writes take the signer, and the split is visible in the shape: positions() never mints a key, open() does.
| Function | What it does |
|---|---|
perps.account() | Equity, free collateral and every open position in one read. |
perps.positions() / perps.position(c) | Open positions, or one coin's. |
perps.orders() | Resting orders. |
perps.fills({ sinceMs }) | Executed trades — windowed, or the recent tail. |
perps.funding({ sinceMs }) | Funding paid or received over a window. |
perps.feeRates() | The account's maker/taker rates, which every plan prices fees with. |
perps.balances() | USDC in the spot account, and USDC on Arbitrum outside the exchange. |
perps.preview(order) | The exact order — price, size, margin, fee, liquidation — without signing. |
perps.open(order) | Open or add to a position. Reserves the notional, not the margin. |
perps.close({ coin, fraction }) | Flatten a position, or part of one, at market. Reserves nothing. |
perps.cancel({ orderId | all, coin }) | Pull resting orders. |
perps.leverage({ coin, leverage }) | Set the leverage the next position on a coin opens at. |
perps.tpsl({ coin, tp, sl }) | Attach take-profit / stop-loss triggers to a position that already exists. |
perps.margin({ coin, deltaUsd }) | Add or remove isolated margin — the direct lever on liquidation distance. |
perps.deposit({ amountUsdc }) | Bridge USDC from Arbitrum onto the exchange, gas checked first. |
perps.withdraw({ amountUsd, to }) | Withdraw to Arbitrum. The destination is whitelist-checked before anything signs. |
perps.deadMan({ minutes | off }) | Arm the exchange's scheduled cancel: resting orders are wiped if nothing checks in. |
Sizing is explicit on purpose: notionalUsd is the position, marginUsd is your own funds, size is base-coin units. Exactly one of the three — under leverage, "$100" is ambiguous by 5x or more, and guessing costs money.
swarm.signals — AI signals
A strategy runs upstream and emits signals; it executes nothing. Acting on one is a perp order you place.
| Function | What it does |
|---|---|
signals.configured() | Whether a proxy is configured at all — every call is a no-op without one. |
signals.list({ coin, limit }) | Newest signals across every owned strategy. |
signals.listOrNull(…) | The same read, but null when the feed is down rather than an empty list. |
signals.stream({ signal }) | Live signals over SSE. The snapshot replays on connect — be idempotent per signal id. |
signals.strategies({ window }) | The strategy catalogue, ranked by PnL. |
signals.strategy(id) | One strategy. |
signals.describe(id) | Its rules, per coin. |
signals.describeFiring(id, …) | The one rule that fired, with the coin's risk frame folded in. |
signals.equity(id, window) | A strategy's equity curve. |
signals.ticket({ signal, usd }) | The whole trade ticket as data — side, leverage, px/sz, margin, and one blocker. |
signals.open({ signal, usd }) | Take the signal's trade. usd is the position's notional, not your margin. |
ticket() never throws: an unusable signal comes back with a blocker (expired → not_tradable → no_amount → below_min → insufficient_margin). below_min compares the notional the exchange will see after size flooring, not the amount you typed.
swarm.risk — standing limits
Limits that outlive one call, unlike a per-run spend cap.
| Function | What it does |
|---|---|
risk.limits() | The stored limits. An unreadable document throws rather than reading as "no limits". |
risk.setLimits(patch) | Merge a change in. null clears one limit; omitting it leaves it alone. |
risk.clearLimits() | Remove every standing limit. |
risk.describe() | The limits as lines a human reads. |
risk.empty() | True when nothing is set. |
risk.check(op) | Would this operation be allowed? The same call every write makes — ask before, not after. |
risk.budget() | What the run's spend cap has left, when one is in force. |
Supported limits: maxDailyLossUsd, maxOrderUsd, maxLeverage, withdrawWhitelist. A daily-loss limit that cannot be evaluated refuses — not knowing how the day went is not grounds to assume it went well.
Helpers: dailyPnlReader(portfolio) builds the PnL reader a policy needs (only venues that can window PnL are counted), startOfUtcDay(now) is the window it uses.
Moving money: send, swap, exec, route, tx
| Function | What it does |
|---|---|
swarm.send(input) | Transfer a coin or token. asset is chain:SYMBOL, resolved against the inventory. |
swarm.swap(input) | Swap chain:SYMBOL → chain:SYMBOL across 1Click, Uniswap v3, gas.zip or LI.FI. |
swarm.exec(request) | One large order released as many: TWAP, ladder or iceberg, journalled so a crash can resume. |
swarm.route(request, deps) | A multi-leg route with a journal — each leg waited out, settlement checked as a balance question. |
swarm.replaceTx(request) | Replace a stuck transaction: same nonce, higher fee. cancel sends 0 to your own address. |
swarm.revokeApproval(request) | Set an ERC-20 allowance to zero. |
send supports EIP-3009: sign a transfer authorization instead of broadcasting a transfer, self-relayed or submitted by a feePayer that pays the gas. The per-chain signing quirks are derived, not asked for — TON and SUI need the public key their address hashes from, NEAR signs with the Solana key.
Slicing is what makes a resumed run safe: child order ids are derived from the parent, so a child resent after a crash carries the same id and the exchange refuses the duplicate rather than filling twice.
Recipes
A portfolio dashboard
const swarm = createSwarm({ identity });
const [holdings, positions, pnl] = await Promise.all([
swarm.portfolio.holdings({ view: { hideZero: true } }),
swarm.portfolio.positions({ sort: "value" }),
swarm.portfolio.pnl({ sinceMs: Date.now() - 7 * 86_400_000 }),
]);
console.log(holdings.totalUsd, positions.rows.length);
for (const problem of positions.problems) {
console.warn(`${problem.venue}: ${problem.problem?.message}`); // never rendered as "flat"
}Open a perp under a cap
const swarm = createSwarm({
identity,
policy: { capUsd: 1_000, limits: { maxLeverage: 5 } },
});
const preview = await swarm.perps.preview({
coin: "ETH",
side: "buy",
notionalUsd: 250,
});
console.log(preview.plan.steps, preview.data.plan.liquidationPx);
const placed = await swarm.perps.open({
coin: "ETH",
side: "buy",
notionalUsd: 250,
leverage: 3,
sl: 2_800,
mode: "live", // nothing signs without this
});
console.log(placed.data.result?.entry, placed.spend?.remainingUsd);Take a signal, or find out why not
for await (const [event, payload] of swarm.signals.stream()) {
if (event !== "open") continue;
const [signal] = signalsFromEvent(payload);
const ticket = await swarm.signals.ticket({ signal, amountUsd: 100 });
if (!ticket.ready) {
console.log(`skipped ${signal.id}: ${ticket.blocker}`);
continue;
}
await swarm.signals.open({ signal, amountUsd: 100, mode: "live" });
}Screen without a wallet
import { markets } from "@swarm-ai-labs/turnkey-extensions/runtime";
const carry = await markets.funding({ minApr: 0.25, minOpenInterestUsd: 5e6 });
console.table(carry.slice(0, 10)); // coin, apr, paidSide, openInterestUsdBeyond the product API
The product layer covers the paths with money and limits in them. The rest of the package is unchanged and stays available per subpath — see CAPABILITIES.md for the complete surface. The parts most likely to be wanted next:
| Area | Where it lives |
|---|---|
| Polymarket account lifecycle | ./polymarket — fetchPolymarketAccount, onboardPolymarketAccount, … |
| ADI PredictStreet | ./adi — markets, orderbook, orders, vault deposit/withdraw |
| 0G Compute (AI inference) | ./zgcompute — broker, ledger, runZgTurn, TEE attestation |
| CoW limit orders | ./cow — an on-chain limit order that outlives the process |
| Alerts and strategies | ./alerts, ./strategy — condition grammar, edge-triggered firing, backtest |
| Paper trading | ./paper — fill application, book replay, mark-to-market |
| React bindings | ./react — hooks and providers over the runtime |
These are the same building blocks the product layer is built from; using one directly is expected, not a fallback.