Stop-Loss & Take-Profit
One-shot orders that sell the full deposit when the watched price crosses a trigger.
A stop_loss or take_profit order deposits its full amount up front, watches a price — the pair ratio, or the input token's USD price, per the order's priceBasis — and swaps exactly once when the trigger is crossed. The two types take identical config fields — only the comparison direction differs.
orderType
Fires when
stop_loss
price <= triggerPrice — the market fell to the trigger
take_profit
price >= triggerPrice — the market rose to the trigger
Both can be made trailing with trailingStopBps, which changes what triggerPrice means. Everything on this page describes the static behaviour.
Price basis
Every price on a trigger order — triggerPrice, takeProfitPrice, stopLossPrice, and on reads currentTriggerPrice, highestObservedPrice and executedPrice — is a fixed-point number scaled by 10^priceDecimals and sent as a string. All of them share one denomination, chosen at creation with the optional priceBasis field:
priceBasis
triggerPrice means
Use for
"pair" (default)
The pair ratio inputTokenUsd / outputTokenUsd — output token per 1 input token
Any order, and all buy-side orders (stablecoin input)
"usd"
USD per 1 whole input token, independent of the output token
Sell-side orders where the user thinks in dollars — "sell JUP for SOL when JUP reaches $0.50"
// Both bases use the same scaling; only the meaning of the number differs.
const toPrice = (value: number, priceDecimals = 6): string =>
BigInt(Math.round(value * 10 ** priceDecimals)).toString();
toPrice(74); // pair: SOL → USDC at $74 → "74000000"
toPrice(1 / 74, 9); // pair: USDC → SOL at $74 → "13513514"
toPrice(0.45); // usd: JUP at $0.45 → "450000"The pair-basis trap. For SOL → USDC the ratio coincides with SOL's USD price. Flip the pair and the coincidence disappears: on a USDC → SOL order with SOL at $74, the correct ratio is 1 / 74 ≈ 0.0135, not 74. For sell-side orders, priceBasis: "usd" avoids the conversion entirely.
priceDecimals is chosen per order, in the range 0–18:
Case
priceDecimals
Prices of $1 and above, or pair ratios above 1
6
Pair ratios below 1
9
usd orders on a token priced below $1
9, or more for sub-cent tokens
At 6 decimals a $0.00002 token is the integer 20 — a 5% trigger granularity, and a trailing stop that ratchets in whole steps. The oracle carries 9 decimals.
Rules for priceBasis: "usd"
Prices the input token
With a stablecoin input (USDC, USDT, PYUSD, USDS and similar) the value is ~1.00 and cannot meaningfully trigger. The request returns 400 VALIDATION_ERROR at path config.priceBasis. Buy-side orders use pair.
Immutable
Like priceDecimals, fixed after creation. Echoed on every read; orders created before the field existed read "pair".
Trigger semantics unchanged
Stop-loss fires on the way down, take-profit on the way up, trailing peaks, OCO ordering and minOutputAmount all behave identically. Only the number being compared differs.
minOutputAmount
Always in output-mint atoms, whatever the basis. On a usd order, derive it from the trigger via the output token's USD price — not by multiplying amount by the USD trigger.
Stablecoin output
For SOL → USDC, the two bases differ only by the stablecoin's deviation from $1.
Config
inputMint
✅
Base58. Must differ from outputMint.
outputMint
✅
Base58.
amount
✅
String, > 0. The input tokens sold when the trigger fires — this is the deposit.
triggerPrice
✅
String, > 0. On the order's priceBasis, scaled by 10^priceDecimals.
priceDecimals
✅
Integer 0–18.
priceBasis
❌
"pair" (default) or "usd" — see Price basis. "usd" with a stablecoin inputMint returns 400 VALIDATION_ERROR.
minOutputAmount
❌
String, > 0. Execution floor in output-mint atoms, on any basis. When the trigger is crossed but the quote comes in below this, the order waits instead of filling badly.
expiresAt
❌
Unix seconds, must be in the future. At expiry the deposit is auto-returned.
onFillOco
❌
Arms a bracket on the bought tokens when this order fills — see Conditional Entry & Bracket.
The same stop denominated in USD, on a non-stable output — "sell 100 JUP for SOL if JUP falls to $0.45". A pair ratio cannot express this trigger because it would drift with SOL's price:
The top-level fields (userPubkey, outputRecipientAddress, platformFee, onboardIfNeeded, and the optional X-Idempotency-Key header) are shared with every order type — see Creating Orders. confirm is byte-for-byte the same call as DCA.
Unfillable configurations
An order created past its trigger is allowed and executes immediately. Titan rejects only configurations that could never fill, with 400 MIN_OUTPUT_UNSATISFIABLE, in two cases:
Case
Applies to
Condition
details
Structural (checked first)
stop_loss only
minOutputAmount exceeds the output implied by triggerPrice. A stop-loss fires only at or below its trigger, so this is a limit above the stop and cannot fill at any market price. Applies to trailing stops too, evaluated at the seed triggerPrice.
priceBasis, triggerPrice, minOutputAmount, outputAtTriggerPrice — the highest floor that would be accepted
Instant-fire
stop_loss, take_profit
The trigger is already crossed at the current price and minOutputAmount exceeds the current market value of the input.
priceBasis, triggerPrice, currentPrice (the market on the order's basis), minOutputAmount, fairOutputAmount
On a priceBasis: "usd" order the trigger-implied output depends on the output token's USD price, so the structural check is evaluated at the current output price. It is advisory and skipped if the oracle is unavailable.
In both cases, lower minOutputAmount or adjust triggerPrice.
Other trigger-specific causes of 400 VALIDATION_ERROR: expiresAt in the past, priceDecimals outside 0–18, amount or prices not > 0, minOutputAmount not > 0, priceBasis not "pair" or "usd", priceBasis: "usd" with a stablecoin inputMint, an invalid onFillOco, or any of the retired take-profit fields (scaleSteps, entryPrice, minProfitBps).
Read
GET /stop-loss/{orderId} and GET /take-profit/{orderId} return the base Order shape plus the type-specific fields:
/stop-loss/{orderId}
amount, triggerPrice, priceDecimals, priceBasis, minOutputAmount?, expiresAt?, executedPrice?, executedPriceDecimals?, amountReceived?, the trailing state trailingStopBps?, trailingMode?, currentTriggerPrice, highestObservedPrice?, and the bracket fields onFillOco?, childOrderId?, onFillError?
/take-profit/{orderId}
the same, with TP-flavored trailing state — trailingStopBps?, trailingActivated, highestObservedPrice? (no currentTriggerPrice) — plus amountRemaining and scaleStepsExecuted
All amount and price fields are strings. All response timestamps are ISO-8601 — including expiresAt, which you sent as Unix seconds. priceBasis is always present — "pair" on orders created before the field existed — and governs every price field on the object, executedPrice included.
executedPrice scale
Read executedPriceDecimals, not priceDecimals, to scale executedPrice. Once an order fills, executedPrice is the realized price on the same basis as triggerPrice — whole output tokens per whole input token for "pair", USD per whole input token for "usd" — scaled by executedPriceDecimals, which equals priceDecimals. It reflects the route's actual fill, while triggerPrice is compared against the oracle mark, so the two differ by slippage and fees.
Orders filled before executedPriceDecimals was introduced carry executedPrice with executedPriceDecimals absent. Those legacy values are the raw swap-quote ratio — output atoms per input atom at 9 decimals — and are off by a factor of 10^(inputDecimals − outputDecimals) (1000× for SOL/USDC). Treat a missing executedPriceDecimals as "not a renderable price" rather than assuming a scale.
Both endpoints return 404 NOT_FOUND when the id exists but is a different orderType. Read orderType from GET /me/orders and route on it rather than probing endpoints.
Ignore these if present: entryPrice, minProfitBps, and scaleSteps are legacy fields that may appear on older orders. They carry no meaning for orders you create — bind your UI to the documented fields above.
Executions
A trigger order produces exactly one execution row, written when it fires. executionType is stop_loss or take_profit_full; price is the fill-triggering price, in the same fixed-point encoding as the order.
Lifecycle
Four differences from DCA are worth building around. The order is active immediately after confirm, with no equivalent of DCA's startAt, so it never sits in pending — and pending_modification doesn't exist for it. Modify is parameter-only: prices, expiry and trail change in place via PATCH /stop-loss/{orderId} or PATCH /take-profit/{orderId} with no transaction — see Modify a trigger order. amount and mints are immutable; cancel and recreate to change size. There's no retry — POST /orders/{orderId}/retry returns 400 RETRY_NOT_SUPPORTED, though in practice you'll see 400 ALREADY_WITHDRAWN first because auto-return usually completes within the minute. And expired is auto-returned too, unlike DCA, so withdrawalStatus flips to completed on its own for both failed and expired.
A completed trigger order owns nothing to withdraw — the single fill consumed the whole deposit and the output went to outputRecipientAddress at execution time, so POST /orders/{orderId}/withdraw on it returns 400 NOTHING_TO_WITHDRAW. Order-level withdrawal only matters for cancelled trigger orders, whose deposit never left the manager.
Pause, resume, and cancel are the same endpoints and semantics as DCA. A paused trigger order isn't price-watched; on resume it re-arms immediately.
An entry order with onFillOco that completes carries a childOrderId pointing at the bracket it created — see Conditional Entry & Bracket.
A trigger order can fire at any moment — there's no nextExecutionAt to anchor your polling to. While the user has an armed order on screen, poll its detail endpoint or GET /me/orders/active at a sensible cadence. There are no webhooks.
Related pages
Trailing Stops — what
trailingStopBpschanges for each typeOCO Brackets — both triggers on one deposit
Conditional Entry & Bracket — buy on a condition, then bracket automatically
Managing Orders — modify prices, expiry and trail in place
Last updated

