Build on one price.
A TypeScript SDK, the clearing engine bit-exact with the contracts, a gasless relayer, the tape and its live stream, and an MCP server for agents. Everything Unison runs on, open.
Watch the tape.
Every batch, every market, as it clears: one uniform price, the reference it cleared on, the band, and the receipt hash that chains it to the last.
import { TapeClient } from "@unison/sdk";
const tape = new TapeClient(TAPE_URL);
// one uniform price per batch, as it clears
tape.stream(["prints:0"], {
print: (p) => console.log(p.upTo, p.price, p.receiptHash),
});
// or ask: prints, candles, fairness, an account's fills
const fairness = await tape.fairness(0, { window: "24h" });Trade from code.
Sign an order with a session key and the relayer submits it. Every order in a batch gets one price, so a script gets the same price as a desk.
import {
buildOrder, RelayerClient, Side, signAsSession, tickOfPrice,
} from "@unison/sdk";
const order = buildOrder({
account, // the account this key trades for
marketId: 0n, // aNVDA/AUSD
side: Side.BID,
tick: tickOfPrice(180_250_000n, 10_000n), // $180.25, never worse
qty: 10n ** 18n, // 1 share
});
const sig = await signAsSession(sessionKey, chainId, gateway, order);
const relayer = new RelayerClient(RELAYER_URL);
const { id } = await relayer.postOrder(order, sig); // no gasKnow the price before it prints.
@unison/engine is the auction itself, ported from Solidity integer for integer and diff-tested against it. Run the batch now forming and see the price it would clear at.
import { compute } from "@unison/engine";
// the batch now forming: per tick in the band, depth + pending orders
const r = compute({ lo, hi, refTick, bidAbove, askBelow, bids, asks });
r.traded; // did demand meet supply inside the band?
r.tick; // the one price everyone in the batch gets
r.volume; // and how much changed handsDon't trust. Re-walk.
Each print commits to the one before it. Recompute the chain from the tape, or from the chain's own logs, and any rewritten batch stands out. The record does this live.
import { encodeAbiParameters, keccak256 } from "viem";
const T = [
"bytes32", "uint256", "uint64", "uint256", "uint256",
"uint256", "uint64", "uint8", "uint256",
].map((type) => ({ type }));
let prev = "0x" + "0".repeat(64);
const prints = await tape.prints(0, { limit: 1000 });
for (const p of prints.reverse()) {
const h = keccak256(encodeAbiParameters(T, [
prev, 0n, BigInt(p.upTo), BigInt(p.tick), BigInt(p.volume),
BigInt(p.refPrice), BigInt(p.refTimeMs), p.status,
BigInt(p.ts / 1000),
]));
if (p.prevReceiptHash === prev && h !== p.receiptHash)
throw new Error(`batch ${p.upTo} doesn't recompute`);
prev = p.receiptHash;
}Hand an agent the keys.
Mint a session key with limits on Agents, then give it to any agent that speaks the Model Context Protocol. It can place and cancel within those limits, and can never withdraw.
claude mcp add unison \
-e DEPLOYMENT=deployments/monad-testnet.json \
-e RELAYER_URL=$RELAYER_URL -e RPC_URL=$RPC_URL \
-e AGENT_ACCOUNT=0x… -e AGENT_PRIVATE_KEY=0x… \
-- node --conditions=development services/mcp/src/main.tsThe surface.
Tape (read)
- GET /v1/markets
- every market: reference, regime, last print, 24 h volume
- GET /v1/markets/:id/prints
- uniform prints, newest first; .csv for all of them
- GET /v1/markets/:id/fairness
- distance from the reference, reference lag, chain check
- GET /v1/accounts/:addr/orders
- orders with per-auction fills at each batch's price
- GET /v1/receipts/:market/:account/:slot
- a fill's receipt, recomputed
- GET /v1/stream
- SSE: heads, prints, regime, account:0x…
- GET /v1/stats
- who trades: accounts with fills, the team counted apart
Relayer (write, no gas)
- POST /v1/orders
- a signed order: session key, passkey or wallet
- POST /v1/cancels
- a signed cancel
- POST /v1/withdrawals
- the account's own signature only
- POST /v1/sessions
- grant or revoke a key, with caps
- POST /v1/passkeys
- register a passkey account
- GET /v1/jobs/:id
- where a relayed action is
Every route, field and error code: docs/API.md.