Skip to content

Prepared transactions: prepare, check, sign, submit

A prepared transaction is what the futures API returns when you ask it to prepare a trade, a withdrawal or an Earn action. It is the exact unsigned Solana transaction the owner will sign, with the API’s sponsor as fee payer, an id, and a summary of what it does.

The API builds the transaction. Your client checks it against what the user asked for. The owner’s wallet signs it. The API adds the sponsor’s signature and sends it. The program enforces the rules. A prepared transaction is not an authorization and not a price guarantee.

1. Prepare POST /v1/tx/quote-prepare (an order, with its quote)
POST /v1/tx/prepare (any action)
2. Check decode the transaction and compare it with the request
3. Sign the owner signs the message bytes, unchanged
4. Submit POST /v1/tx/submit { id, transaction }
5. Wait GET /v1/tx/:signature/wait?commitment=processed, then confirmed

POST /v1/tx/quote-prepare takes an order (market, side, lo, hi, lots, and optionally limit, mode, minFill, close, withdraw) plus owner, and returns { quote, prepared }. Call it while the user edits; only the latest prepared transaction matters. POST /v1/tx/prepare takes { owner, action, params } for every action. The actions and their parameters are in the API reference.

The response is { id, transaction, blockhash, lastValidBlockHeight, summary }. transaction is a base64 serialized transaction whose signatures are all empty. It starts with SetComputeUnitLimit(1_400_000); an order also requests a 256 KiB heap frame. Steps are added only when needed: a deposit of exactly the cash an order is short of, the creation of the owner’s USDC account, a withdrawal of all free cash when asked.

Check the decoded transaction, not only the summary:

  1. Fee payer. The first account is the sponsor named by /v1/config.
  2. Signers. The owner is a required signer, and nobody but the owner and the sponsor is.
  3. Programs. Every top-level instruction calls the compute budget program, the associated token program (to create the owner’s own USDC account) or the futures program you trust.
  4. Market and accounts. Every futures instruction targets the market you asked for, with the owner as signer and the owner’s own USDC token account.
  5. Intent. The order is exactly the one asked for: side, range, lots, and a limit no worse than the quote plus the user’s slippage. A close names the owner’s own position. A deposit is no larger than the order can need. There is no withdrawal the user did not ask for, and none to someone else’s account.

The web app checks 1 to 3 on every transaction. Petri checks all five before it signs a sponsored futures transaction. If any check fails, sign nothing.

The owner signs the message bytes exactly as prepared. The sponsor co-signs only a message that is byte-identical to the one it prepared, so a wallet that changes the transaction while signing makes it unusable. The web app compares the signed message with the prepared one and refuses to send it if they differ.

POST /v1/tx/submit with { id, transaction }, where transaction is the owner-signed transaction in base64. The API checks the id (prepared less than 90 seconds ago, not used), the bytes, the fee payer and every signature, adds the sponsor’s signature, and sends the transaction. It returns the signature at once and resends the transaction every 300 ms until it is processed or its blockhash expires.

GET /v1/tx/:signature/wait?commitment=processed resolves when the transaction lands, fails or expires. Then …?commitment=confirmed returns its decoded events: fills, positions, deposits. GET /v1/stream?owner= pushes the same changes.

  • A stale package. An order carries a package proven against the book as it was prepared. If the book changed before the transaction landed, it fails with BadProposal, Unbalanced, NotBestPrice, NotMaxFill or FifoViolation, and nothing changes. Prepare it again; the web app does this once on its own, and the user signs again.
  • An expired transaction. Its blockhash expired before it landed; nothing was charged. Prepare it again.
  • No answer yet. If a wait times out, do not resubmit blindly: the transaction may still land. Check GET /v1/tx/:signature and the owner’s positions first.
  • Each id once. A prepared id can be submitted once. Prepare again for a new attempt.

Program errors are named in the wait and status responses, for example InsufficientFunds, MinFill or WouldCross.

The API only prepares sponsored transactions. To pay the network fee from your own wallet, build the transaction yourself from the chain, as Petri’s local route does with the futures client, and send it through an RPC. The SDK builds the instructions; the package comes from the Rust client’s solver. The browser RPC gateway at /rpc relays a fully signed transaction, but refuses one whose fee payer is the sponsor. See Rust and Petri.