Skip to main content
A bracket is a take-profit / stop-loss pair: two price legs on one asset, one-cancels-other, where whichever leg’s trigger is reached first sells the asset and the other leg is discarded. Flash offers it in two forms:
  • A standalone bracket (orderType: "bracket") protects an asset the funder already holds. It is a sell order with a fixed qty and two legs. See Standalone brackets.
  • An attached bracket rides a market, limit, or twap order and protects what that order receives. It is placed in the same quote, sign, and submit flow as the entry. See Attached brackets.
Both forms can carry their own expireTime, both read back with their legs, and both work with minimal authorization.

Standalone brackets

Set orderType: "bracket" with side: "sell", a qty in the sold asset, and exactly two triggers with opposite triggerType: the upper leg is the take-profit, the lower leg the stop-loss. Each leg takes exactly one of notionalPrice (USD) or crossPrice (pair rate, target / contra). Legs have no limit price; whichever fires sells qty at market. expireTime is optional: set it for good-til-time, omit it for good-til-cancelled. To protect 0.05 WETH on Base with a take-profit at $5,000 and a stop-loss at $3,000, expiring in a week:
Quote, sign, and submit exactly as for a stop-loss: one signing payload, and any approval or permit the quote returns for the sold asset. The order rests from submit and reads back as "orderType": "bracket" with both legs under brackets and trigger null:
The Orders WebSocket carries the same brackets array on every frame for the order. A few rules specific to standalone brackets:
  • Any number per asset. A funder may rest several brackets on the same asset at once, each with its own qty. Protection is per bracket: a 0.02 WETH bracket and a 0.03 WETH bracket each sell their own qty when they fire.
  • Neither trigger may already be met at placement. A leg whose trigger price has already been reached when the order is accepted is not fired; the order is closed with closeReason REASON_ORDER_TRIGGERED_ON_ENTRY and nothing is sold. Place the take-profit above and the stop-loss below the current price.
  • Keep qty in the funder wallet. The exit pulls the sold asset from the funder wallet when a leg fires. If the wallet holds less than qty at that moment, or the allowance was revoked, the exit fails and the order closes without selling. A resting bracket whose funder wallet has been emptied of the sold asset is cancelled before either leg fires, with closeReason REASON_INSUFFICIENT_FUNDER_BALANCE; see Automatic cancellation.
  • Expiry. With expireTime the bracket closes REASON_ORDER_EXPIRED at that instant if neither leg has fired. There is no execution buffer: a leg that fires in the final seconds before expireTime closes expired without executing, the same as a stop-loss or take-profit.
With forceMinimalAllowance, the authorization on the sold asset is sized to this bracket’s qty plus every other open order that sells the same asset, including your other brackets.

Attached brackets

An attached bracket adds a take-profit / stop-loss pair to an order at placement time. One quote returns two signing payloads, you sign both with the funder wallet, and one submit places the entry order with its protection attached. The pair protects what the entry receives: when a leg’s trigger is reached it sells the received asset, and the entry stops filling at that moment. For the target/contra and pricing-basis concepts, see Placing Orders; for standalone trigger orders, see Trigger Orders. Attached brackets are available on same-chain EVM and Solana orders with orderType market, limit, or twap, and require funderAddress on the quote request. On EVM the entry must receive an ERC-20: an entry buying the native gas asset cannot carry a bracket, so quote to the wrapped asset (WETH rather than ETH) instead. On Solana, bracket quotes are available only through POST /quote, not the streaming quote subscription. The pair is good-til-cancelled unless you give it its own expireTime inside the attachedBracket block; see Pair expiry. The guide below walks the EVM flow; the Solana differences are the signing primitive and the funding actions — the lifecycle is identical.

The attachedBracket block

Add attachedBracket to the quote request. It carries two legs, and each leg takes exactly one trigger price plus an optional limit price for the exit. Each leg: Three rules tie the legs together:
  • Leg prices are on the asset the entry receives (the quote’s to leg) — the target asset on a buy, the contra asset on a sell.
  • Both legs must use the same basis: both notionalPrice or both crossPrice.
  • The takeProfit trigger must be above the stopLoss trigger.
  • On a limit entry, express the entry’s limit as limitCrossPrice. limitNotionalPrice is rejected together with the block.
A leg with limitPrice exits as a limit order once it fires: the exit rests at that price and fills only there or better. It does not fall back to a market exit, so it can stay unfilled if the market keeps moving away — the classic stop-limit versus stop-market trade-off, and the choice is yours per leg. It matters most on a stop-loss leg, since by the time the exit rests, the other leg and the entry’s unfilled remainder are already cancelled. Omit limitPrice to exit at market. For example, to buy WETH with 100 USDC on Base and protect the position with a take-profit at $5,000 and a stop-loss at $3,000:
The same block attaches to a limit or twap quote request unchanged; the entry’s own fields (limit price, duration, and so on) follow the usual rules for its type.

Two signatures from one quote

The quote response is the usual one — pricing, fees, and the entry’s evm signing and funding actions — plus an attachedBracket block for the pair:
You sign twice with the funder wallet: once over evm.orderTypedData for the entry, once over attachedBracket.evm.orderTypedData for the pair. The pair sells the received asset, so that asset may need its own funding actions before submit, exactly like the spent asset does for the entry: send attachedBracket.evm.approveTx if it is non-null, and if attachedBracket.evm.permitTypedData is non-null, sign it and echo the payload and signature back as attachedBracket.evmPermitTypedData / attachedBracket.evmPermitSignature on submit. By default the authorization the quote returns for the received asset is unlimited: an approve for the maximum amount, or a Permit2 permit for the maximum amount with no expiration. If the wallet’s existing authorization already covers signedMaxFromAmount (and, for Permit2, outlives the pair), the quote returns none for the pair. With forceMinimalAllowance the pair’s authorization is sized instead to signedMaxFromAmount plus every other open order that sells the received asset, and a Permit2 permit’s expiration follows the pair’s expireTime, or stays unlimited when the pair has none. The signature caps the protection. The pair’s signature authorizes selling up to signedMaxFromAmount of the received asset, a value derived from the entry’s expected output with headroom on top: currently the quoted output plus 5% for a market entry, twice the output implied by the limit price for a limit entry, and twice the quoted output for a twap entry. Protection is capped there: if the entry ends up receiving more than the signed maximum, the excess is not covered by the exits. Receiving less than the signed maximum is normal — the maximum is headroom, not a target. The trigger prices are not part of either signature; the legs ride the quote and submit requests themselves.

Submit

Submit the order with the same attachedBracket legs plus the signature material echoed from the quote:
The response carries the entry’s orderId plus the pair’s starting status:

On Solana

The flow is the same four steps — quote, fund, sign twice, submit — with Solana’s signing primitive and funding actions. See Solana for the base flow the entry follows. Quote. The response carries the entry’s svm block as usual, plus the pair’s own signing material under attachedBracket.svm:
Each Solana flash order signs over its own nonce, so a composed quote reserves two — the entry’s (svm.nonce) and the pair’s (attachedBracket.svm.nonce). They are always different values; echo each one on its own block at submit. Fund. The pair sells the received asset, and that asset’s token account needs its own one-time delegate approval, exactly like the spent asset’s does for the entry. Execute any svm.ataSetupIxs from the entry block first (token accounts must exist before delegation), then attachedBracket.svm.delegateIx when it is non-null. That delegation is for the maximum amount by default, or for signedMaxFromAmount plus the funder’s other open orders on the asset under forceMinimalAllowance; the quote returns it only when the token account doesn’t already delegate the required amount to Flash. When Definitive sponsors the approvals, both the entry’s and the pair’s ride one combined transaction on the entry-level svm.sponsoredDelegateTx — sign and echo it back as usual, and attachedBracket.svm.delegateIx is null.
An SPL token account holds exactly one delegate. If the funder later grants a different app delegate authority over the received asset’s token account, Flash’s authority is revoked and the pair’s exit will fail on-chain when it fires. Keep the standing delegation in place for as long as the pair is live.
Sign and submit. Produce a 64-byte Ed25519 signature over each message with the funder wallet: the entry’s svm.orderMessage as userSignature, and attachedBracket.svm.orderMessage as attachedBracket.userSignature (base58 or hex). The submit block echoes svmNonce instead of salt, and there are no permit fields on Solana:
Everything from here — activation at the first fill, growth, firing, cancellation — behaves exactly as described below.

Lifecycle

At submit the pair is not an order yet. It activates on the entry’s first fill.
  • pending_activation — submitted, waiting for the entry’s first fill. There is nothing to protect yet, and no pair order to read — a GET for the pair returns not-found until activation, so track it through the entry’s attachedBracket object or the stream.
  • Active — on the first fill the pair becomes a live order with its own orderId, protecting what the entry has received so far. Later entry fills grow the protected amount, up to the signed maximum — do not assume the pair’s size is immutable.
  • never_activated — the entry ended without ever filling (cancelled, or expired unfilled). The pair never becomes an order.
The entry’s order carries the pair’s state. GET /v1/orders/{orderId}?funderAddress=0x… on the entry returns an attachedBracket object:
Once active, the pair is an ordinary order: bracketOrderId is how you find it. Its own row reads as "orderType": "bracket", "side": "sell", and carries sourceEntryOrderId pointing back at the entry. It appears alongside your other orders — poll GET /v1/orders (same funderAddress query parameter) or stream the Orders WebSocket, whose entry frames carry attachedBracket with the status and bracketOrderId (the leg configuration rides the GET response only) — and you can cancel it by its own orderId (see Cancelling Orders). Reads can trail a placement or cancel by a second or two while the order projects; retry shortly rather than treating a just-placed order’s absence as an error. Cancelling the entry after a fill does not cancel the pair; it keeps protecting whatever the entry received. Cancelling the entry before any fill means the pair never activates. The independence runs both ways: cancelling the pair leaves the entry working, unprotected.

When a leg fires

Three things happen at once when either trigger is reached:
  1. The exit is placed. The pair sells the received asset — at market, or resting at the leg’s limitPrice.
  2. The other leg is cancelled. The two legs are one-cancels-other (OCO): only one exit ever executes.
  3. The entry stops filling. Any unfilled remainder of the entry is cancelled. If the entry has already finished, there is no remainder to cancel and only the sale happens.
The entry-side cancel is deliberate: the remainder is cancelled so a stop-out does not keep buying into the move that triggered it.

Pair expiry

Set attachedBracket.expireTime on the quote request to give the pair its own good-til-time expiry. It must be in the future and, when the entry has an expireTime, no earlier than it: a pair that ended before its entry finished filling would protect nothing. The pair’s signature carries it as the deadline, and once the pair is an order it closes REASON_ORDER_EXPIRED at that instant if neither leg has fired. If the deadline passes while the pair is still pending_activation and the entry fills afterwards, the pair activates and closes expired at once. There is no execution buffer: a leg that fires in the final seconds before expireTime closes expired without executing. Omit the field and the pair stays live until it fills or you cancel it. The entry’s attachedBracket.expiresAt and the pair order’s own expiresAt both read the value back. An exit can execute across more than one transaction. If an entry fill lands after a leg has fired — a fill already in flight when the trigger was reached — it is sold as it lands.

Activation is at the first fill

A resting entry keeps resting through a price gap; attaching a bracket does not change that. The pair activates on the entry’s first fill, and a leg whose trigger is already true at that moment fires immediately. Concretely: place a limit buy at $3,900 with a take-profit at $4,400 and a stop-loss at $3,000, and if the price gaps straight down through the entry price and the stop, the entry fills into the falling market and the stop-loss sells the position at once. You can buy into a falling market and be stopped out immediately — the protection engages the moment there is something to protect, at whatever the market is by then. The same applies on the other side: a take-profit already true at first fill takes profit immediately.
Keep the received funds in the funder wallet while protection is active. Exits pull the received asset from the funder wallet at execution time. If the wallet no longer holds the pair’s full size when a leg fires — proceeds moved or spent, or the allowance revoked — the exit fails entirely: it does not partially sell what remains, it does not retry, and the protective order is cancelled. Keeping the funds and their allowance in place is your responsibility.forceMinimalAllowance accounts for pairs: a pending pair counts at its signedMaxFromAmount, and an active pair at the amount it can still grow to, so a later minimal-allowance order on the same asset keeps the pair covered. If you set a smaller allowance yourself, it must cover signedMaxFromAmount plus every other open order that sells that asset.

End-to-end example

Prerequisites. The example uses viem. All requests go to https://flash.definitive.fi/v1 with your key in the x-definitive-api-key header. The public integrator key dpka_513a2bd7_57a2_46d2_927b_2a3857fe271b is safe for development. The onchain steps also need an RPC endpoint and the funder wallet’s key, supplied here through environment variables.
Buy WETH with 100 USDC on Base, protected by a take-profit at $5,000 and a stop-loss at $3,000. On a buy, qty is denominated in the contra asset (USDC), and the pair protects the WETH received. If the spent or received token needs an allowance, the quote returns approve calldata (or a Permit2 payload) for it; handle whichever comes back for both the entry and the pair, then sign both payloads and submit.
To track the entry after submit, poll GET /orders or stream live updates over the Orders WebSocket. Once the pair activates it is an order of its own and shows up on the same surfaces.

Trigger Orders

Single-leg stop, stop-loss, and take-profit orders.

Placing Orders

The full parameter reference for every order type.