> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mobula.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Take Profit / Stop Loss Trading Bot

> Build a non-custodial Solana take-profit and stop-loss trading bot with Mobula Advanced Orders, price targets, custom token triggers, signed intents, and automatic swap execution.

<Warning>
  **Alpha feature**: Advanced Orders are gated and not publicly available yet. Reach out to the Mobula team to enable access for your account and to receive the deposit wallet for your environment.
</Warning>

This cookbook shows how to build a Take Profit / Stop Loss trading bot using Mobula Advanced Orders. The bot creates signed, deposit-backed intents; Mobula watches market data and Token Details; then the execution engine quotes and sends the swap only when the order condition is satisfied.

Use this pattern for:

* Solana take-profit and stop-loss automation.
* Trading bots that draw TP / SL lines on a chart and convert them to orders.
* Custom trigger orders based on `priceUSD`, `volume24hUSD`, holder stats, liquidity, or security flags.
* Non-custodial exits where the user signs an intent but never gives the bot a wallet private key or reusable approval.

## Architecture

```mermaid theme={null}
sequenceDiagram
  participant Bot as Trading Bot
  participant User as User Wallet
  participant API as Mobula API
  participant Worker as Advanced Orders Worker
  participant Engine as Mobula Execution Engine
  participant Chain as Solana

  Bot->>User: Build deposit transfer
  User->>Chain: Transfer tokenIn amountRaw to Mobula deposit wallet
  Bot->>API: POST /advanced-orders/intent
  API-->>Bot: Canonical message + payload
  Bot->>User: Sign canonical message
  Bot->>API: POST /advanced-orders with intentSignature
  Worker->>Chain: Verify exact deposit
  Worker->>Worker: Watch swaps-stream and/or getTokenData triggers
  Worker->>Engine: Claim order and request fresh quote
  Engine->>Chain: Sign and broadcast swap from deposit wallet
  Engine->>User: Settle tokenOut to recipientAddress
```

## End-to-End Flow

Advanced Orders are deposit-backed intents. The user keeps custody of their wallet keys and never gives Mobula a reusable approval, session key, or delegated signing power. The only funds Mobula can execute with are the exact tokens the user transfers to the Mobula deposit wallet for that order.

<Note>
  The deposited amount is held in the Mobula deposit wallet while the order is open. If the order is cancelled, expires, or cannot execute, the settlement flow refunds the confirmed deposit to the owner.
</Note>

| Step | Actor                   | What happens                                                                                                                                                                                                                |
| ---- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1    | Integrator              | Use the Mobula-provided deposit wallet for the environment. The end user must not choose this address.                                                                                                                      |
| 2    | User wallet             | Transfer exactly `amountRaw` of `tokenIn` from `ownerAddress` to the Mobula deposit wallet.                                                                                                                                 |
| 3    | Client                  | Call `POST /api/2/swap/advanced-orders/intent` with the deposit transaction hash and all order parameters.                                                                                                                  |
| 4    | Mobula API              | Normalizes the payload, resolves the server deposit wallet, validates addresses and amounts, then returns the canonical `message` to sign.                                                                                  |
| 5    | User wallet             | Signs the canonical `message`. This signature covers the chain, owner, recipient, tokens, amounts, trigger settings, expiry, deposit transaction, routing restrictions, and fees.                                           |
| 6    | Client                  | Call `POST /api/2/swap/advanced-orders` with the same fields plus `intentSignature`.                                                                                                                                        |
| 7    | Mobula API              | Verifies the owner signature and stores the order idempotently by intent hash.                                                                                                                                              |
| 8    | Mobula worker           | Polls open orders every few seconds and verifies the deposit transaction on-chain. The deposit must be finalized, successful, sent by `ownerAddress`, sent to the Mobula deposit wallet, and match `tokenIn` + `amountRaw`. |
| 9    | Mobula worker           | Watches `swaps-stream` for relevant token/pair updates. For TP/SL it pre-checks quote movement; for Triggers it confirms the configured Token Details fields through `getTokenData`.                                        |
| 10   | Mobula execution engine | Claims the order, builds a fresh quote with the order constraints, checks `minAmountOutRaw`, signs only the swap transaction for the deposit wallet, and broadcasts it.                                                     |
| 11   | Mobula settlement       | Confirms the broadcasted transaction. If filled, output funds are sent to `recipientAddress`. If cancelled, expired, or non-executable, the confirmed deposit is refunded to `ownerAddress`.                                |

## Non-Custodial Guarantees

| Guarantee                                    | How it is enforced                                                                                                                          |
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| Mobula cannot trade from the user's wallet   | The user only signs an intent message, not a transaction spending future wallet balances.                                                   |
| Mobula cannot mutate the order after signing | The canonical message includes the full order payload. Changing any signed field changes the intent hash and invalidates the signature.     |
| Mobula cannot use a different deposit        | Deposit verification checks the exact finalized transfer: owner, deposit wallet, token, and raw amount.                                     |
| Mobula cannot bypass the output guard        | Execution always checks the fresh quote against `minAmountOutRaw` before sending.                                                           |
| The user can exit before execution           | The owner can cancel open/triggered orders with a signed cancel message. Confirmed deposits are then handled by the refund settlement flow. |

## Client Checklist

1. Build and send the token transfer to the Mobula deposit wallet.
2. Wait for the deposit transaction signature.
3. Call `POST /advanced-orders/intent`.
4. Ask the owner wallet to sign `data.message`.
5. Call `POST /advanced-orders` with `intentSignature`.
6. Poll `GET /advanced-orders` or `GET /advanced-orders/:orderId`.
7. To cancel, sign the canonical cancel message and call `POST /advanced-orders/:orderId/cancel`.

## Important Bot Rule: One Deposit, One Order

Advanced Orders are deposit-backed. A single deposit transaction can only back one order because the API stores orders idempotently by intent and enforces unique deposit usage.

For a TP/SL bot, choose one model:

| Model           | How it works                                                                                                 |
| --------------- | ------------------------------------------------------------------------------------------------------------ |
| One active exit | Create either the take-profit or the stop-loss order, then cancel/replace it when the strategy changes.      |
| Split position  | Split the position into two deposits, one backing the take-profit order and one backing the stop-loss order. |
| OCO bracket     | Not available yet as one atomic order. Do not reuse the same deposit for both TP and SL.                     |

## Price Math

Advanced Orders use raw integer amounts. For a sell order from a token to USDC:

```txt theme={null}
targetOutRaw = floor(amountInRaw * targetPriceUSD * 10^tokenOutDecimals / 10^tokenInDecimals)
```

Example:

| Value             | Example                          |
| ----------------- | -------------------------------- |
| Token amount      | `29654.6` tokens                 |
| Token decimals    | `6`                              |
| Take-profit price | `$0.00036`                       |
| Stop-loss price   | `$0.00028`                       |
| USDC decimals     | `6`                              |
| TP output         | `10.675656` USDC, raw `10675656` |
| SL output         | `8.303288` USDC, raw `8303288`   |

Use `triggerAmountOutRaw` for classic TP/SL quote thresholds. Use `triggers` when the bot wants to trigger from Token Details fields like `priceUSD`, `volume24hUSD`, or `security.noMintAuthority`.

## Shared TypeScript Helpers

```ts theme={null}
const MOBULA_BASE_URL = "https://api.mobula.io";
const CHAIN_ID = "solana:solana";
const USDC_MINT = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
const USDC_DECIMALS = 6;

type TriggerDirection = "gte" | "lte";
type OrderKind = "take_profit" | "stop_loss" | "limit";

type AdvancedOrderIntentBody = {
  chainId: string;
  ownerAddress: string;
  recipientAddress: string;
  tokenIn: string;
  tokenOut: string;
  amountRaw: string;
  minAmountOutRaw: string;
  triggerAmountOutRaw: string;
  triggerDirection: TriggerDirection;
  orderKind: OrderKind;
  slippage: number;
  depositTransactionHash: string;
  expiresAt: string;
  onlyRouters?: string[];
  triggers?: unknown;
};

async function mobula<T>(path: string, init: RequestInit = {}): Promise<T> {
  const res = await fetch(`${MOBULA_BASE_URL}${path}`, {
    ...init,
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.MOBULA_API_KEY!,
      ...(init.headers ?? {}),
    },
  });

  const json = await res.json();
  if (!res.ok) throw new Error(json.message ?? JSON.stringify(json));
  return json.data as T;
}

function decimalToRaw(value: string, decimals: number): bigint {
  const [whole = "0", fraction = ""] = value.split(".");
  const paddedFraction = fraction.padEnd(decimals, "0").slice(0, decimals);
  return BigInt(whole) * 10n ** BigInt(decimals) + BigInt(paddedFraction || "0");
}

function quoteOutRawAtPrice(params: {
  amountInRaw: string;
  tokenInDecimals: number;
  tokenOutDecimals: number;
  tokenInPriceUSD: string;
  tokenOutPriceUSD?: string;
}): string {
  const priceScale = 12;
  const tokenInPrice = decimalToRaw(params.tokenInPriceUSD, priceScale);
  const tokenOutPrice = decimalToRaw(params.tokenOutPriceUSD ?? "1", priceScale);
  const amountInRaw = BigInt(params.amountInRaw);

  return (
    (amountInRaw * tokenInPrice * 10n ** BigInt(params.tokenOutDecimals)) /
    (10n ** BigInt(params.tokenInDecimals) * tokenOutPrice)
  ).toString();
}

function applyBps(raw: string, bps: number): string {
  return ((BigInt(raw) * BigInt(bps)) / 10_000n).toString();
}
```

## Fetch the Current Token Price

Use Token Details to show the current price in your bot UI and to build custom trigger rules.

```ts theme={null}
type TokenDetails = {
  address: string;
  chainId: string;
  decimals: number;
  priceUSD: number;
  volume24hUSD?: number;
  security?: {
    noMintAuthority?: boolean;
    isFreezable?: boolean;
    buyTax?: string;
    sellTax?: string;
  } | null;
};

async function getTokenDetails(tokenMint: string): Promise<TokenDetails> {
  return mobula<TokenDetails>(
    `/api/2/token/details?chainId=${encodeURIComponent(CHAIN_ID)}&address=${encodeURIComponent(tokenMint)}`,
    { method: "GET" },
  );
}

const token = await getTokenDetails("6KDh3wLSZMg37nnU7prtKZr7Rut7WQGSf33Vp1G7pump");

console.log({
  priceUSD: token.priceUSD,
  volume24hUSD: token.volume24hUSD,
  decimals: token.decimals,
  security: token.security,
});
```

## Build the Deposit Transaction

The user must transfer exactly `amountRaw` of `tokenIn` to the Mobula deposit wallet for your environment.

```ts theme={null}
import {
  Connection,
  PublicKey,
  Transaction,
} from "@solana/web3.js";
import {
  createAssociatedTokenAccountIdempotentInstruction,
  createTransferInstruction,
  getAssociatedTokenAddressSync,
} from "@solana/spl-token";

async function buildSplDepositTransaction(params: {
  connection: Connection;
  ownerAddress: string;
  depositWalletAddress: string;
  tokenMint: string;
  amountRaw: string;
}): Promise<Transaction> {
  const owner = new PublicKey(params.ownerAddress);
  const depositWallet = new PublicKey(params.depositWalletAddress);
  const mint = new PublicKey(params.tokenMint);

  const ownerAta = getAssociatedTokenAddressSync(mint, owner);
  const depositAta = getAssociatedTokenAddressSync(mint, depositWallet, true);

  const tx = new Transaction().add(
    createAssociatedTokenAccountIdempotentInstruction(owner, depositAta, depositWallet, mint),
    createTransferInstruction(ownerAta, depositAta, owner, BigInt(params.amountRaw)),
  );

  tx.feePayer = owner;
  tx.recentBlockhash = (await params.connection.getLatestBlockhash("confirmed")).blockhash;
  return tx;
}
```

After the user signs and sends this transaction, keep the Solana signature as `depositTransactionHash`.

## Create a Classic Take-Profit Order

This version uses quote output as the trigger. It executes when the fresh quote output reaches the target output.

```ts theme={null}
async function createTakeProfitOrder(params: {
  ownerAddress: string;
  tokenMint: string;
  depositTransactionHash: string;
  amountRaw: string;
  tokenDecimals: number;
  takeProfitPriceUSD: string;
}) {
  const triggerAmountOutRaw = quoteOutRawAtPrice({
    amountInRaw: params.amountRaw,
    tokenInDecimals: params.tokenDecimals,
    tokenOutDecimals: USDC_DECIMALS,
    tokenInPriceUSD: params.takeProfitPriceUSD,
  });

  const intentBody: AdvancedOrderIntentBody = {
    chainId: CHAIN_ID,
    ownerAddress: params.ownerAddress,
    recipientAddress: params.ownerAddress,
    tokenIn: params.tokenMint,
    tokenOut: USDC_MINT,
    amountRaw: params.amountRaw,
    minAmountOutRaw: applyBps(triggerAmountOutRaw, 9_900), // 1% execution guard
    triggerAmountOutRaw,
    triggerDirection: "gte",
    orderKind: "take_profit",
    slippage: 1,
    depositTransactionHash: params.depositTransactionHash,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
    onlyRouters: ["mobula"],
  };

  return createAdvancedOrder(intentBody);
}
```

## Create a Classic Stop-Loss Order

This version executes when the fresh quote output falls to the stop threshold or lower.

```ts theme={null}
async function createStopLossOrder(params: {
  ownerAddress: string;
  tokenMint: string;
  depositTransactionHash: string;
  amountRaw: string;
  tokenDecimals: number;
  stopLossPriceUSD: string;
}) {
  const triggerAmountOutRaw = quoteOutRawAtPrice({
    amountInRaw: params.amountRaw,
    tokenInDecimals: params.tokenDecimals,
    tokenOutDecimals: USDC_DECIMALS,
    tokenInPriceUSD: params.stopLossPriceUSD,
  });

  const intentBody: AdvancedOrderIntentBody = {
    chainId: CHAIN_ID,
    ownerAddress: params.ownerAddress,
    recipientAddress: params.ownerAddress,
    tokenIn: params.tokenMint,
    tokenOut: USDC_MINT,
    amountRaw: params.amountRaw,
    minAmountOutRaw: applyBps(triggerAmountOutRaw, 9_800), // 2% downside execution guard
    triggerAmountOutRaw,
    triggerDirection: "lte",
    orderKind: "stop_loss",
    slippage: 2,
    depositTransactionHash: params.depositTransactionHash,
    expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
    onlyRouters: ["mobula"],
  };

  return createAdvancedOrder(intentBody);
}
```

## Add Custom Triggers

Custom triggers are checked through `getTokenData` / Token Details. This is useful when the bot should only execute after both a price condition and safety conditions match.

```ts theme={null}
function withSafetyTriggers(params: {
  direction: "take_profit" | "stop_loss";
  targetPriceUSD: number;
}) {
  return {
    all: [
      {
        field: "priceUSD",
        operator: params.direction === "take_profit" ? "gte" : "lte",
        value: params.targetPriceUSD,
      },
      { field: "volume24hUSD", operator: "gte", value: 10_000 },
      { field: "security.noMintAuthority", operator: "equals", value: true },
      { field: "security.isFreezable", operator: "equals", value: false },
    ],
  };
}
```

Attach the trigger expression to the same intent body:

```ts theme={null}
const tokenDetails = await getTokenDetails(tokenMint);
const amountRaw = decimalToRaw("29654.6", tokenDetails.decimals).toString();

const takeProfitPriceUSD = "0.00036";
const takeProfitOutRaw = quoteOutRawAtPrice({
  amountInRaw: amountRaw,
  tokenInDecimals: tokenDetails.decimals,
  tokenOutDecimals: USDC_DECIMALS,
  tokenInPriceUSD: takeProfitPriceUSD,
});

const intentBody: AdvancedOrderIntentBody = {
  chainId: CHAIN_ID,
  ownerAddress,
  recipientAddress: ownerAddress,
  tokenIn: tokenMint,
  tokenOut: USDC_MINT,
  amountRaw,
  minAmountOutRaw: applyBps(takeProfitOutRaw, 9_900),
  triggerAmountOutRaw: takeProfitOutRaw,
  triggerDirection: "gte",
  orderKind: "take_profit",
  slippage: 1,
  depositTransactionHash,
  expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
  onlyRouters: ["mobula"],
  triggers: withSafetyTriggers({
    direction: "take_profit",
    targetPriceUSD: Number(takeProfitPriceUSD),
  }),
};
```

When `triggers` is present, Mobula confirms that trigger expression first. The execution engine still quotes the swap and rejects execution if the quote does not satisfy `minAmountOutRaw`.

## Sign and Create the Order

The user signs the canonical message returned by the intent endpoint. The bot should never construct this message itself.

```ts theme={null}
import bs58 from "bs58";

type PreparedAdvancedOrderIntent = {
  orderId: string;
  intentHash: string;
  message: string;
  payload: AdvancedOrderIntentBody & {
    version: 1;
    depositWalletAddress: string;
  };
};

type SolanaMessageSigner = {
  signMessage(message: Uint8Array): Promise<Uint8Array>;
};

declare const wallet: SolanaMessageSigner;

async function createAdvancedOrder(intentBody: AdvancedOrderIntentBody) {
  const prepared = await mobula<PreparedAdvancedOrderIntent>("/api/2/swap/advanced-orders/intent", {
    method: "POST",
    body: JSON.stringify(intentBody),
  });

  const intentSignature = await signWalletMessage(prepared.message);

  return mobula(`/api/2/swap/advanced-orders`, {
    method: "POST",
    body: JSON.stringify({
      ...intentBody,
      intentMessage: prepared.message,
      intentSignature,
    }),
  });
}

async function signWalletMessage(message: string): Promise<string> {
  const messageBytes = new TextEncoder().encode(message);
  const signatureBytes = await wallet.signMessage(messageBytes);
  return bs58.encode(signatureBytes);
}
```

## Monitor and Cancel Orders

The bot can read owner orders with a read signature. Cancellation uses a different canonical message.

```ts theme={null}
function readMessage(ownerAddress: string, orderId?: string) {
  return `Mobula Advanced Order Read v1\n${JSON.stringify({
    ...(orderId ? { orderId } : {}),
    ownerAddress,
  })}`;
}

function cancelMessage(params: {
  chainId: string;
  orderId: string;
  ownerAddress: string;
}) {
  return `Mobula Advanced Order Cancel v1\n${JSON.stringify(params)}`;
}

async function listOrders(ownerAddress: string) {
  const readSignature = await signWalletMessage(readMessage(ownerAddress));
  return mobula(`/api/2/swap/advanced-orders?ownerAddress=${ownerAddress}&readSignature=${readSignature}`);
}

async function cancelOrder(orderId: string, ownerAddress: string) {
  const cancelSignature = await signWalletMessage(
    cancelMessage({ chainId: CHAIN_ID, orderId, ownerAddress }),
  );

  return mobula(`/api/2/swap/advanced-orders/${orderId}/cancel`, {
    method: "POST",
    body: JSON.stringify({ ownerAddress, cancelSignature }),
  });
}
```

## Production Checklist

| Check                                                  | Why it matters                                                                                   |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------ |
| Do not reuse deposit transactions                      | One deposit backs one order.                                                                     |
| Show `priceUSD`, target price, and computed output raw | Users need to understand the TP/SL line and expected proceeds.                                   |
| Use `minAmountOutRaw` as a hard guard                  | It protects the user from bad final execution quotes.                                            |
| Use custom triggers for safety conditions              | Example: require volume, no mint authority, and non-freezable token before execution.            |
| Keep cancellation visible                              | Users can cancel open/triggered orders before execution claim.                                   |
| Track `status`, `depositStatus`, `settlementStatus`    | These three fields explain whether the order is waiting, executing, filled, refunded, or failed. |

## Related Docs

<CardGroup cols={3}>
  <Card title="Take Profit" icon="arrow-up-right" href="/exec/advanced-orders/take-profit">
    Create quote-based take-profit orders with Mobula Advanced Orders.
  </Card>

  <Card title="Stop Loss" icon="arrow-down-right" href="/exec/advanced-orders/stop-loss">
    Create stop-loss orders with raw output thresholds and signed intents.
  </Card>

  <Card title="Triggers" icon="zap" href="/exec/advanced-orders/triggers">
    Add Token Details conditions such as price, volume, holders, and security flags.
  </Card>
</CardGroup>
