> For the complete documentation index, see [llms.txt](https://titan-exchange.gitbook.io/titan/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://titan-exchange.gitbook.io/titan/developer-doc/special-order-types/order-types/stop-loss-take-profit.md).

# 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](/titan/developer-doc/special-order-types/order-types/trailing.md) 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" |

```typescript
// 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"
```

{% hint style="warning" %}
**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.
{% endhint %}

`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

```http
POST /orders/intent
X-Titan-Key: <partner-api-key>
X-Titan-User: <your stable user id>
Content-Type: application/json

{
  "orderType": "stop_loss",
  "userPubkey": "<user's external Solana wallet>",
  "config": {
    "inputMint":  "So11111111111111111111111111111111111111112",
    "outputMint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    "amount": "1000000000",
    "triggerPrice": "50000000",
    "priceDecimals": 6,
    "minOutputAmount": "45000000",
    "expiresAt": 1767225600
  }
}
```

| 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](#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](/titan/developer-doc/special-order-types/order-types/trailing.md).                                              |
| `trailingMode`    | ❌        | `stop_loss` only, requires `trailingStopBps`. A UI-only label — see [Trailing Stops](/titan/developer-doc/special-order-types/order-types/trailing.md).                             |
| `onFillOco`       | ❌        | Arms a bracket on the bought tokens when this order fills — see [Conditional Entry & Bracket](/titan/developer-doc/special-order-types/order-types/otoco.md).                       |

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:

```http
POST /orders/intent
X-Titan-Key: <partner-api-key>
X-Titan-User: <your stable user id>
Content-Type: application/json

{
  "orderType": "stop_loss",
  "userPubkey": "<user's external Solana wallet>",
  "config": {
    "inputMint":  "JUPyiwrYJFskUPiHa7hkeR8VUtAeFoSYbKedZNsDvCN",
    "outputMint": "So11111111111111111111111111111111111111112",
    "amount": "100000000",
    "triggerPrice": "450000",
    "priceDecimals": 6,
    "priceBasis": "usd"
  }
}
```

The top-level fields (`userPubkey`, `outputRecipientAddress`, `platformFee`, `onboardIfNeeded`, and the optional `X-Idempotency-Key` header) are shared with every order type — see [Creating Orders](/titan/developer-doc/special-order-types/guides/creating-orders.md). `confirm` is byte-for-byte the same call as DCA.

{% hint style="info" %}
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`.
{% endhint %}

### 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](/titan/developer-doc/special-order-types/reference/schema.md#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.

{% hint style="warning" %}
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.
{% endhint %}

{% hint style="warning" %}
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.
{% endhint %}

**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

```
intent ──confirm──▶ active ──trigger crossed──▶ executing ──▶ completed
                      │
                      ├─ pause/resume ↔ paused          (price not watched while paused)
                      │
                      ├─ cancelled  ── (user cancel; withdraw flow as DCA)
                      │
                      ├─ failed   ── deposit auto-returned within ~1 minute
                      │
                      └─ expired  ── (config.expiresAt reached) ── deposit auto-returned
```

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](/titan/developer-doc/special-order-types/guides/managing-orders.md#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](/titan/developer-doc/special-order-types/order-types/otoco.md).

{% hint style="warning" %}
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.
{% endhint %}

## Related pages

* [Trailing Stops](/titan/developer-doc/special-order-types/order-types/trailing.md) — what `trailingStopBps` changes for each type
* [OCO Brackets](/titan/developer-doc/special-order-types/order-types/oco.md) — both triggers on one deposit
* [Conditional Entry & Bracket](/titan/developer-doc/special-order-types/order-types/otoco.md) — buy on a condition, then bracket automatically
* [Managing Orders](/titan/developer-doc/special-order-types/guides/managing-orders.md#modify-a-trigger-order) — modify prices, expiry and trail in place
