# Deserialize Ultra Aggregator — complete integration contract > One API for DeFi data and actions across six sectors (swaps, memecoins, perps, NFT, prediction markets, RWA) on Solana, Ethereum, Base, Arbitrum, BSC and Polygon. Read-and-build only: the API never holds funds, never signs and never submits. Every action returns an ordered `steps[]` for the user's wallet to sign and send. This file is self-sufficient: it contains everything needed to integrate successfully, with no other page required. Base URL: https://ultra-api.deserialize.xyz --- # Quickstart No signup, no key, no wallet needed to read. These calls work right now: ```sh curl https://ultra-api.deserialize.xyz/v1/health curl https://ultra-api.deserialize.xyz/v1/chains # a real meta-aggregated quote: 1 SOL -> USDC curl -X POST https://ultra-api.deserialize.xyz/v1/swaps/quote \ -H "content-type: application/json" \ -d '{"chain":"solana", "inputToken":"So11111111111111111111111111111111111111112", "outputToken":"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", "amountIn":"1000000000"}' ``` ## The four-step integration path 1. **Read** — call any GET endpoint. Nothing is required but the URL. 2. **Quote** — POST a `/quote` endpoint to price an action. Quotes are never cached and expire. 3. **Build** — POST a `/build` endpoint with the user's address as `taker`/`owner`. You get `steps[]`. Swap-shaped builds accept `"simulate": false` to skip the pre-return simulation gate (see Safety guarantees). 4. **Execute** — walk `steps[]` in order with the user's wallet. The API is not involved. ## Authentication and rate limits - **Auth is optional.** No key is required for any endpoint. Free tier: **10 requests/second per IP**. - **Enterprise keys are not rate limited.** Send yours as the `x-api-key` header. Keys are issued manually: email sales@deserialize.xyz → invoice → on payment the key is issued. - Over the limit returns HTTP 429 `PROVIDER_RATE_LIMITED` with `retry-after` and `x-ratelimit-*` headers. - `partnerFeeBps` / `partnerFeeRecipient` take an integrator fee where the venue supports one natively. **The fee is applied at quote time**, so send it to `/quote` as well as `/build` — a quote without it will not match the build. Where it works: **KyberSwap** (EVM, incl. `/v1/rwa`) charges it on the output token; **Jupiter** (Solana) charges it on the quote, and `partnerFeeRecipient` must be a Jupiter *referral token account for the output mint*, not a wallet. Where it does not: **LiFi** needs a registered integrator (portal.li.fi) and refuses otherwise; **Hyperliquid**, **four.meme** and **Polymarket** have no integrator-fee field at all. In every unsupported case the build returns a `partnerFee NOT applied` warning rather than silently pretending — check `warnings[]`. Fees are capped at 1000 bps (10%). When a venue does charge, the quote carries `fee: {amount, token, bps, recipient, chargedOn}` — the amount the **venue itself reported**, in base units of `token`, not the bps you asked for. `GET /v1/fees/summary?recipient=…` totals those per chain and token. **Read its `note` before treating any of it as revenue: these are fees accrued on actions we returned, not fees earned** — this API never learns whether you signed or broadcast, so reconcile against the recipient address on-chain. An unscoped summary (no `recipient`) requires an enterprise key, since auth is otherwise open. ## Optional TypeScript SDK The REST API is complete on its own; the SDK just wraps it and executes steps. ```ts npm i @deserialize/ultra-agg-sdk import { UltraAggClient, executeSteps } from '@deserialize/ultra-agg-sdk' const ultra = new UltraAggClient({ baseUrl: 'https://ultra-api.deserialize.xyz' }) const action = await ultra.swapBuild({ chain, inputToken, outputToken, amountIn, taker }) await executeSteps(action, wallet) // wallet signs; the SDK never sees a key ``` --- # Conventions, envelope and errors Every response uses the same envelope: ```json { "success": true, "data": { ... } } { "success": false, "error": { "code": "NO_ROUTE", "message": "...", "retryable": false } } ``` ## Hard rules - **Amounts are decimal strings in BASE units** (wei, lamports, raw token units) — never numbers, never human-readable decimals. `"1000000000"` is 1 SOL. - **Addresses**: checksummed `0x…` on EVM, base58 mints on Solana. Native asset uses the sentinel `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` (EVM) or the wrapped-SOL mint (Solana). - **Chains** are the literal strings `solana`, `ethereum`, `base`, `arbitrum`, `bsc`, `polygon`. - **Pagination** uses opaque cursors: send `?limit=` and `?cursor=`, read `nextCursor` (null means end). - **Quotes expire.** Re-quote instead of reusing a stale one; build endpoints re-quote internally. ## Error codes `error.code` is a closed set — handle these exactly: | code | HTTP | meaning | | --- | --- | --- | | `VALIDATION_ERROR` | 400 | malformed request body or params | | `UNSUPPORTED_CHAIN` | 400 | chain not supported for this operation | | `UNSUPPORTED_VENUE` | 400 | venue lacks this capability, or the asset is permissioned | | `TOKEN_NOT_FOUND` | 404 | unknown token or asset id | | `MARKET_NOT_FOUND` | 404 | unknown market id for that venue | | `NO_ROUTE` | 404 | no provider could route this pair | | `INSUFFICIENT_LIQUIDITY` | 422 | route exists but cannot fill the size | | `PROVIDER_ERROR` | 502 | an upstream venue or chain RPC failed — retryable; on build endpoints you may also pass simulate:false | | `PROVIDER_RATE_LIMITED` | 503 | upstream or our free tier throttled you — back off | | `QUOTE_EXPIRED` | 410 | the quote or action went stale; re-request it | | `SIMULATION_FAILED` | 422 | the built tx reverts in simulation; revert reason in details | | `NOT_FOUND` | 404 | no such route | | `INTERNAL` | 500 | our bug — safe to retry once | Provider outages always degrade to `PROVIDER_ERROR` / `PROVIDER_RATE_LIMITED` envelopes — never a 500, never a partial-looking success. --- # The steps[] contract Read this before building any action. Every action endpoint returns: ```json { "actionId": "01J…", "chain": "base", "expiresAt": "2026-07-20T12:00:00.000Z", "steps": [ … ], "quote": { … }, "warnings": [] } ``` Act before `expiresAt` — a stale action must be re-requested, not signed. Always read `warnings[]`: it carries per-action caveats (e.g. "not simulated" when the build was requested with `simulate: false`, or an approve step that could not be verified). Execute `steps` **in array order**. There are exactly three kinds: 1. `sign-transaction` — an unsigned transaction. **You sign AND broadcast it yourself.** `tx.format` is `evm` (`{to, data, value, gasLimit?, chainNumericId}`) or `solana-versioned` (`{base64, lastValidBlockHeight?}` — deserialize a VersionedTransaction). If `waitForConfirmation` is true, wait for confirmation before the next step. 2. `sign-typed-data` — produce a signature; nothing is broadcast. `payload.format` is `eip712` (`{domain, types, primaryType, message}`), `personal-message`, or `solana-message`. 3. `post-to-endpoint` — an HTTP request you send. **`request.url` is always the VENUE's URL, never ours.** Compose `bodyTemplate`, send it, capture fields named in `captureOutputs`. `request.method` is `GET`, `POST`, `PUT` or `DELETE`; a `GET` step carries no `bodyTemplate`, and any `authScheme` signs an empty body. ## Template language (closed set) Strings inside a step may contain placeholders you substitute from earlier results: ``` {{wallet.address}} the acting wallet address {{now_ms}} current time in milliseconds {{steps..signature}} full signature from a sign-typed-data step {{steps..signature.r|s|v}} split secp256k1 signature components {{steps..txHash}} hash of a broadcast sign-transaction step {{steps..response.}} dot-path into a post-to-endpoint response {{credentials.}} a venue credential YOU hold (never ours) ``` A placeholder that is the *entire* string keeps its native JSON type; inline placeholders stringify. ## Skipping steps A step may carry `skipIfSatisfied`. If the condition already holds on-chain, skip that step: - `evm-allowance-at-least` — `{chain, token, spender, amount}`: skip if `allowance(owner, spender) >= amount`. - `evm-erc721-approved-for-all` — `{chain, collection, operator}`: skip if `isApprovedForAll(owner, operator)`. The API already prunes approvals it can prove are satisfied for the `taker` you pass. ## Worked example — EVM swap with approval ```json {"steps":[ {"id":"approve","kind":"sign-transaction","chain":"base", "description":"Approve KyberSwap router to spend USDC", "skipIfSatisfied":{"type":"evm-allowance-at-least","chain":"base", "token":"0x8335…2913","spender":"0x6131…37b5","amount":"100000000"}, "tx":{"format":"evm","to":"0x8335…2913","data":"0x095ea7b3…", "value":"0","chainNumericId":8453},"waitForConfirmation":true}, {"id":"swap","kind":"sign-transaction","chain":"base", "description":"Swap 100 USDC -> WETH", "tx":{"format":"evm","to":"0x6131…37b5","data":"0xe21fd0e9…", "value":"0","gasLimit":"420000","chainNumericId":8453}} ]} ``` ## Worked example — off-chain order (sign, then POST to the venue) ```json {"steps":[ {"id":"sign-order","kind":"sign-typed-data","chain":"arbitrum", "payload":{"format":"eip712","domain":{"name":"Exchange","version":"1", "chainId":1337,"verifyingContract":"0x0000…0000"}, "types":{"Agent":[{"name":"source","type":"string"}, {"name":"connectionId","type":"bytes32"}]}, "primaryType":"Agent","message":{"source":"a","connectionId":"0x7f3c…"}}}, {"id":"post-order","kind":"post-to-endpoint", "request":{"method":"POST","url":"https://api.hyperliquid.xyz/exchange", "headers":{"Content-Type":"application/json"}, "bodyTemplate":{"action":{},"nonce":1789000000000, "signature":{"r":"{{steps.sign-order.signature.r}}", "s":"{{steps.sign-order.signature.s}}", "v":"{{steps.sign-order.signature.v}}"}}}} ]} ``` Some `post-to-endpoint` steps carry `authScheme` describing headers you compute. `polymarket-l2-hmac` derives them from credentials captured in an earlier step. `limitless-hmac` uses **your own** scoped API token (limitless.exchange → API Tokens → Derive): base64 `HMAC-SHA256(secret, `${isoTimestamp}\n${METHOD}\n${pathAndQuery}\n${body}`)` sent as `lmts-signature` with `lmts-api-key` and an ISO-8601 `lmts-timestamp`. Its `apiKeyFrom`/`secretFrom` resolve `{{credentials.}}` — a namespace **you** populate (SDK: `executeSteps(action, wallet, { credentials })`), because this API never receives venue credentials. The SDK implements both schemes; raw clients follow the recipe. ## Safety guarantees - Every EVM transaction we return has passed `estimateGas` from your `taker` (otherwise you get `SIMULATION_FAILED` with the revert reason). Every Solana transaction has passed `simulateTransaction`. - **Opting out of simulation**: simulation-gated build endpoints (`/v1/swaps/build`, `/v1/rwa/build`, `/v1/predictions/{venue}/claims/build`, `/v1/memecoins/{venue}/buy` and `/sell`) accept `"simulate": false` (default `true`) as an escape hatch when chain RPCs are degraded (`PROVIDER_ERROR` from a build you know is sound). The steps come back **unverified**, any approve step is kept even if the allowance is already sufficient, and `warnings[]` says so — simulate client-side before broadcasting if you can. - We never receive your signatures, keys, or signed payloads. - `post-to-endpoint` URLs are venue-owned and never ours — verify them if you wish. --- # Endpoints Generated from the live OpenAPI document, so this list cannot drift. Full machine-readable request/response schemas: https://ultra-api.deserialize.xyz/v1/openapi.json — interactive: https://ultra-api.deserialize.xyz/docs ## fees - `GET /v1/fees/summary` — Integrator fees accrued on built actions, grouped by token (scoped) ## memecoins - `GET /v1/memecoins/graduating` — Bonding curves close to graduation - `GET /v1/memecoins/new` — Newest launches - `GET /v1/memecoins/tokens/{chain}/{address}` — Single memecoin detail (curve progress, status, mcap) - `GET /v1/memecoins/trending` — Trending by activity - `POST /v1/memecoins/{venue}/buy` — Build a bonding-curve buy transaction - `POST /v1/memecoins/{venue}/sell` — Build a bonding-curve sell transaction ## meta - `GET /v1/chains` — Supported chains - `GET /v1/health` — Service + infrastructure health - `GET /v1/providers` — Provider availability (key-gated adapters) ## other - `DELETE /mcp` — - `GET /llms-full.txt` — - `GET /llms.txt` — - `GET /llms/{doc}` — - `GET /mcp` — - `GET /v1/openapi.json` — - `POST /mcp` — ## perps - `GET /v1/perps/venues` — Perps venues + capabilities - `GET /v1/perps/{venue}/markets/{marketId}/funding` — Funding + open interest - `GET /v1/perps/{venue}/markets/{marketId}/orderbook` — L2 orderbook snapshot - `GET /v1/perps/{venue}/markets` — Markets for a venue - `GET /v1/perps/{venue}/orders/{owner}` — Open orders (live, uncached) - `GET /v1/perps/{venue}/positions/{owner}` — Open positions (live, uncached) - `POST /v1/perps/{venue}/collateral` — Build an isolated-margin collateral add/remove action - `POST /v1/perps/{venue}/margin` — Build a leverage / margin-mode config action - `POST /v1/perps/{venue}/orders/build` — Build an order (market/limit, optional TP/SL legs) as signable steps - `POST /v1/perps/{venue}/orders/cancel` — Build an order-cancel action ## predictions - `GET /v1/predictions/compare` — Same-event markets matched across venues (slug/fuzzy; never auto-merged) - `GET /v1/predictions/markets` — Unified market list across all venues (per-venue degradation, never a 500) - `GET /v1/predictions/venues` — Prediction venues with capabilities (data-only venues are tradable:false) - `GET /v1/predictions/{venue}/markets/{marketId}/odds` — Current odds per outcome (5s cache) - `GET /v1/predictions/{venue}/markets/{marketId}/orderbook` — YES-side orderbook (probability prices; outcomeId query selects another outcome) - `GET /v1/predictions/{venue}/markets` — Prediction markets - `GET /v1/predictions/{venue}/positions/{owner}` — Outcome-share positions (live, uncached) - `POST /v1/predictions/{venue}/claims/build` — Build the claim (redeem winnings) transaction for a resolved market - `POST /v1/predictions/{venue}/onboarding` — Build the one-time onboarding steps (allowances + CLOB credentials) - `POST /v1/predictions/{venue}/orders/build` — Build a signed-order flow for an outcome (full steps incl. credentials) ## rwa - `GET /v1/rwa/assets/{id}/yield-history` — Historical APY + TVL series (daily) for yield-bearing assets - `GET /v1/rwa/assets/{id}` — One RWA asset ({chain}:{address}) - `GET /v1/rwa/assets` — Curated RWA registry with live prices (filter by category/chain/tradability) - `GET /v1/rwa/categories` — RWA categories with asset counts - `POST /v1/rwa/build` — Build a buy/sell transaction for a tradable RWA - `POST /v1/rwa/quote` — Quote buying/selling a tradable RWA via the swap engine ## swaps - `GET /v1/swaps/providers` — Swap providers per chain - `POST /v1/swaps/build` — Build swap transaction steps via the best (or pinned) provider - `POST /v1/swaps/quote` — Meta-aggregated swap quote (all providers, best first) ## tokens - `GET /v1/tokens/{chain}/{address}/price` — Spot price (USD), 10s cache - `GET /v1/tokens/{chain}/{address}` — Token metadata (registry-backed, lazily populated) --- # WebSocket streams and MCP ## WebSocket Connect to `wss://ultra-api.deserialize.xyz/v1/ws` and send JSON frames: ```json {"op":"subscribe","stream":"prices","params":{"chain":"solana","address":"So1111…"}} {"op":"unsubscribe","id":""} {"op":"ping"} ``` Streams: `prices`, `memecoins.launches`, `perps.markets`, `predictions.odds`. `params` are optional filters (`chain`, `address`, `venue`, `marketId`) — omit to receive the whole stream. Server frames: `{"op":"subscribed","id","stream"}`, `{"op":"event","id","stream","key","ts","data"}`, `{"op":"unsubscribed"}`, `{"op":"pong"}`, `{"op":"error","message"}`. ## MCP (AI agents) Streamable-HTTP MCP server at `POST https://ultra-api.deserialize.xyz/mcp` (stateless; POST only). Every data endpoint is exposed as a native tool — enumerate with `tools/list`. ```json {"jsonrpc":"2.0","id":1,"method":"tools/list"} {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_swap_quote","arguments":{"chain":"solana","inputToken":"…","outputToken":"…","amountIn":"1000000000"}}} ``` Send `accept: application/json, text/event-stream`. An agent proposing an action still gets `steps[]` — a human or the agent's wallet layer signs. The agent never gains custody. --- # Sector notes and gotchas - **Swaps** are meta-aggregated: `/v1/swaps/quote` returns `{best, all, failed}` so you can show alternatives. `/v1/swaps/build` re-quotes and returns steps. - **Memecoins**: a token that has graduated off its bonding curve returns `QUOTE_EXPIRED` (pump.fun) or `UNSUPPORTED_VENUE` (four.meme) from the builders — route it through `/v1/swaps` instead, and an address that is not a curve token at all returns `TOKEN_NOT_FOUND`. Both venues quote before building, so `slippageBps` is enforced on-chain (four.meme `minAmount`/`minFunds`, pump.fun via the routed swap) and the returned `quote` carries the expected output. pump.fun curve trades are routed through Jupiter; four.meme is encoded directly against its TokenManager. Builds are simulation-gated like swaps and accept `"simulate": false`. - **Perps**: check `/v1/perps/venues` for per-venue `capabilities` before calling orderbook / TP-SL / margin endpoints; unsupported combinations return `UNSUPPORTED_VENUE`. - **Predictions**: check `/v1/predictions/venues` first — venues carry `capabilities` and `tradable`; unsupported combinations (and unknown venues) return `UNSUPPORTED_VENUE`. **Limitless is the venue where a plain EOA can open a position** — it accepts `signatureType 0` with `signer == maker`, no proxy or deposit wallet, on Base with USDC collateral. `POST /v1/predictions/limitless/orders/build` needs your profile id as `ownerId` (from limitless.exchange) and returns a `sign-typed-data` + `post-to-endpoint` pair; you supply the API token as executor credentials. Polymarket by contrast refuses a bare EOA as maker in CLOB V2 — it requires a deposit wallet created through their onboarding, which is why `funder`/`signatureType` exist. Kalshi is data-only (`chain: "offchain"`, custodial venue): markets, odds and orderbook, no positions or actions; its prices are normalized to 0..1 probability decimal strings. First-time Polymarket wallets need onboarding (allowances + CLOB credentials): call `/v1/predictions/{venue}/onboarding`, or pass `hints` to `/orders/build` so satisfied steps are pruned. **`hints.hasApiCreds` selects the credential step, and the two are mutually exclusive at the venue**: a first-time wallet gets `POST /auth/api-key` (create), a returning one gets `GET /auth/derive-api-key` (derive) — creating twice is a 400 and deriving before creating is a 400. If your wallet trades from a Polymarket proxy/deposit wallet rather than a bare EOA, pass `funder` (the address holding the funds, which becomes the order's `maker`) and the matching `signatureType` (0 EOA, 1 proxy, 2 Gnosis safe, 3 EIP-1271); the venue rejects orders whose maker is an account it does not know. **Polymarket trades settle on CLOB V2 (since 2026-04-28): collateral is pUSD — wrap USDC via `pUSD.wrap()` — and fees are protocol-set at match time.** Redeem winnings of a resolved market with `POST /v1/predictions/{venue}/claims/build` (simulation-gated; accepts `simulate: false`); resolved markets expose `meta.resolution` = `{resolved, winningOutcomeId, winningOutcomeIndex}`. Orderbooks are the YES-side view, sorted best-first on both sides (`?outcomeId=` selects another outcome and must belong to that market; Kalshi asks are derived as 1 − NO-bid). An odds outcome carrying `"noQuotes": true` has no quotes on either side — its `price` is a floor, not a market price, so do not treat it as a probability. Cross-venue: `GET /v1/predictions/markets` (unified list with per-venue degradation) and `GET /v1/predictions/compare?query=` return same-event groups matched by slug (confidence 1) or fuzzy text (confidence <1) — markets are never auto-merged because resolution criteria differ per venue; each entry names its `resolutionSource`. **In practice `compare` returns few or no groups today**: the venues slice and phrase markets very differently (Polymarket "Will Bitcoin reach $100,000 in July?" vs Kalshi "BTC price range on Jul 29 at 7pm"), so lexical matching finds almost nothing. Treat it as best-effort; use the per-venue endpoints for coverage. Note also that only Polymarket has real text search — Kalshi has none (its recurring price series are reachable only by `series_ticker`, which we index for a few high-traffic topics) and Limitless caps upstream pages at 25, so a query on those venues is a bounded local sweep, not exhaustive. - **RWA**: `freelyTradable` describes **access reality only** (open DEX + no transfer restrictions) — it is not a liquidity opinion. Permissioned assets refuse builds with `UNSUPPORTED_VENUE` plus `accessMode`, `kycRequired` and `issuerUrl` so you can point the user at the issuer. - **NFT** endpoints are currently disabled. - Adapters whose optional provider key is unset disable themselves — check `/v1/providers` to see what is live. --- # Support - Enterprise keys and commercial terms: sales@deserialize.xyz - Machine-readable schema: https://ultra-api.deserialize.xyz/v1/openapi.json - Interactive docs: https://ultra-api.deserialize.xyz/docs