Skip to content

Running Stitch on Fireblocks

Stitch normally signs with a hot wallet, a private key on the machine running the bot. That is what How to become a filler assumes, and it is fine to start with. If your inventory is large enough that a key on a server is the wrong answer, Stitch can sign with Fireblocks instead. The key stays in Fireblocks' MPC custody and the bot never holds it.

This page covers a real workspace, not the Developer Sandbox.

Experimental

The Fireblocks backend is new and marked experimental in the Stitch panel. Signing is a round trip to Fireblocks rather than a local operation, and whether that fits inside the quote deadline depends on your workspace. Measure it before you rely on it. See Latency.

What a compromised bot host can do

Read this before you configure anything. It decides what the rest of the page is for.

The bot host holds the Fireblocks API key and the RSA key. Together those can ask your workspace to sign anything your policy auto-approves. Assume the host can be stolen and work from there.

Stitch itself only ever signs EIP-712 typed messages: Permit2 orders, plus a handshake with the Textile venue. It never asks Fireblocks to sign arbitrary bytes, which is why it does not need the Raw Signing entitlement.

But an attacker does not have to use Stitch. With the credentials they call Fireblocks directly, and a Typed Message policy rule cannot read what a message says. It scopes who signs for which vault, not what they sign. So "typed messages only" on its own does not mean "cannot move funds":

  • Many tokens, USDC among them, support EIP-2612 permit, which is itself an EIP-712 message. An attacker signs a permit granting themselves an allowance on the token and drains the vault directly. The Permit2 allowance you carefully bounded is not involved.
  • Any other EIP-712 message a contract in your stack honours is available to them too.

The Contract Call rule is the bigger hole. Step 5 has you add one so the panel can send Permit2 approvals. A contract call is an arbitrary transaction: if that rule auto-approves, a stolen host does not need an EIP-712 trick at all, it just transfers the tokens out. Do not leave it auto-approving. See step 5.

So the containment this page promises is not a property of typed signing. It is a property of typed signing plus content validation, which is what Validate content with a callback handler sets up. Treat that section as required, not optional. Without it, a stolen bot host can empty the vault, and a bounded Permit2 allowance will not stop it.

Prerequisites

A Fireblocks workspace with an API Co-Signer. Sort this out first. The Signer role holds an MPC key share, and Fireblocks only offers that role once a co-signer exists to hold it. Without one you reach the role dropdown and find no Signer in it, with nothing on screen explaining why.

If your workspace already signs programmatically it has a co-signer. If not, that is a conversation with Fireblocks before anything else here.

A vault account you are willing to lose. Not your treasury. See Blast radius.

1. Create the API user

Settings → Users → Add user, type API user, role Signer.

Two roles look plausible and are not. Editor and Viewer read your vaults fine and then fail on the first signature. Embedded Wallet Signer belongs to a different product: it signs for end-user wallets, not the vault account Stitch trades from.

Fireblocks asks for a CSR. Generate the key pair on your own machine:

bash
openssl req -new -newkey rsa:4096 -nodes -subj "/CN=stitch" \
  -keyout fireblocks_secret.key -out fireblocks.csr

One command, two files. Upload fireblocks.csr, which is the public half. Keep fireblocks_secret.key. Fireblocks never sees it, and it is the file you give Stitch. -nodes leaves the key without a passphrase, which is required because the bot reads it unattended.

When the user is approved you get the API key, a UUID. That plus the key file is everything Stitch needs.

2. Prepare the vault account

Use a dedicated vault account for the bot, not the one holding the rest of your business.

It needs at least one EVM asset wallet so there is an address to read. Add Ethereum to it. You do not need to hold ETH or trade Ethereum. Adding the asset is how Fireblocks mints the wallet and its address.

You only do this once. On EVM chains a Fireblocks vault account has one address across every network, so the same address serves Celo, Base, Arbitrum, BSC and the rest.

Fund it with your trading inventory plus a little native gas on each chain you quote.

3. Write the Typed Message policy rule

Fireblocks rejects typed-message signing until a policy allows it. In Policies, add a Typed Message rule scoped to the one vault account the bot uses and the one API user you just created. Not workspace-wide: a rule that broad lets this key sign for vaults it has nothing to do with.

Set it to auto-approve. This is not optional. Textile gives a filler roughly 750 milliseconds to answer a quote request, so a rule that routes to a human approver means you do not quote.

Be clear about what auto-approval means: there is no human in the loop. Whatever the rule permits, the bot can do unattended. That is why the scoping above matters, and why Securing the setup exists.

4. Point Stitch at it

In the Stitch panel, open the bot's settings and pick Signer → Fireblocks. Enter the API key, then drop fireblocks_secret.key onto the field. Pick your workspace region if it is not the global one, since an EU or US-East workspace answers only on its own host.

Load the vault accounts, pick yours, and press Verify.

Verify signs one throwaway message for real. That single action proves the credentials parse, the co-signer is online, the policy rule exists and auto-approves, the vault resolves to an address, and how long a signature takes. It signs a payload that authorises nothing, so it costs no gas and touches no chain.

The address it reports is written into your bot's configuration. It is not read off an API response. It is the address a real signature recovered to.

5. Approve Permit2

Permit2 pulls your tokens when a quote fills. Until you grant it an allowance the bot quotes and every fill fails.

Sending an approval means signing a transaction, which is the one thing a typed-message signer cannot do. The panel gets around that with a different Fireblocks operation: a contract call, where Fireblocks builds, prices, nonces, signs and broadcasts the transaction itself. The bot never touches a transaction hash, and this needs no extra entitlement on your workspace.

Two things first:

  1. A Contract Call policy rule, scoped to the same vault account and API user as the Typed Message rule from step 3. Same reasoning: not workspace-wide.

    Unlike the Typed Message rule, do not set this one to auto-approve. A contract call is an arbitrary transaction, so an auto-approving rule hands anyone holding the bot's credentials the power to move the vault however they like, and none of the rest of this page contains that. Route it to a human approver, or to an allowlist of the token contracts you are approving. Approvals are rare and you are already sitting at the panel when they happen, so the friction costs you nothing. The panel waits up to three minutes for the call to mine, which is enough time to approve it in the console or the mobile app.

  2. A little native gas in the vault on each chain you quote. An ERC-20 approve is about 46,000 gas, so this is cents everywhere except Ethereum.

Then let the add-bot wizard handle it. It sends one approval per input token on the way to a live bot. To approve later, or to top one up, open the bot's Tools tab and press Approve beside each token reading "Not approved".

The panel resolves the Fireblocks asset id for the chain by reading your workspace, so the call is filed under the right network without you configuring anything. A chain your workspace does not have is reported as such rather than guessed at.

The request stays open until the call is mined, normally a few seconds. If it hangs, the usual cause is a policy rule routing to a human approver instead of auto-approving, the same thing that makes Verify slow.

The panel grants an unlimited allowance. That is the right default for a hot wallet and a deliberate decision for custodied inventory. If you want a bounded one, read Blast radius first and set it yourself from the console.

Securing the setup

Validate content with a callback handler

This is the control that makes the rest of the setup mean anything. Fireblocks policy scopes who signs for which vault; it cannot read what an EIP-712 message says. Only a callback handler can.

Run a Co-Signer Callback Handler. The co-signer POSTs every signing request to an HTTPS service you run, signed as a JWT, and your service returns approve or reject. Fireblocks publishes a plugin-based reference implementation.

Write it as an allowlist, not a blocklist. Reject every domain and primary type that is not one Stitch produces. Anything else, including an EIP-2612 Permit under a token's own domain, gets rejected. A blocklist cannot work here, because you cannot enumerate every message some contract might honour.

Three types pass the domain check:

  • PermitWitnessTransferFrom under the Permit2 domain. This is an order, so it needs the field checks below.
  • MakerSession and MakerEnroll under the Textile venue domains. These authenticate the bot to the venue and move no funds.
  • SignerCheck under the Stitch Signer Check domain. This is what the panel's Verify button signs. It authorises nothing, and a handler that omits it makes Verify fail every time you re-run it after rotating credentials.

For an order, the type and domain are not enough. Check all of these:

  • spender is your known reactor address.
  • witness.info.swapper is the vault address.
  • Every witness.outputs[].recipient is the vault address.
  • Both tokens are on an allowlist.
  • permitted.amount is within a per-order cap.
  • The output amount against the input amount is inside an acceptable rate band.
  • The deadline is near-term.

The two recipient checks are the ones that are easy to miss and expensive to skip. A forged order can pass the reactor, token, cap and deadline checks while naming the attacker as the output recipient. BaseReactor._fill sends the output to whoever the signed order names, so an attacker filling their own order pays the output to themselves and keeps your input inventory. Binding swapper and every output recipient to the vault removes that, and the rate band stops the same trick priced as a legitimate-looking order.

If you are not going to run a handler, do not treat this setup as containing a compromised host. Size the vault accordingly and know that the Permit2 allowance is not your cap.

Blast radius

With content validation in place, a Permit2 order settles against the ERC-20 allowance you granted Permit2, not your vault balance. That allowance is then your exposure and the number to control.

Without content validation it is not the cap at all, for the reason in What a compromised bot host can do. Bounding the allowance is worth doing either way, but on its own it does not bound the damage.

For a hot wallet, Stitch grants an unlimited allowance by default: approve once, never think about it again. For custodied inventory, set a bounded allowance instead, roughly the liquidity you actually commit to the corridor. A stolen signature then cannot take more than you were already willing to quote.

Bounding it is manual. The panel's Approve always grants unlimited, so set the number yourself: send the approve from the Fireblocks console, or let the panel approve and then lower it in the Token Allowance Manager.

Replace "max" liquidity with fixed amounts first, or the bot will not start. The corridor presets ship buy_total_liquidity_debt and sell_total_liquidity_collateral set to "max", meaning "quote everything funded". A side in that mode only accepts an effectively unlimited allowance, so Stitch's preflight refuses to go live against a bounded one. Set a number on every enabled side in stitch.toml before you bound the allowance, and keep the allowance at or above it.

The cost is real. The allowance is consumed as orders fill, so you re-approve periodically to keep quoting. For custodied inventory that trade is usually worth it. If you would rather not manage either number, take the unlimited allowance and treat the whole vault balance as the exposure.

Combined with a dedicated vault account, your worst case becomes the inventory you committed to one corridor rather than everything in Fireblocks.

Typed messages only

Stitch signs typed messages and nothing else, and that is the supported configuration. There is nothing to turn on.

A signer that will sign arbitrary bytes is strictly worse: it can sign a transaction directly, so no amount of content validation on typed messages helps. Typed-only is the floor this setup needs, not the whole of it.

Quoting and the resting ladder both work this way. The on-chain legs do not run on this backend. See What runs and what does not.

Know your kill switch

Revoke the Permit2 allowance from the Fireblocks console. It ships a Token Allowance Manager for exactly this. Every outstanding signature becomes unfillable the moment it lands, including any already stolen.

You do not need the bot, the host, or the API key to do it. Find the path before you need it.

Key hygiene

fireblocks_secret.key is the real secret. The API key UUID is useless without it. Stitch writes it to disk owner-only, and both the bot and the panel read it, so host access is key access. Treat the bot machine accordingly.

Rotation means creating a new API user with a new CSR. The private key never leaves your machine in either direction.

Latency

Fireblocks signing is asynchronous. Stitch creates a signing request and polls for it, where a local hot wallet signs in microseconds.

Textile gives a filler about 750 milliseconds to answer a quote request, and live fillers answer in around 22 milliseconds. Verify reports the measured round trip and warns above 400 milliseconds.

If your number is high, the usual cause is a policy rule that is not auto-approving. If it stays high after that, Fireblocks custody may not suit the quoting path for your workspace. The inventory is safe either way, but you win fewer quotes than your prices deserve.

What runs and what does not

A typed-message signer cannot sign transactions, which sets the shape of a Fireblocks filler.

Runs. Answering RFQ quote requests, and resting orders on the public ladder. Both only sign Permit2 orders.

Does not run. Filling other users' resting limit orders, and the closer. Both build and sign their own transactions on every fill, so neither can go through a contract call the way an approval does. Stitch refuses them when you save the config rather than failing at the first fill.

If those legs matter to your strategy, give that bot a hot wallet. Running one Fireblocks bot for RFQ and a separate hot-wallet bot for the on-chain legs is a normal setup.

Reference

The technical walkthrough, including manual stitch.toml configuration and troubleshooting, is in the Stitch repository: docs/signer-fireblocks.md.