Skip to content

X-Trader API write rules: Idempotency-Key, If-Match, ETag, decimal strings

Every write to the X-Trader Public API must use a JSON body of at most 64 KiB, strict objects with no unknown properties, and decimal strings (not JSON numbers) for amounts, fees, percentages and durations. Create requests require an Idempotency-Key header; update, pause, resume and delete requests on configs and templates require If-Match with the latest ETag such as "v3". New configurations are always created paused, and committed changes reach execution nodes asynchronously.

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.

Operation Idempotency-Key If-Match Response ETag
POST create config (sniper, copy-trade, transfer-snipe) required no yes (201)
POST create template (sniper, copy-trade) required no yes (201)
POST /labels required no no
PATCH config or template no required yes (200)
POST /{id}/pause, POST /{id}/resume no required yes (200)
DELETE config or template no required no (204, empty)
PATCH /labels/{id}, DELETE /labels/{id} no not used no
GET single config or template n/a n/a yes
  • JSON bodies are limited to 64 KiB.
  • Every object in a request schema is strict (additionalProperties: false). An unknown property anywhere, including inside nested config objects, is rejected with validation_error instead of being ignored.
  • user_id is never accepted. Config create and update bodies also reject version and template. Template bodies reject user_id, version and risk_label_id. Label bodies reject user_id.
  • PATCH bodies must contain at least one top-level field.
  • Text fields are trimmed: leading and trailing spaces are removed from remark (max 64 characters) and from name (labels 1-32, templates 1-64 characters).

Amounts, fees, percentages and durations are sent as decimal strings, for example "0.1", with at most 12 fractional digits. Do not send JSON numbers for these fields. Each field has its own range in the schema, for example:

Field (sniper config) Rule from the spec
config.buy_amount SOL, decimal string greater than 0 and at most 1,000,000 (max length 32)
config.jito_mode.tip_amount SOL, decimal string from 0 to 1,000
config.jito_mode.priority_fee SOL, decimal string from 0 to 1,000
{ "buy_amount": "0.1", "jito_mode": { "tip_amount": "0.001", "priority_fee": "0.0005" } }

Integers that are not amounts (for example label sort_order, or limit in list queries) are ordinary JSON integers. Field-by-field ranges are listed on sniper config and copy-trade config.

Property Value
Header Idempotency-Key
Required on every POST that creates a config, template or label
Length 8-128 characters
Allowed characters A-Z, a-z, 0-9, ., _, :, - (pattern ^[A-Za-z0-9._:-]+$)
Retention 24 hours
Same key, same body safe to retry
Same key, different body idempotency_conflict
Missing header idempotency_required

Practical guidance: generate one key per logical create (for example a UUID or create-sniper-20260720-001), store it with the pending operation, and reuse it on network retries. Use a new key only for a genuinely new resource.

Terminal window
curl --request POST 'https://x-trader.ai/api/v1/sniper/configs' \
--header "Authorization: Bearer $X_TRADER_API_TOKEN" \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: create-sniper-20261016-001' \
--data @sniper-config.json

If-Match and ETag (optimistic concurrency)

Section titled “If-Match and ETag (optimistic concurrency)”

Configs and templates carry an integer version. Single-resource responses return it as the ETag header in the form "v<version>", for example "v3". Writes to an existing config or template must send that value back:

Property Value
Header If-Match
Format "v<N>" including the double quotes, pattern ^"v[1-9][0-9]*"$
Required on PATCH, DELETE, POST /{id}/pause, POST /{id}/resume for configs; PATCH, DELETE for templates
Stale value version_conflict

On version_conflict, GET the resource again, compare its current state with the intended change, and only then decide whether to resend with the new ETag. List responses do not return ETag headers; take the version from a single-resource GET or from the last write response.

Changing or clearing a label binding (risk_label_id) does not require pausing the task, but it still requires the latest If-Match.

import os
import requests
BASE = "https://x-trader.ai/api/v1"
H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"}
cfg_id = "00000000-0000-4000-8000-000000000000" # your config id
r = requests.get(f"{BASE}/sniper/configs/{cfg_id}", headers=H, timeout=10)
r.raise_for_status()
etag = r.headers["ETag"] # e.g. '"v3"'
r = requests.patch(
f"{BASE}/sniper/configs/{cfg_id}",
headers={**H, "If-Match": etag},
json={"remark": "night session"},
timeout=10,
)
if r.status_code == 409 and r.json()["error"]["code"] == "version_conflict":
# someone else changed it: re-read before deciding to retry
pass
r.raise_for_status()
new_etag = r.headers["ETag"]
Change Active config Paused config
remark allowed allowed
risk_label_id (bind, change, null to unassign) allowed allowed
config (full replacement) rejected, pause first (config_must_be_paused) allowed
linked_wallets (copy-trade, full replacement) rejected, pause first allowed
target_wallet immutable immutable
Delete rejected allowed

When config is present in a PATCH, it replaces the whole nested object and must satisfy the full schema; partial nested patches are not supported. linked_wallets likewise replaces the whole array.

Create endpoints always store the resource with is_active=false. Inspect the 201 response, then call POST /{id}/resume with its ETag to start execution. Resume revalidates the complete stored config against the current strict schema; data that no longer passes is not activated.

Terminal window
curl --request POST "https://x-trader.ai/api/v1/sniper/configs/$CONFIG_ID/resume" \
--header "Authorization: Bearer $X_TRADER_API_TOKEN" \
--header 'If-Match: "v1"'

A successful write returns meta.persistence = "committed": the database write is complete. meta.propagation = "eventual" means execution nodes observe the change asynchronously. Pause changes the database state immediately, but nodes may still act on the previous state for a short time. For accounts with very large task sets the spec states refresh can take 30-60 seconds in extreme cases.

  • Sniper, copy-trade and transfer-snipe tasks share one label set per account, maximum 20 labels.
  • Create labels at /labels, then bind them to tasks with risk_label_id on create or PATCH.
  • A label id owned by another account returns 404 not_found, the same as a missing id.
  • Labels do not use ETag or If-Match. Details: labels.
  • Sniper templates use sniper:* scopes; copy-trade templates use copytrade:* scopes. There are no transfer-snipe templates.
  • Sniper and copy-trade templates share a limit of 1,000 templates per account.
  • Templates never accept or return risk_label_id.
  • Updating a template does not rewrite existing task snapshots. Deleting a template detaches linked tasks (ON DELETE SET NULL) without deleting them or changing their stored config.
  • Details: templates.

List and Get return the stored snapshot as-is. Configurations created earlier through the web UI may contain legacy fields that the current request schema would reject. Create, update and resume always enforce the latest strict schema, so a snapshot read from the API is not guaranteed to be valid as a write body. Remove or migrate legacy fields before sending a config back in a PATCH, otherwise expect validation_error; a paused legacy config whose stored data fails revalidation will not resume.

Amounts, fees, percentages and durations are decimal strings such as "0.1" with at most 12 fractional digits. JSON numbers are not accepted for these fields.

What happens if I retry a create with the same Idempotency-Key?

Section titled “What happens if I retry a create with the same Idempotency-Key?”

The same key with the same body can be retried safely within the 24-hour retention window. The same key with a different body returns idempotency_conflict.

From the ETag header of the latest single-resource response (create, get, update, pause or resume), for example "v3". It equals the resource version field.

Configurations are always created paused (is_active=false). Call POST /{id}/resume with the current If-Match to start execution.

No. Labels do not use ETag or If-Match. Label creation still requires an Idempotency-Key.