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.
Summary of write requirements
Section titled “Summary of write requirements”| 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 |
Body size and strict objects
Section titled “Body size and strict objects”- JSON bodies are limited to 64 KiB.
- Every object in a request schema is strict (
additionalProperties: false). An unknown property anywhere, including inside nestedconfigobjects, is rejected withvalidation_errorinstead of being ignored. user_idis never accepted. Config create and update bodies also rejectversionandtemplate. Template bodies rejectuser_id,versionandrisk_label_id. Label bodies rejectuser_id.PATCHbodies must contain at least one top-level field.- Text fields are trimmed: leading and trailing spaces are removed from
remark(max 64 characters) and fromname(labels 1-32, templates 1-64 characters).
Decimal strings
Section titled “Decimal strings”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.
Idempotency-Key on create
Section titled “Idempotency-Key on create”| 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.
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.jsonIf-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 osimport 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 passr.raise_for_status()new_etag = r.headers["ETag"]What can change while a config is active
Section titled “What can change while a config is active”| 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.
Configs are created paused
Section titled “Configs are created paused”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.
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"'Persistence and propagation
Section titled “Persistence and propagation”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.
Labels
Section titled “Labels”- 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 withrisk_label_idon create orPATCH. - A label id owned by another account returns
404 not_found, the same as a missing id. - Labels do not use
ETagorIf-Match. Details: labels.
Templates
Section titled “Templates”- Sniper templates use
sniper:*scopes; copy-trade templates usecopytrade:*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.
Legacy fields in stored snapshots
Section titled “Legacy fields in stored snapshots”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.
Why must amounts be sent as strings?
Section titled “Why must amounts be sent as strings?”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.
Where do I get the value for If-Match?
Section titled “Where do I get the value for If-Match?”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.
Why is my new config not running?
Section titled “Why is my new config not running?”Configurations are always created paused (is_active=false). Call POST /{id}/resume with the current If-Match to start execution.
Do labels need If-Match?
Section titled “Do labels need If-Match?”No. Labels do not use ETag or If-Match. Label creation still requires an Idempotency-Key.