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.
Where auto_sell lives
Section titled “Where auto_sell lives”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.
Top-level fields
Section titled “Top-level fields”| 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. |
tp_orders stage fields
Section titled “tp_orders stage fields”| 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).
Switching basis
Section titled “Switching basis”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.
tp_multi_hit: merge vs highest_only
Section titled “tp_multi_hit: merge vs highest_only”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. |
Example
Section titled “Example”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.
tp_loop
Section titled “tp_loop”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.
Per-stage timeout
Section titled “Per-stage timeout”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.
sl_orders
Section titled “sl_orders”| 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.
follow_sell and proportional_sell
Section titled “follow_sell and proportional_sell”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.
trailing_take_profit
Section titled “trailing_take_profit”| 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.
ignore_fee_for_auto_sell
Section titled “ignore_fee_for_auto_sell”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.
Example inside a create body
Section titled “Example inside a create body”{ "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.
Updating auto_sell on an existing config
Section titled “Updating auto_sell on an existing config”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.
ID='<config-uuid>'BASE='https://x-trader.ai/api/v1'
# 1. Read the current config and its ETagcurl -sD headers.txt "$BASE/copy-trade/configs/$ID" \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ | jq '.data.config' > config.jsonETAG=$(grep -i '^etag:' headers.txt | cut -d' ' -f2 | tr -d '\r')
# 2. Replace auto_sell inside the full configjq '.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-Matchcurl --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.jsonPython
Section titled “Python”import osimport 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.
Can I PATCH only auto_sell?
Section titled “Can I PATCH only auto_sell?”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.