For the complete documentation index, see llms.txt. This page is also available as Markdown.

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"

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"

Rule
Detail

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

Field
Required
Notes

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.

trailingStopBps

❌

Integer 1–9999. Makes the order trailing — see Trailing Stops.

trailingMode

❌

stop_loss only, requires trailingStopBps. A UI-only label — see Trailing Stops.

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.

The minimum-notional gate applies to the full amount, not a per-cycle slice. A $10 floor means $10 total for the order, checked the same way and returning the same 400 MIN_NOTIONAL_NOT_MET.

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:

Endpoint
Adds

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

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.

Last updated