# 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.