Skip to content

X-Trader auto_sell: TP/SL stages, tp_trigger_basis, tp_multi_hit, trailing TP

auto_sell is the required object inside config that controls automatic exits: take-profit stages (tp_orders), stop-loss stages (sl_orders), trailing take-profit, sells that follow the target wallet, and an expiry. tp_trigger_basis decides whether each TP trigger is measured from the entry cost or from the previous stage, and tp_multi_hit decides what happens when one price move crosses several stages. The schema is the same for sniper, copy-trade and transfer-snipe configurations.

Primary source: X-Trader API reference. This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader.

auto_sell is a required key of config in SniperCreate, SniperPatch, CopyTradeCreate, CopyTradePatch, TransferSnipeCreate, TransferSnipePatch and both template schemas. The JSON schema of the object is byte-for-byte identical in all of them, so there is no sniper-only or copy-trade-only field. Its siblings are auto_strategy and timed_strategy. See sniper config and copy-trade config for the rest of config.

The object is strict (additionalProperties: false): unknown keys return validation_error. All percentages, durations and amounts are decimal strings (for example "50", not 50), at most 32 characters and at most 12 fractional digits. See write rules.

Field Type Required Values and limits Meaning
enabled boolean yes Turns automatic selling on or off.
expiry_time string yes "0", or minutes "1" to "525600" How long the auto-sell rules stay active. "0" means permanent. 525,600 minutes is 365 days.
tp_orders array yes no fixed count limit (64 KiB body limit applies) Take-profit stages. May be empty.
sl_orders array yes no fixed count limit Stop-loss stages. May be empty.
follow_sell boolean yes Sell when the followed wallet sells.
proportional_sell boolean yes When follow-sell fires, sell the same ratio the followed wallet sold.
tp_trigger_basis string no entry_price (default), previous_stage How TP triggers are measured.
tp_multi_hit string no merge (default), highest_only Behaviour when one jump covers several TP stages. entry_price only.
tp_loop boolean no Turns the last tp_orders item into a repeating, compounding rule.
trailing_take_profit object no default {"enabled": false, "tiers": []} Trailing TP tiers.
ignore_fee_for_auto_sell boolean no omitted means global setting Whether trigger PnL ignores Gas/Tip.
tab string no tpsl, dev Stored auto-sell mode identifier.
Field Type Required Values and limits
trigger string yes PnL percentage, greater than 0. Meaning depends on tp_trigger_basis.
sell string yes Percentage of the current position, "0" to "100". "0" advances the stage without selling.
timeout object no See Per-stage timeout.

sell is a share of the position held at the moment the stage fires, not of the original buy. Two stages of "50" therefore leave 25% of the original position, not 0%.

tp_trigger_basis: entry_price vs previous_stage

Section titled “tp_trigger_basis: entry_price vs previous_stage”
Value Ordering What trigger measures
entry_price (default; omit for legacy configs) Stages are sorted by trigger ascending, array order does not matter. Total PnL relative to entry cost for every stage.
previous_stage Array order is kept as sent. TP1: PnL from entry. TP2 and later: percent rise from the previous stage’s confirmed trigger or fill price.

Worked example: same numbers, different prices

Section titled “Worked example: same numbers, different prices”

Take entry cost normalised to 1.00 and this tp_orders array:

[
{ "trigger": "50", "sell": "30" },
{ "trigger": "100", "sell": "30" },
{ "trigger": "200", "sell": "40" }
]
Stage entry_price target previous_stage target
TP1 (50) 1.00 x 1.50 = 1.50 (+50%) 1.00 x 1.50 = 1.50 (+50%)
TP2 (100) 1.00 x 2.00 = 2.00 (+100%) 1.50 x 2.00 = 3.00 (+200% from entry)
TP3 (200) 1.00 x 3.00 = 3.00 (+200%) 3.00 x 3.00 = 9.00 (+800% from entry)

Under previous_stage the base for TP2 and TP3 is the previous stage’s confirmed trigger or fill price, so with slippage the real targets differ slightly from the arithmetic above. The spec does not state which of the two (trigger or fill) is used in which case.

To express “sell at 1.5x, 2x and 3x of entry” under previous_stage, the triggers would have to be 50, 33.33, 50 (1.5 x 1.3333 = 2.0, 2.0 x 1.5 = 3.0).

The product UI locks the basis while tp_orders is non-empty, but the API accepts any combination. Flipping tp_trigger_basis alone silently changes every target after TP1. When changing the basis, rewrite tp_orders in the same PATCH so the numbers match the new meaning.

A single price tick can jump past several stages at once. tp_multi_hit decides how many sells that produces. It applies only when tp_trigger_basis is entry_price or omitted; sending it together with previous_stage is rejected with the reason tp_multi_hit_requires_entry_price.

Value Behaviour
merge (default; omit for legacy) One sell whose size is the sum of the sell values of all covered stages, capped at 100%.
highest_only Intermediate covered stages are marked complete without selling; only the highest covered stage’s sell is executed.

Stages under entry_price: TP1 +50% sell 25, TP2 +100% sell 25, TP3 +200% sell 50. Price moves from +30% to +120% in one tick, crossing TP1 and TP2.

Mode Result of the tick Stages left
merge One sell of 25 + 25 = 50% of the current position. TP3
highest_only TP1 marked done, no sell. One sell of TP2’s 25%. TP3

If the same tick reached +250% (all three stages): merge sells 25 + 25 + 50 = 100% (the cap); highest_only sells TP3’s 50% and keeps the rest.

With tp_loop: true, the raw last item of tp_orders is not an ordinary stage. It becomes an independent loop rule that keeps generating new targets while a balance remains.

Rule Detail
Position of the loop rule Always the last array item. Under entry_price only the items before it are sorted; the loop item stays last and is never included in fixed-stage sorting.
First loop target Preceding ordinary target x (1 + trigger / 100). With no ordinary stage, entry x (1 + trigger / 100).
Later targets Each compounds from the preceding loop target by the same factor.
sell Must be below 100. "0" advances to the next target without submitting a trade.
timeout Restarts for every generated target; seconds may be "0" when enabled.

Example under entry_price: ordinary stages +50% and +100%, loop item { "trigger": "50", "sell": "20" }. The last ordinary target is 2.00x entry, so loop targets are 3.00x, 4.50x, 6.75x and so on, each selling 20% of what is left. The spec describes the first-target formula explicitly for entry_price; it does not give a separate formula for previous_stage.

tp_orders[].timeout sells part of the position if the next tier is not reached in time.

Field Type Required Values and limits
enabled boolean yes
seconds string yes "0" to "86400", 0.1-second precision
sell string yes "1" to "100" when enabled

The schema requires all three keys whenever timeout is present, even with enabled: false. The spec does not state exactly when the countdown starts; the description ties it to reaching the next tier, so the practical reading is that it runs after the stage fires. For loop targets it restarts at each generated target.

Field Type Required Values and limits
trigger string yes "-100" to "-1" (PnL percent)
sell string yes "1" to "100", percent of the current position
confirm_seconds string no Max 32 characters. The spec gives no description or range.

Stop-loss stages fire immediately when their trigger is hit. Delayed and two-phase stop-losses are separate rules in auto_strategy (sl_protection, phased_sl) and do not replace sl_orders.

Both are required booleans.

follow_sell proportional_sell Behaviour
false ignored No sells triggered by the followed wallet.
true false Sell when the followed wallet sells. The spec does not state the size in this case.
true true Sell the same ratio of the position that the followed wallet sold.

The field is defined in the shared schema for sniper configs as well; the spec does not describe how “followed trader” is resolved for a sniper target.

Field Type Required Values and limits
enabled boolean yes
tiers array yes No fixed tier-count limit
tiers[].activation string yes Profit percentage that arms the tier
tiers[].drawdown string yes Greater than 0, at most 100: drop from the observed peak
tiers[].sell string yes Greater than 0, percent of the current position

A tier is armed once profit reaches activation; after that, a fall of drawdown percent from the highest observed point sells sell percent. The object defaults to {"enabled": false, "tiers": []}. The spec does not document how tiers are ordered or whether several armed tiers can fire together.

Optional boolean that changes the PnL used for triggers of TP, SL, trailing TP and profit protection (auto_strategy.profit_protection):

Value Trigger PnL
true Ignores Gas and Tip.
false Includes Gas and Tip.
omitted Follows the account’s global setting.

Positions, trade details and Telegram notifications always use the real cost regardless of this flag.

tab stores the auto-sell mode identifier, tpsl or dev. The spec does not describe how the two modes behave differently.

{
"target_wallet": "<solana-address>",
"config": {
"auto_sell": {
"enabled": true,
"expiry_time": "1440",
"tp_trigger_basis": "entry_price",
"tp_multi_hit": "merge",
"tp_loop": false,
"ignore_fee_for_auto_sell": false,
"tp_orders": [
{ "trigger": "50", "sell": "30",
"timeout": { "enabled": true, "seconds": "600", "sell": "50" } },
{ "trigger": "100", "sell": "30" },
{ "trigger": "200", "sell": "100" }
],
"sl_orders": [
{ "trigger": "-30", "sell": "100" }
],
"follow_sell": true,
"proportional_sell": true,
"trailing_take_profit": {
"enabled": true,
"tiers": [
{ "activation": "80", "drawdown": "20", "sell": "50" }
]
}
},
"auto_strategy": { "migration_clear": false, "timed_sell": false, "sell_delay": "0" },
"timed_strategy": { "enabled": false, "orders": [] }
}
}

The other required config keys are omitted here; they differ between sniper and copy-trade.

PATCH replaces the complete config, so the flow is: GET the config, take its ETag, edit auto_sell locally, PATCH the full config with If-Match. The config must be paused, otherwise the API returns config_must_be_paused. A stale ETag returns version_conflict. Snapshots returned by GET may contain legacy web-UI fields that the strict write schema rejects; remove any key reported in error.fields before retrying. Scopes: sniper:write for /sniper/configs, copytrade:write for /copy-trade/configs.

Terminal window
ID='<config-uuid>'
BASE='https://x-trader.ai/api/v1'
# 1. Read the current config and its ETag
curl -sD headers.txt "$BASE/copy-trade/configs/$ID" \
--header "Authorization: Bearer $X_TRADER_API_TOKEN" \
| jq '.data.config' > config.json
ETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# 2. Replace auto_sell inside the full config
jq '.auto_sell.tp_trigger_basis = "previous_stage"
| del(.auto_sell.tp_multi_hit)
| .auto_sell.tp_orders = [
{"trigger":"50","sell":"30"},
{"trigger":"33.33","sell":"30"},
{"trigger":"50","sell":"100"}]' config.json \
| jq '{config: .}' > patch.json
# 3. Send it with If-Match
curl --request PATCH "$BASE/copy-trade/configs/$ID" \
--header "Authorization: Bearer $X_TRADER_API_TOKEN" \
--header 'Content-Type: application/json' \
--header "If-Match: $ETAG" \
--data @patch.json
import os
import requests
BASE = "https://x-trader.ai/api/v1"
CONFIG_ID = "<config-uuid>"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['X_TRADER_API_TOKEN']}"
url = f"{BASE}/copy-trade/configs/{CONFIG_ID}" # or /sniper/configs/{id}
resp = session.get(url)
resp.raise_for_status()
etag = resp.headers["ETag"] # e.g. "v7"
resource = resp.json()["data"]
if resource["is_active"]:
raise SystemExit("Pause the config first (POST .../pause with If-Match)")
config = resource["config"]
config["auto_sell"].update({
"tp_trigger_basis": "entry_price",
"tp_multi_hit": "highest_only",
"tp_orders": [
{"trigger": "50", "sell": "25"},
{"trigger": "100", "sell": "25"},
{"trigger": "200", "sell": "50"},
],
"sl_orders": [{"trigger": "-35", "sell": "100"}],
})
patch = session.patch(url, json={"config": config}, headers={"If-Match": etag})
if patch.status_code >= 400:
print(patch.json()["error"]) # code, message, fields[]
patch.raise_for_status()
print("new version:", patch.headers["ETag"])

Do the same tp_orders numbers mean the same prices under entry_price and previous_stage?

Section titled “Do the same tp_orders numbers mean the same prices under entry_price and previous_stage?”

No. Under entry_price every trigger is total PnL from entry cost, so 50/100/200 means 1.5x, 2x and 3x. Under previous_stage each stage after the first is a rise from the previous stage’s confirmed price, so 50/100/200 means 1.5x, 3x and 9x.

Can tp_multi_hit be used with previous_stage?

Section titled “Can tp_multi_hit be used with previous_stage?”

No. tp_multi_hit only applies under entry_price (or an omitted basis). Combining it with previous_stage is rejected with the reason tp_multi_hit_requires_entry_price.

Is auto_sell different between sniper and copy-trade configs?

Section titled “Is auto_sell different between sniper and copy-trade configs?”

No. The auto_sell schema is identical in SniperCreate, CopyTradeCreate, their Patch schemas, transfer-snipe and both template schemas.

How do I keep auto-sell active with no expiry?

Section titled “How do I keep auto-sell active with no expiry?”

Set expiry_time to "0". Any other value is a number of minutes from 1 to 525,600.

No. When config is present in a PATCH it replaces the whole nested config and must pass the full schema, so read the current config, change auto_sell and send the complete config with If-Match.