Skip to main content
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.
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

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.
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.
StepActorWhat happens
1IntegratorUse the Mobula-provided deposit wallet for the environment. The end user must not choose this address.
2User walletTransfer exactly amountRaw of tokenIn from ownerAddress to the Mobula deposit wallet.
3ClientCall POST /api/2/swap/advanced-orders/intent with the deposit transaction hash and all order parameters.
4Mobula APINormalizes the payload, resolves the server deposit wallet, validates addresses and amounts, then returns the canonical message to sign.
5User walletSigns the canonical message. This signature covers the chain, owner, recipient, tokens, amounts, trigger settings, expiry, deposit transaction, routing restrictions, and fees.
6ClientCall POST /api/2/swap/advanced-orders with the same fields plus intentSignature.
7Mobula APIVerifies the owner signature and stores the order idempotently by intent hash.
8Mobula workerPolls 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.
9Mobula workerWatches 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.
10Mobula execution engineClaims the order, builds a fresh quote with the order constraints, checks minAmountOutRaw, signs only the swap transaction for the deposit wallet, and broadcasts it.
11Mobula settlementConfirms 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

GuaranteeHow it is enforced
Mobula cannot trade from the user’s walletThe user only signs an intent message, not a transaction spending future wallet balances.
Mobula cannot mutate the order after signingThe canonical message includes the full order payload. Changing any signed field changes the intent hash and invalidates the signature.
Mobula cannot use a different depositDeposit verification checks the exact finalized transfer: owner, deposit wallet, token, and raw amount.
Mobula cannot bypass the output guardExecution always checks the fresh quote against minAmountOutRaw before sending.
The user can exit before executionThe 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:
ModelHow it works
One active exitCreate either the take-profit or the stop-loss order, then cancel/replace it when the strategy changes.
Split positionSplit the position into two deposits, one backing the take-profit order and one backing the stop-loss order.
OCO bracketNot 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:
targetOutRaw = floor(amountInRaw * targetPriceUSD * 10^tokenOutDecimals / 10^tokenInDecimals)
Example:
ValueExample
Token amount29654.6 tokens
Token decimals6
Take-profit price$0.00036
Stop-loss price$0.00028
USDC decimals6
TP output10.675656 USDC, raw 10675656
SL output8.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

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

CheckWhy it matters
Do not reuse deposit transactionsOne deposit backs one order.
Show priceUSD, target price, and computed output rawUsers need to understand the TP/SL line and expected proceeds.
Use minAmountOutRaw as a hard guardIt protects the user from bad final execution quotes.
Use custom triggers for safety conditionsExample: require volume, no mint authority, and non-freezable token before execution.
Keep cancellation visibleUsers can cancel open/triggered orders before execution claim.
Track status, depositStatus, settlementStatusThese three fields explain whether the order is waiting, executing, filled, refunded, or failed.

Take Profit

Create quote-based take-profit orders with Mobula Advanced Orders.

Stop Loss

Create stop-loss orders with raw output thresholds and signed intents.

Triggers

Add Token Details conditions such as price, volume, holders, and security flags.