# X-Trader Public API: what it does and how to authenticate URL: https://docs.blizz.id/xtrader/overview/ Updated: 2026-10-16 The X-Trader Public API v1.0.0 is a REST interface for managing the caller's own sniper, copy-trade and transfer-snipe configurations, sniper and copy-trade templates, and a shared set of task labels. It never exposes wallet keys, trade exits, fund transfers or administrative capabilities. Every request is authenticated with a personal bearer token of the form `xt_live_.`, and the account is derived from that token alone. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Scope of the API | Managed by the API | Not available through the API | |---|---| | Sniper configurations (`/sniper/configs`) | Wallet private keys | | Copy-trade configurations (`/copy-trade/configs`) | Executing or closing trades (exits) | | Transfer-snipe tasks (`/transfer-snipe/configs`), one watched wallet per task | Fund transfers | | Sniper templates (`/sniper/templates`) | Administrative functions | | Copy-trade templates (`/copy-trade/templates`) | Native backend (Supabase) APIs | | Shared labels (`/labels`) | Any other user's resources | | Credential metadata (`/me`) | Bulk delete | There is no transfer-snipe template endpoint in v1.0.0. Configurations are always created paused and must be resumed explicitly (see [write rules](/xtrader/write-rules/)). ## Base URL and transport | Item | Value | |---|---| | Base URL | `https://x-trader.ai/api/v1` (the only server listed, marked Production) | | Format | JSON request and response bodies | | Auth header | `Authorization: Bearer ` on every request | | Response envelope | `{"data": ..., "meta": {"request_id": ...}}` on success, `{"error": ..., "request_id": ...}` on failure | Write responses carry `meta.persistence = "committed"` (the database write finished) and `meta.propagation = "eventual"` (execution nodes pick the change up shortly after). ## Token format and creation The security scheme is HTTP bearer with `bearerFormat` `xt_live_.`. | Part | Meaning | |---|---| | `xt_live_` | Fixed prefix for live tokens | | `` | Non-secret identifier. `GET /me` returns it as `token_prefix`, matching `^xt_live_[a-f0-9]{8}$` (8 lowercase hex characters) | | `.` | Separator | | `` | Secret part. Shown only once | Tokens are created in the X-Trader web app under **Settings, Developer API**. The documented setup steps are: 1. Sign in and open the Developer API settings. 2. Select only the scopes the integration needs. 3. Add an IP allowlist and an expiration date whenever possible. 4. Copy the token into a server-side secret store immediately; X-Trader cannot show it again. 5. Call `GET /me` to verify it. The spec advises keeping tokens in trusted server-side code only: never in browser JavaScript, mobile bundles, URLs, logs, Git repositories or third-party proxies. ## Scopes A token carries up to 9 scopes. Each operation declares a required scope. | Scope | Allows | |---|---| | `sniper:read` | List and get sniper configs and sniper templates; list and get labels | | `sniper:write` | Create, update, pause and resume sniper configs; create and update sniper templates; create, update and delete labels | | `sniper:delete` | Delete sniper configs and sniper templates | | `copytrade:read` | List and get copy-trade configs and copy-trade templates; list and get labels | | `copytrade:write` | Create, update, pause and resume copy-trade configs; create and update copy-trade templates; create, update and delete labels | | `copytrade:delete` | Delete copy-trade configs and copy-trade templates | | `transfersnipe:read` | List and get transfer-snipe tasks | | `transfersnipe:write` | Create, update, pause and resume transfer-snipe tasks | | `transfersnipe:delete` | Delete transfer-snipe tasks | Notes derived from the per-operation requirements: - Label endpoints accept either the sniper or the copy-trade scope (`sniper:read` or `copytrade:read` for reads, `sniper:write` or `copytrade:write` for writes, including `DELETE /labels/{id}`). The spec does not list any `transfersnipe:*` scope for label endpoints, so a token with only transfer-snipe scopes cannot manage labels. - Deleting a label needs a `:write` scope, not a `:delete` scope. - `GET /me` declares no specific scope; any valid token can call it. - A call without the required scope fails with `insufficient_scope` (see [errors](/xtrader/errors/)). ## GET /me `GET /me` returns metadata about the credential used for the request. It returns no internal user identifier. It is the recommended first call after creating or rotating a token. | Field | Type | Description | |---|---|---| | `token_prefix` | string, `^xt_live_[a-f0-9]{8}$` | Non-secret identifier of this credential | | `token_version` | integer, greater than 0 | Credential version; rotation increments it | | `scopes` | array of scope strings (max 9) | Exact permissions granted to this token | | `expires_at` | date-time or `null` | Expiry timestamp; `null` means the token does not expire | | `quota.max` | integer | Maximum number of configurations for the account | | `quota.used` | integer | Current number of sniper, copy-trade and transfer-snipe configurations combined | | `quota.remaining` | integer | Configurations that can still be created | | `login_username` | string or `null` (default `null`) | Login name of the account; the spec gives no further description | | `family_role` | enum `none`, `primary`, `member` (default `none`) | Account family role; the spec gives no further description | The quota is shared: sniper, copy-trade and transfer-snipe configurations all count against the same `max`. Templates and labels have their own separate limits (1,000 templates shared by sniper and copy-trade, 20 labels). ```bash export X_TRADER_API_TOKEN='xt_live_xxxxxxxx.your_secret' curl 'https://x-trader.ai/api/v1/me' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" ``` ```python import os import requests BASE = "https://x-trader.ai/api/v1" headers = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} r = requests.get(f"{BASE}/me", headers=headers, timeout=10) r.raise_for_status() me = r.json()["data"] print(me["token_prefix"], me["scopes"]) print("quota remaining:", me["quota"]["remaining"], "of", me["quota"]["max"]) ``` ## IP allowlist and expiry A token can be restricted to an IP allowlist and given an expiration date when it is created. The spec recommends both. A request from an address outside the allowlist is rejected with `ip_not_allowed`. The expiry is visible in `expires_at`. The spec does not document the allowlist format, whether these settings can be edited after creation, or which error code an expired token returns. As practical guidance, treat `invalid_api_token` as the likely result for a token that is no longer accepted and check `expires_at` proactively. ## Rotation and revocation - Rotate or revoke the token immediately if exposure is suspected. - Rotation invalidates the previous token at once; there is no grace period in the documented behaviour. - After rotation, `token_version` is incremented. Call `GET /me` with the new token to confirm it. Rotation and revocation are performed in the web app; the API has no endpoint for them. ## Identity and ownership isolation - No request body, query parameter or path accepts `user_id`; sending it in a body is rejected. - Every list returns only the token owner's resources. - A config, template or label id that belongs to another account returns `404 not_found`, exactly like a missing id. This also applies to a foreign `risk_label_id` in a request body and a foreign `label_id` list filter. ## Endpoint map | Method and path | Scope | Purpose | Page | |---|---|---|---| | `GET /me` | any | Credential metadata and shared quota | this page | | `GET /sniper/configs` | `sniper:read` | List sniper configs | [sniper](/xtrader/sniper/) | | `POST /sniper/configs` | `sniper:write` | Create a paused sniper config | [sniper](/xtrader/sniper/) | | `GET /sniper/configs/{id}` | `sniper:read` | Get one sniper config | [sniper](/xtrader/sniper/) | | `PATCH /sniper/configs/{id}` | `sniper:write` | Update a sniper config | [sniper](/xtrader/sniper/) | | `DELETE /sniper/configs/{id}` | `sniper:delete` | Delete a paused sniper config | [sniper](/xtrader/sniper/) | | `POST /sniper/configs/{id}/pause` | `sniper:write` | Pause | [sniper](/xtrader/sniper/) | | `POST /sniper/configs/{id}/resume` | `sniper:write` | Revalidate and resume | [sniper](/xtrader/sniper/) | | `GET, POST /copy-trade/configs` | `copytrade:read`, `copytrade:write` | List, create | [copy-trade](/xtrader/copy-trade/) | | `GET, PATCH, DELETE /copy-trade/configs/{id}` | `copytrade:read`, `:write`, `:delete` | Get, update, delete | [copy-trade](/xtrader/copy-trade/) | | `POST /copy-trade/configs/{id}/pause`, `/resume` | `copytrade:write` | Pause, resume | [copy-trade](/xtrader/copy-trade/) | | `GET, POST /transfer-snipe/configs` | `transfersnipe:read`, `transfersnipe:write` | List, create | [transfer-snipe](/xtrader/transfer-snipe/) | | `GET, PATCH, DELETE /transfer-snipe/configs/{id}` | `transfersnipe:read`, `:write`, `:delete` | Get, update, delete | [transfer-snipe](/xtrader/transfer-snipe/) | | `POST /transfer-snipe/configs/{id}/pause`, `/resume` | `transfersnipe:write` | Pause, resume | [transfer-snipe](/xtrader/transfer-snipe/) | | `GET, POST /sniper/templates` | `sniper:read`, `sniper:write` | List, create | [templates](/xtrader/templates/) | | `GET, PATCH, DELETE /sniper/templates/{id}` | `sniper:read`, `:write`, `:delete` | Get, update, delete | [templates](/xtrader/templates/) | | `GET, POST /copy-trade/templates` | `copytrade:read`, `copytrade:write` | List, create | [templates](/xtrader/templates/) | | `GET, PATCH, DELETE /copy-trade/templates/{id}` | `copytrade:read`, `:write`, `:delete` | Get, update, delete | [templates](/xtrader/templates/) | | `GET, POST /labels` | `sniper:*` or `copytrade:*` read / write | List, create | [labels](/xtrader/labels/) | | `GET, PATCH, DELETE /labels/{id}` | read / write | Get, update, delete | [labels](/xtrader/labels/) | Related reference pages: [write rules](/xtrader/write-rules/), [errors](/xtrader/errors/), [pagination](/xtrader/pagination/), and the config field references [sniper config](/xtrader/config-sniper/), [copy-trade config](/xtrader/config-copy-trade/), [auto-sell](/xtrader/auto-sell/), [auto strategy](/xtrader/auto-strategy/), [timed strategy](/xtrader/timed-strategy/). ## FAQ ### What can the X-Trader Public API change? Only the caller's own sniper, copy-trade and transfer-snipe configurations, sniper and copy-trade templates, and shared labels. Wallet keys, trade exits, fund transfers and administrative functions are not exposed. ### What does an X-Trader API token look like? A personal token has the form `xt_live_.` and is sent as `Authorization: Bearer `. The full token is shown once at creation and cannot be recovered. ### Which call should I make first? `GET /me`. It confirms the token works and returns its prefix, version, scopes, expiry and the shared configuration quota. ### Why does a config id from another account return 404? The user is derived only from the token. An id owned by someone else returns the same `404 not_found` as an id that does not exist, so existence is never disclosed. ### What happens to the old token when I rotate it? Rotation invalidates the previous token immediately and increments `token_version`. Update every client before or right after rotating. --- # X-Trader API write rules: Idempotency-Key, If-Match, ETag, decimal strings URL: https://docs.blizz.id/xtrader/write-rules/ Updated: 2026-10-16 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](https://x-trader.ai/api/docs). 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 | 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 - 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). ## 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 | ```json { "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](/xtrader/config-sniper/) and [copy-trade config](/xtrader/config-copy-trade/). ## 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. ```bash 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) Configs and templates carry an integer `version`. Single-resource responses return it as the `ETag` header in the form `"v"`, for example `"v3"`. Writes to an existing config or template must send that value back: | Property | Value | |---|---| | Header | `If-Match` | | Format | `"v"` 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`. ```python 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"] ``` ## 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 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. ```bash 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 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 - 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](/xtrader/labels/). ## Templates - 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](/xtrader/templates/). ## 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. ## FAQ ### 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? 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? 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? 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? No. Labels do not use `ETag` or `If-Match`. Label creation still requires an `Idempotency-Key`. --- # X-Trader API error codes explained URL: https://docs.blizz.id/xtrader/errors/ Updated: 2026-10-16 Every X-Trader Public API error returns a JSON body with a stable machine-readable `error.code` (17 possible values), a human-readable `error.message`, optional field-level `error.fields[]`, and a `request_id`. Clients should branch on `error.code`, honour `Retry-After` on `429` and `503`, and keep `X-Request-Id` or `request_id` for support reports. The spec declares the statuses `400`, `401`, `403`, `404`, `409`, `429`, `500` and `503` for every operation. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Error body ```json { "error": { "code": "validation_error", "message": "Request validation failed", "fields": [ { "path": "config.buy_amount", "reason": "invalid_string" } ] }, "request_id": "req_0123456789abcdef" } ``` The values above are illustrative; the spec defines the shape, not concrete messages or reasons. | Field | Type | Required | Meaning | |---|---|---|---| | `error.code` | enum string | yes | Stable code; branch on this | | `error.message` | string | yes | English summary for humans; do not parse | | `error.fields` | array, max 20 | no | Field-level validation failures | | `error.fields[].path` | string | yes (per item) | Dot-separated JSON path; `$` is the whole request body | | `error.fields[].reason` | string | yes (per item) | Stable reason produced by the request schema | | `request_id` | string | yes | Request identifier for support | The error object is strict and never contains database or internal implementation details. ## Headers on errors | Header | When | Use | |---|---|---| | `Retry-After` | `429` and `503` responses | Minimum wait before retrying | | `X-Request-Id` | Every response (or `request_id` in the body) | Quote it when reporting a problem | ## Status mapping The spec uses one shared `ErrorResponse` schema for all error statuses and does not publish a per-code status table or response examples. The status column below is therefore documented only where the spec ties a code to a status (`404 not_found`, and the `Retry-After` statuses `429` and `503`). The remaining assignments are inferred from the declared status set and standard HTTP semantics, and are marked "inferred". Handle errors by `error.code`, not by status. ## All error codes | `error.code` | HTTP status | What triggers it | What to do | |---|---|---|---| | `invalid_request` | 400 (inferred) | Malformed request that is not a schema field error, for example unparseable JSON, a body over 64 KiB, an invalid query or header value. The spec does not enumerate triggers. | Fix the request; do not retry unchanged. | | `invalid_api_token` | 401 (inferred) | The bearer token is missing, malformed or not accepted. A token replaced by rotation or revoked is presumably reported this way; the spec does not say so explicitly. | Check the `Authorization: Bearer xt_live_.` header; rotate or create a token if needed. | | `insufficient_scope` | 403 (inferred) | The token lacks the operation's required scope, for example `sniper:delete` for `DELETE /sniper/configs/{id}`. | Call `GET /me` to see `scopes`; issue a token with the needed scope. | | `ip_not_allowed` | 403 (inferred) | The request comes from an address outside the token's IP allowlist. | Call from an allowlisted address or update the allowlist in the web app. | | `rate_limit_exceeded` | 429 (inferred from `Retry-After`) | Too many requests. The spec does not publish the limits. | Wait for `Retry-After`, then retry with backoff. | | `validation_error` | 400 (inferred) | Body fails the strict schema: unknown property, wrong type, JSON number instead of decimal string, more than 12 fractional digits, value out of range, forbidden field such as `user_id`, `version`, `template` or (on templates) `risk_label_id`, empty `PATCH`. A failed revalidation on resume is probably reported the same way; the spec does not name that code. | Read `error.fields[]` for `path` and `reason`, fix the body. | | `not_found` | 404 | Resource does not exist **or** belongs to another account; also a foreign or missing `risk_label_id` in a body or `label_id` filter. | Verify the id, the product path and that the token belongs to the owning account. | | `target_already_exists` | 409 (inferred) | The spec does not describe the trigger. The name suggests a config for the same `target_wallet` already exists in that product for this account. | List configs with `q=` and update the existing one instead of creating another. | | `template_name_exists` | 409 (inferred) | The spec does not describe the trigger. The name suggests a template name collision when creating or renaming a template. | Choose a different `name`. | | `label_name_exists` | 409 (inferred) | Creating or renaming a label to a name already used in the account (names are unique per account). | Choose a different `name` or reuse the existing label. | | `quota_exceeded` | 409 (inferred) | A limit is reached: the shared config quota (`quota.max` in `GET /me`), the 1,000-template limit, or the 21st label. | Delete unused resources, or check `GET /me` before creating. | | `config_must_be_paused` | 409 (inferred) | Changing `config` or `linked_wallets`, or deleting, while the config is active. | `POST /{id}/pause`, perform the change, then resume. | | `version_conflict` | 409 (inferred) | `If-Match` does not match the current `version`. | GET the resource, re-evaluate, resend with the new `ETag`. | | `idempotency_required` | 400 (inferred) | A create request without an `Idempotency-Key` header. | Add a key of 8-128 characters from `A-Za-z0-9._:-`. | | `idempotency_conflict` | 409 (inferred) | The same `Idempotency-Key` was reused within 24 hours with a different body. | Resend the original body, or use a new key for a new resource. | | `service_unavailable` | 503 (inferred from `Retry-After`) | The service is temporarily unable to handle the request. | Wait for `Retry-After`; retry only safe operations. | | `internal_error` | 500 (inferred) | Unexpected server-side failure. | Retry cautiously if the operation is safe to repeat; report with `request_id`. | The `400`/`409` split for the last group is the least certain part of the table: the spec lists `409` for every operation, which fits the conflict-type codes, but it does not confirm any individual assignment. ## Retry policy | Situation | Retry? | |---|---| | `rate_limit_exceeded`, `service_unavailable` | Yes, after `Retry-After`, with backoff | | `internal_error` | Only if repeating is safe | | Create with the same `Idempotency-Key` and body | Safe to retry within 24 hours | | `PATCH`, pause, resume, delete | Safe to resend with the same `If-Match`: if the first attempt succeeded, the retry fails with `version_conflict` instead of applying twice | | `validation_error`, `invalid_request`, `not_found`, `insufficient_scope`, `ip_not_allowed`, `*_exists`, `quota_exceeded` | No, fix the cause first | Practical guidance: a `version_conflict` on a retried write may mean your own previous attempt succeeded. GET the resource to check before doing anything else. ## Example: handling errors ```bash curl --include --request DELETE "https://x-trader.ai/api/v1/sniper/configs/$CONFIG_ID" \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'If-Match: "v4"' ``` ```python import os import time import requests BASE = "https://x-trader.ai/api/v1" H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} def call(method, path, retries=3, headers=None, **kw): hdrs = {**H, **(headers or {})} for attempt in range(retries + 1): r = requests.request(method, f"{BASE}{path}", headers=hdrs, timeout=15, **kw) if r.ok: return r err = r.json().get("error", {}) code = err.get("code") if code in ("rate_limit_exceeded", "service_unavailable") and attempt < retries: time.sleep(float(r.headers.get("Retry-After", 2 ** attempt))) continue rid = r.headers.get("X-Request-Id") or r.json().get("request_id") for f in err.get("fields", []): print("field", f["path"], f["reason"]) raise RuntimeError(f"{r.status_code} {code}: {err.get('message')} (request {rid})") me = call("GET", "/me").json()["data"] ``` ## FAQ ### Why do I get 404 not_found for my own config id? Check that the token belongs to the same X-Trader account that owns the config and that the id is sent to the right product path (a copy-trade id under `/sniper/configs/` is not found). Ids owned by another account, and foreign label ids in `risk_label_id` or `label_id`, return the same `404 not_found` as missing ids, so the API never reveals whether they exist. ### What does version_conflict after PATCH mean? The `If-Match` value is older than the stored `version` because the resource changed since you read it, possibly by your own earlier retry or by the web UI. GET it again, review the current state, then resend with the new `ETag` if the change still applies. ### What is config_must_be_paused? The request needs a paused config: changing `config` or `linked_wallets`, or deleting. Pause it with `POST /{id}/pause`, make the change, then resume if needed. `remark` and `risk_label_id` can be changed while active. ### How do I handle rate_limit_exceeded? Wait for the number of seconds in the `Retry-After` header before retrying, and add backoff. Retry writes only when they are safe to repeat (same `Idempotency-Key`, or the same `If-Match`). ### Should I parse the error message text? No. Branch on `error.code`, which is stable. The `message` is a human-readable English summary and may change. --- # X-Trader API cursor pagination URL: https://docs.blizz.id/xtrader/pagination/ Updated: 2026-10-16 X-Trader Public API list endpoints use opaque cursor pagination: each response returns `data.items` and `data.next_cursor`, and the next page is requested by passing `next_cursor` unchanged as the `cursor` query parameter. Pages contain 20 items by default and at most 100 (`limit`). A `null` `next_cursor` marks the last page, and a cursor is valid only for the same token and the same filters. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Paginated endpoints | Endpoint | Scope | Order | |---|---|---| | `GET /sniper/configs` | `sniper:read` | `updated_at`, then `id` | | `GET /copy-trade/configs` | `copytrade:read` | `updated_at`, then `id` | | `GET /transfer-snipe/configs` | `transfersnipe:read` | `updated_at`, then `id` | | `GET /sniper/templates` | `sniper:read` | not stated in the spec | | `GET /copy-trade/templates` | `copytrade:read` | not stated in the spec | `GET /labels` is not paginated (see below). The spec states that config lists are ordered by `updated_at` and `id` for stable paging but does not state the direction (ascending or descending). ## Query parameters ### Config lists (sniper, copy-trade, transfer-snipe) | Parameter | Type | Default | Constraints | Meaning | |---|---|---|---|---| | `limit` | integer | 20 | 1-100 | Items per page | | `cursor` | string | none | max 2048 characters | Opaque signed cursor from the previous page | | `status` | enum | none | `active`, `paused` | Filter by execution state (`is_active`) | | `q` | string | none | max 64 characters | Search in `remark` and `target_wallet`, own resources only | | `label_id` | string | none | label UUID or literal `unlabeled` | Only tasks with this label, or with no label | A `label_id` UUID that is missing or owned by another account returns `404 not_found`. ### Template lists (sniper, copy-trade) | Parameter | Type | Default | Constraints | Meaning | |---|---|---|---|---| | `limit` | integer | 20 | 1-100 | Items per page | | `cursor` | string | none | max 2048 characters | Opaque signed cursor, bound to the token and the search query | | `q` | string | none | max 64 characters | Search template names only | Template lists have no `status` or `label_id` filter (templates never carry a label). ## Response shape ```json { "data": { "items": [ { "id": "...", "version": 3, "is_active": false } ], "next_cursor": "opaque-string-or-null" }, "meta": { "request_id": "req_0123456789abcdef" } } ``` | Field | Type | Meaning | |---|---|---| | `data.items` | array | Resources of the page, in the same shape as single `GET` results | | `data.next_cursor` | string or `null` (required) | Cursor for the next page; `null` when there are no more pages | | `meta.request_id` | string | Request identifier | Items are returned as stored snapshots and may contain legacy fields (see [write rules](/xtrader/write-rules/)). List responses do not carry per-item `ETag` headers; use each item's `version` or a single `GET` before writing. ## Cursor rules - Treat the cursor as opaque: pass it unchanged, never parse, decode or edit it. - It is bound to the token that produced it. A cursor from one token is not usable with another (including a rotated token, as practical guidance). - It is bound to the filters (`status`, `q`, `label_id` on config lists; `q` on template lists). After changing any filter, restart from the first page without `cursor`. - Keep `limit` and filters identical across pages of one traversal. - The spec does not document cursor expiry or the error code for an invalid cursor. As practical guidance, on a `400` error for a cursor request restart from the first page. ## Examples First page of paused sniper configs, 50 per page: ```bash curl 'https://x-trader.ai/api/v1/sniper/configs?limit=50&status=paused' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" ``` Next page (the cursor must be URL-encoded): ```bash curl --get 'https://x-trader.ai/api/v1/sniper/configs' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --data-urlencode 'limit=50' \ --data-urlencode 'status=paused' \ --data-urlencode "cursor=$NEXT_CURSOR" ``` Python loop fetching every page: ```python import os import requests BASE = "https://x-trader.ai/api/v1" H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} def fetch_all(path, **filters): params = {"limit": 100, **filters} items = [] while True: r = requests.get(f"{BASE}{path}", headers=H, params=params, timeout=15) r.raise_for_status() data = r.json()["data"] items.extend(data["items"]) cursor = data["next_cursor"] if cursor is None: return items params = {**params, "cursor": cursor} # same limit and filters, new cursor active_copy = fetch_all("/copy-trade/configs", status="active") unlabeled_snipers = fetch_all("/sniper/configs", label_id="unlabeled") templates = fetch_all("/sniper/templates", q="night") print(len(active_copy), len(unlabeled_snipers), len(templates)) ``` `requests` URL-encodes `params`, so the cursor is passed through unchanged. ## Labels list `GET /labels` takes no query parameters and returns every label of the account in `data.items`, with no `next_cursor`. There are at most 20 labels, ordered by `sort_order`, then `created_at`. See [labels](/xtrader/labels/). ## FAQ ### How many items does an X-Trader list return? 20 by default. Set `limit` to any integer from 1 to 100. ### How do I know I reached the last page? `data.next_cursor` is `null`. Otherwise pass it unchanged as the `cursor` query parameter on the next request. ### Can I change filters while paging? No. Cursors are bound to the token and the filters of the first request. After changing `status`, `q` or `label_id`, restart from the first page without a cursor. ### Is the labels list paginated? No. `GET /labels` takes no parameters and returns all labels (at most 20) in `data.items` without `next_cursor`. --- # X-Trader sniper configs API URL: https://docs.blizz.id/xtrader/sniper/ Updated: 2026-10-16 The sniper configs API is the set of seven operations under `/sniper/configs` that manage the caller's own sniper tasks: list, create, get, patch, delete, pause and resume. A configuration is always created paused (`is_active=false`) and starts executing only after `POST /sniper/configs/{id}/resume`. Reads need `sniper:read`, writes need `sniper:write`, and delete needs `sniper:delete`. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Operations | Method and path | Purpose | Scope | Required headers | Success | |---|---|---|---|---| | `GET /sniper/configs` | List owned configurations | `sniper:read` | `Authorization` | `200` list envelope | | `POST /sniper/configs` | Create a paused configuration | `sniper:write` | `Authorization`, `Idempotency-Key` | `201` resource envelope | | `GET /sniper/configs/{id}` | Read one configuration | `sniper:read` | `Authorization` | `200` resource envelope | | `PATCH /sniper/configs/{id}` | Update mutable fields | `sniper:write` | `Authorization`, `If-Match` | `200` resource envelope | | `DELETE /sniper/configs/{id}` | Delete a paused configuration | `sniper:delete` | `Authorization`, `If-Match` | `204` no body | | `POST /sniper/configs/{id}/pause` | Stop execution | `sniper:write` | `Authorization`, `If-Match` | `200` resource envelope | | `POST /sniper/configs/{id}/resume` | Revalidate and start execution | `sniper:write` | `Authorization`, `If-Match` | `200` resource envelope | Base URL: `https://x-trader.ai/api/v1`. Every operation also lists `400`, `401`, `403`, `404`, `409`, `429`, `500` and `503` as possible error statuses. Bulk delete does not exist. ## Headers | Header | Where | Rule | |---|---|---| | `Authorization` | All requests | `Bearer xt_live_.`. The user is derived from the token only. | | `Idempotency-Key` | `POST /sniper/configs` | Required. 8-128 characters from `A-Z a-z 0-9 . _ : -`. Kept for 24 hours; the same key with a different body is rejected (`idempotency_conflict`). | | `If-Match` | PATCH, DELETE, pause, resume | Required. The latest `ETag` of the resource, matching `"v"`, for example `"v3"`. A stale value returns `version_conflict`. | | `ETag` (response) | 201 and 200 single-resource responses | Current version. Send it as `If-Match` on the next write. | | `X-Request-Id` (response) | Single-resource and delete responses | Request identifier for support. | General write rules (64 KiB body limit, decimal strings, strict objects) are covered on [/xtrader/write-rules/](/xtrader/write-rules/). ## List: GET /sniper/configs Returns only configurations owned by the token's user, ordered by `updated_at` and `id` so that cursor pagination is stable. | Query parameter | Type | Rule | |---|---|---| | `limit` | integer | 1-100, default 20 | | `cursor` | string | Max 2048 characters. Opaque `next_cursor` from the previous page, bound to the token and filters. | | `status` | string | `active` or `paused` | | `q` | string | Max 64 characters. Searches `remark` and `target_wallet` within the caller's data. | | `label_id` | string | A label UUID owned by the account, or the literal `unlabeled`. A foreign or missing UUID returns `404`. | The response is `{ "data": { "items": [SniperResource...], "next_cursor": string or null }, "meta": { "request_id": ... } }`. Restart from the first page after changing a filter. Details: [/xtrader/pagination/](/xtrader/pagination/). ## Create: POST /sniper/configs Request body (`SniperCreate`, strict, unknown fields rejected; `user_id`, `version` and `template` are explicitly rejected): | Field | Type | Required | Rule | |---|---|---|---| | `remark` | string | yes (default `""`) | Max 64 characters, trimmed. | | `target_wallet` | string | yes | Solana address, 32-44 characters. Immutable after creation. | | `risk_label_id` | string (UUID) or null | no | Label owned by the same account. See [/xtrader/labels/](/xtrader/labels/). | | `config` | object | yes | Complete sniper strategy. Field reference: [/xtrader/config-sniper/](/xtrader/config-sniper/). | Required keys inside `config`: `buy_amount`, `jito_mode`, `priority_mode`, `selected_dexes`, `dex_source`, `buy_once`, `sol_slippage_mode`, `snipe_external`, `auto_sell`, `auto_strategy`, `timed_strategy`. The sell-side objects are documented on [/xtrader/auto-sell/](/xtrader/auto-sell/), [/xtrader/auto-strategy/](/xtrader/auto-strategy/) and [/xtrader/timed-strategy/](/xtrader/timed-strategy/). The spec describes `target_wallet` only as a valid Solana address; it does not define its role in sniper execution beyond that. A successful create returns `201` with `is_active: false`, an `ETag` (initially `"v1"` in the spec's header example) and the stored resource. The new configuration counts toward the shared configuration quota reported by `GET /me` (`quota.max`, `quota.used`, `quota.remaining`), which sniper, copy-trade and transfer-snipe tasks share. ### Example: create ```bash 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 '{ "remark": "Primary Pump sniper", "target_wallet": "11111111111111111111111111111111", "config": { "buy_amount": "0.1", "jito_mode": {"tip_amount": "0.01", "priority_fee": "0.0005", "slippage": "15", "bundle_amount": ""}, "priority_mode": {"tip_amount": "0.001", "priority_fee": "0.005", "slippage": "15", "bundle_amount": ""}, "selected_dexes": ["pump-fun"], "dex_source": "global", "dev_min_amount": "", "dev_max_amount": "", "buy_once": false, "sol_slippage_mode": false, "snipe_external": false, "auto_sell": { "enabled": false, "tab": "tpsl", "expiry_time": "10", "tp_orders": [{"trigger": "90", "sell": "100"}], "sl_orders": [], "follow_sell": false, "proportional_sell": false, "trailing_take_profit": {"enabled": false, "tiers": []} }, "auto_strategy": {"migration_clear": false, "timed_sell": false, "sell_delay": "30"}, "timed_strategy": {"enabled": false, "orders": []} } }' ``` Response `201 Created` (headers `ETag: "v1"`, `X-Request-Id`). IDs, timestamps and request ID are illustrative: ```json { "data": { "id": "3f2b8c1e-5d4a-4b7e-9c2f-1a6d8e0b4c37", "remark": "Primary Pump sniper", "target_wallet": "11111111111111111111111111111111", "is_active": false, "risk_label_id": null, "version": 1, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:00:00Z", "config": { "buy_amount": "0.1", "...": "stored config as sent" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` `meta.persistence` and `meta.propagation` are optional in the schema; when present, `committed` means the database write finished and `eventual` means execution nodes may need a short time to see it. ## Get: GET /sniper/configs/{id} `id` is a UUID. A missing ID and an ID owned by another user both return `404 not_found`. The `200` response carries the `ETag` needed for the next write. List and get return the stored snapshot as it is, so older configurations may contain legacy web-UI fields that the current create schema would not accept. ## Patch: PATCH /sniper/configs/{id} Body (`SniperPatch`): any non-empty subset of `remark`, `risk_label_id`, `config`. Rules: - At least one top-level field must be present. - `target_wallet` cannot be changed. - On an active configuration only `remark` and `risk_label_id` may change. Changing `config` requires the configuration to be paused, otherwise the request fails (the error enum provides `config_must_be_paused` for this case). - `config`, when present, replaces the whole stored config and must satisfy the full schema. Send the complete object, not a diff. - `risk_label_id: null` removes the label. The operation description also says to pause before changing `linked_wallets`, but `SniperPatch` has no `linked_wallets` property; that field exists only on copy-trade. ## Pause and resume `POST /sniper/configs/{id}/pause` sets the stored state to paused immediately; propagation to execution nodes is eventual. `POST /sniper/configs/{id}/resume` revalidates the complete stored config against the current schema before activating it. A stored config that no longer validates (for example legacy data) is not activated, and the request fails with a validation error. Neither call takes a body; both require `If-Match` and return the updated resource with a new `ETag`. ### Example: resume ```bash curl --request POST 'https://x-trader.ai/api/v1/sniper/configs/3f2b8c1e-5d4a-4b7e-9c2f-1a6d8e0b4c37/resume' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'If-Match: "v1"' ``` Response `200 OK` (header `ETag` with the new version; values illustrative): ```json { "data": { "id": "3f2b8c1e-5d4a-4b7e-9c2f-1a6d8e0b4c37", "remark": "Primary Pump sniper", "target_wallet": "11111111111111111111111111111111", "is_active": true, "risk_label_id": null, "version": 2, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:01:10Z", "config": { "buy_amount": "0.1", "...": "unchanged stored config" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` Read the version from the `ETag` header rather than computing it; the spec only states that `version` is a positive concurrency version. ### Python: create then resume ```python import os import uuid import requests BASE = "https://x-trader.ai/api/v1" HEADERS = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} body = { "remark": "Primary Pump sniper", "target_wallet": "11111111111111111111111111111111", "config": { "buy_amount": "0.1", "jito_mode": {"tip_amount": "0.01", "priority_fee": "0.0005", "slippage": "15", "bundle_amount": ""}, "priority_mode": {"tip_amount": "0.001", "priority_fee": "0.005", "slippage": "15", "bundle_amount": ""}, "selected_dexes": ["pump-fun"], "dex_source": "global", "dev_min_amount": "", "dev_max_amount": "", "buy_once": False, "sol_slippage_mode": False, "snipe_external": False, "auto_sell": { "enabled": False, "tab": "tpsl", "expiry_time": "10", "tp_orders": [{"trigger": "90", "sell": "100"}], "sl_orders": [], "follow_sell": False, "proportional_sell": False, "trailing_take_profit": {"enabled": False, "tiers": []}, }, "auto_strategy": {"migration_clear": False, "timed_sell": False, "sell_delay": "30"}, "timed_strategy": {"enabled": False, "orders": []}, }, } r = requests.post(f"{BASE}/sniper/configs", json=body, headers={**HEADERS, "Idempotency-Key": f"sniper-{uuid.uuid4()}"}) r.raise_for_status() config_id = r.json()["data"]["id"] etag = r.headers["ETag"] r = requests.post(f"{BASE}/sniper/configs/{config_id}/resume", headers={**HEADERS, "If-Match": etag}) r.raise_for_status() print(r.json()["data"]["is_active"], r.headers["ETag"]) ``` Store the generated idempotency key with the request if you may need to retry it; a retry with the same key and the same body is safe. ## Delete: DELETE /sniper/configs/{id} Requires `sniper:delete` and `If-Match`. Only paused configurations can be deleted; an active one is refused. Success is `204` with no body. ## Lifecycle 1. `POST /sniper/configs` creates the task paused and returns `ETag`. 2. Inspect the returned resource, then `POST .../resume` with that `ETag`. 3. To change strategy: `POST .../pause`, then `PATCH` with the full `config` and the latest `ETag`, then `POST .../resume`. 4. `remark` and `risk_label_id` can be patched without pausing, but still need the latest `If-Match`. 5. To remove: pause, then `DELETE`. Every write returns a new `ETag`; always use the one from the latest response. ## Response envelope and errors Success bodies have `data` (one `SniperResource`, or `items` plus `next_cursor` for lists) and `meta.request_id`. `SniperResource` fields: `id`, `remark`, `target_wallet`, `is_active`, `risk_label_id`, `version`, `created_at`, `updated_at`, `config`. Errors use `{ "error": { "code", "message", "fields"? }, "request_id" }`, where `fields` lists up to 20 `{ path, reason }` validation failures. Branch on `error.code`. Codes most relevant to this resource: | Code | Typical cause on /sniper/configs | |---|---| | `validation_error` | Unknown property, JSON number instead of decimal string, out-of-range value, empty PATCH, failed revalidation on resume | | `insufficient_scope` | Token lacks `sniper:read`, `sniper:write` or `sniper:delete` | | `not_found` | Unknown ID, another user's ID, or a foreign `risk_label_id` / `label_id` | | `config_must_be_paused` | `config` PATCH or delete on an active configuration | | `version_conflict` | `If-Match` is not the latest version; GET again before retrying | | `idempotency_required` / `idempotency_conflict` | Missing `Idempotency-Key`, or key reused with a different body | | `quota_exceeded` | Shared configuration quota reached | | `rate_limit_exceeded`, `service_unavailable` | `429` / `503`, with `Retry-After` | The spec lists the error codes and the HTTP statuses per operation but does not publish a code-to-status table. See [/xtrader/errors/](/xtrader/errors/). ## FAQ ### Why is my new sniper config not running after create? Every create returns a configuration with `is_active=false`. Call `POST /sniper/configs/{id}/resume` with the `ETag` from the create response as `If-Match` to enable it. ### Can I change the config of an active sniper? No. While a sniper is active only `remark` and `risk_label_id` can be patched. Pause it first, then PATCH `config`, then resume. ### Can I change target_wallet after creation? No. `target_wallet` is immutable. Create a new configuration for a different address and delete the old one after pausing it. ### Which scopes does the sniper API need? `sniper:read` for list and get, `sniper:write` for create, patch, pause and resume, and `sniper:delete` for delete. ### What does a PATCH with config do to fields I leave out? The `config` object in a PATCH replaces the stored config completely and must pass the full schema, so omitted nested fields are not merged from the old value. --- # X-Trader copy-trade configs API URL: https://docs.blizz.id/xtrader/copy-trade/ Updated: 2026-10-16 The copy-trade configs API is the set of seven operations under `/copy-trade/configs` that manage the caller's own copy-trade tasks: list, create, get, patch, delete, pause and resume. Each configuration follows one `target_wallet`, can carry up to 20 `linked_wallets`, and is always created paused (`is_active=false`) until `POST /copy-trade/configs/{id}/resume`. Reads need `copytrade:read`, writes `copytrade:write`, delete `copytrade:delete`. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Operations | Method and path | Purpose | Scope | Required headers | Success | |---|---|---|---|---| | `GET /copy-trade/configs` | List owned configurations | `copytrade:read` | `Authorization` | `200` list envelope | | `POST /copy-trade/configs` | Create a paused configuration | `copytrade:write` | `Authorization`, `Idempotency-Key` | `201` resource envelope | | `GET /copy-trade/configs/{id}` | Read one configuration | `copytrade:read` | `Authorization` | `200` resource envelope | | `PATCH /copy-trade/configs/{id}` | Update mutable fields | `copytrade:write` | `Authorization`, `If-Match` | `200` resource envelope | | `DELETE /copy-trade/configs/{id}` | Delete a paused configuration | `copytrade:delete` | `Authorization`, `If-Match` | `204` no body | | `POST /copy-trade/configs/{id}/pause` | Stop execution | `copytrade:write` | `Authorization`, `If-Match` | `200` resource envelope | | `POST /copy-trade/configs/{id}/resume` | Revalidate and start execution | `copytrade:write` | `Authorization`, `If-Match` | `200` resource envelope | Base URL: `https://x-trader.ai/api/v1`. Every operation also lists `400`, `401`, `403`, `404`, `409`, `429`, `500` and `503`. Bulk delete is not available. Note the scope spelling: `copytrade:*` without a hyphen, while the path uses `copy-trade`. ## Headers | Header | Where | Rule | |---|---|---| | `Authorization` | All requests | `Bearer xt_live_.` | | `Idempotency-Key` | `POST /copy-trade/configs` | Required. 8-128 characters from `A-Z a-z 0-9 . _ : -`, kept 24 hours. Same key with a different body: `idempotency_conflict`. | | `If-Match` | PATCH, DELETE, pause, resume | Required. Latest `ETag`, format `"v"`. Stale value: `version_conflict`. | | `ETag` (response) | 201 and 200 single-resource responses | Version to send on the next write | | `X-Request-Id` (response) | Single-resource and delete responses | Support correlation ID | See [/xtrader/write-rules/](/xtrader/write-rules/) for body size, decimal-string and strict-object rules. ## List: GET /copy-trade/configs Only the token owner's configurations, ordered by `updated_at` and `id`. | Query parameter | Type | Rule | |---|---|---| | `limit` | integer | 1-100, default 20 | | `cursor` | string | Max 2048 characters; pass `next_cursor` unchanged. Bound to token and filters. | | `status` | string | `active` or `paused` | | `q` | string | Max 64 characters; searches `remark` and `target_wallet` | | `label_id` | string | Owned label UUID or `unlabeled`; foreign or missing UUID returns `404` | Response: `data.items` (array of `CopyTradeResource`), `data.next_cursor` (string or null), `meta.request_id`. See [/xtrader/pagination/](/xtrader/pagination/). ## Create: POST /copy-trade/configs Body (`CopyTradeCreate`). Strict object; `user_id`, `version`, `template` and any unknown field are rejected. | Field | Type | Required | Rule | |---|---|---|---| | `remark` | string | yes (default `""`) | Max 64 characters, trimmed | | `target_wallet` | string | yes | Solana address, 32-44 characters. The wallet being copied. Immutable after creation. | | `linked_wallets` | array of string | yes (default `[]`) | Max 20 items, each a unique Solana address of 32-44 characters | | `risk_label_id` | string (UUID) or null | no | Label owned by the account; see [/xtrader/labels/](/xtrader/labels/) | | `config` | object | yes | Complete copy-trade strategy; field reference at [/xtrader/config-copy-trade/](/xtrader/config-copy-trade/) | Required keys in `config`: `buy_config_mode`, `buy_config_universal`, `buy_config_tiers`, `selected_dexes`, `dex_source`, `buy_token_once`, `first_buy_only`, `reentry_after_flat`, `buy_once`, `skip_dev_launch`, `dev_sell_only`, `dev_sell_all_only`, `address_blacklist`, `min_trade_amount`, `max_trade_amount`, `auto_sell`, `auto_strategy`, `timed_strategy`. Sell-side objects: [/xtrader/auto-sell/](/xtrader/auto-sell/), [/xtrader/auto-strategy/](/xtrader/auto-strategy/), [/xtrader/timed-strategy/](/xtrader/timed-strategy/). ### linked_wallets The spec defines `linked_wallets` as unique Solana addresses linked to the copy-trade target, with a maximum of 20. It does not further describe how execution uses them. Rules that follow from the schema and operation text: - Duplicates inside the array are not allowed (the items must be unique). - On PATCH the array replaces the stored array completely; it is not appended to. - Changing it requires the configuration to be paused. - Do not confuse it with `config.address_blacklist.wallets` (max 50), which is a separate copy-trade filter inside `config`. ### Target wallet uniqueness The error enum contains `target_already_exists`. The spec does not spell out the uniqueness scope; the code name indicates that the `target_wallet` already has a configuration. Practical handling: search with `GET /copy-trade/configs?q=`, then pause and PATCH the existing configuration instead of creating another. The created configuration also counts toward the shared configuration quota shown by `GET /me` (`quota_exceeded` when it is full). ### Example: create ```bash curl --request POST 'https://x-trader.ai/api/v1/copy-trade/configs' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: create-copy-20261016-001' \ --data '{ "remark": "Primary copy-trade target", "target_wallet": "11111111111111111111111111111111", "linked_wallets": [], "config": { "buy_config_mode": "universal", "buy_config_universal": { "market_cap_min": "", "market_cap_max": "", "amount_sol": "0.1", "jito_tip": "0.01", "jito_priority_fee": "0.0005", "jito_slippage": "15", "jito_bundle_amount": "", "priority_fee": "0.005", "priority_tip": "0.001", "priority_slippage": "15", "priority_bundle_amount": "", "sol_slippage_mode": false }, "buy_config_tiers": [], "selected_dexes": ["pump-fun"], "dex_source": "global", "buy_token_once": false, "first_buy_only": false, "reentry_after_flat": false, "buy_once": false, "skip_dev_launch": false, "dev_sell_only": false, "dev_sell_all_only": false, "address_blacklist": {"enabled": false, "wallets": []}, "min_trade_amount": "", "max_trade_amount": "", "auto_sell": { "enabled": false, "tab": "tpsl", "expiry_time": "10", "tp_orders": [{"trigger": "90", "sell": "100"}], "sl_orders": [], "follow_sell": false, "proportional_sell": false, "trailing_take_profit": {"enabled": false, "tiers": []} }, "auto_strategy": {"migration_clear": false, "timed_sell": false, "sell_delay": "30"}, "timed_strategy": {"enabled": false, "orders": []} } }' ``` Response `201 Created` (headers `ETag: "v1"`, `X-Request-Id`; IDs and timestamps illustrative): ```json { "data": { "id": "7a1c2e9d-0b3f-4d6a-8e5c-2f9b1d7a4c60", "remark": "Primary copy-trade target", "target_wallet": "11111111111111111111111111111111", "is_active": false, "risk_label_id": null, "version": 1, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:00:00Z", "linked_wallets": [], "config": { "buy_config_mode": "universal", "...": "stored config as sent" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` ## Get: GET /copy-trade/configs/{id} `id` is a UUID. Missing IDs and IDs owned by other users both return `404 not_found`. The stored snapshot is returned as is and may include legacy web-UI fields; create, update and resume still enforce the current strict schema. ## Patch: PATCH /copy-trade/configs/{id} Body (`CopyTradePatch`): at least one of `remark`, `linked_wallets`, `risk_label_id`, `config`. | Field | Allowed while active | Behaviour | |---|---|---| | `remark` | yes | Replaces the note | | `risk_label_id` | yes | Binds a label; `null` unassigns | | `linked_wallets` | no, pause first | Replaces the whole array (max 20) | | `config` | no, pause first | Replaces the whole config; must pass the full schema | | `target_wallet` | never | Immutable, not part of the patch schema | The error enum provides `config_must_be_paused` for writes that require a paused configuration. ### Python: replace linked_wallets ```python import os import requests BASE = "https://x-trader.ai/api/v1" H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} cid = "7a1c2e9d-0b3f-4d6a-8e5c-2f9b1d7a4c60" r = requests.get(f"{BASE}/copy-trade/configs/{cid}", headers=H) r.raise_for_status() etag, was_active = r.headers["ETag"], r.json()["data"]["is_active"] if was_active: r = requests.post(f"{BASE}/copy-trade/configs/{cid}/pause", headers={**H, "If-Match": etag}) r.raise_for_status() etag = r.headers["ETag"] r = requests.patch( f"{BASE}/copy-trade/configs/{cid}", json={"linked_wallets": ["So11111111111111111111111111111111111111112"]}, headers={**H, "If-Match": etag}, ) r.raise_for_status() etag = r.headers["ETag"] if was_active: r = requests.post(f"{BASE}/copy-trade/configs/{cid}/resume", headers={**H, "If-Match": etag}) r.raise_for_status() ``` ## Pause and resume Pause changes the stored state immediately; execution nodes observe it eventually. Resume revalidates the full stored config before activation and refuses invalid historical data. Both take no body and require `If-Match`. ### Example: resume ```bash curl --request POST 'https://x-trader.ai/api/v1/copy-trade/configs/7a1c2e9d-0b3f-4d6a-8e5c-2f9b1d7a4c60/resume' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'If-Match: "v1"' ``` Response `200 OK` (new `ETag` header; values illustrative): ```json { "data": { "id": "7a1c2e9d-0b3f-4d6a-8e5c-2f9b1d7a4c60", "remark": "Primary copy-trade target", "target_wallet": "11111111111111111111111111111111", "is_active": true, "risk_label_id": null, "version": 2, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:01:10Z", "linked_wallets": [], "config": { "buy_config_mode": "universal", "...": "unchanged stored config" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` Use the returned `ETag` header for the next write instead of deriving it from `version`. ## Delete: DELETE /copy-trade/configs/{id} Needs `copytrade:delete` and `If-Match`. Active configurations cannot be deleted; pause first. Success is `204` with no body. ## Lifecycle 1. Create (paused) with `Idempotency-Key`; keep the `ETag`. 2. Resume with that `ETag`. 3. For `config` or `linked_wallets` changes: pause, PATCH with full values, resume. 4. `remark` and `risk_label_id` can change at any time with the latest `If-Match`. 5. To remove: pause, then delete. ## Response envelope and errors `CopyTradeResource` fields: `id`, `remark`, `target_wallet`, `is_active`, `risk_label_id`, `version`, `created_at`, `updated_at`, `linked_wallets`, `config`. Errors: `{ "error": { "code", "message", "fields"? }, "request_id" }`. | Code | Typical cause on /copy-trade/configs | |---|---| | `validation_error` | Unknown field, more than 20 `linked_wallets`, duplicate or malformed address, JSON number instead of decimal string, failed resume revalidation | | `target_already_exists` | Target wallet already used by an existing configuration | | `insufficient_scope` | Missing `copytrade:read`, `copytrade:write` or `copytrade:delete` | | `not_found` | Unknown or foreign ID, foreign label | | `config_must_be_paused` | `config` or `linked_wallets` change, or delete, on an active configuration | | `version_conflict` | Stale `If-Match` | | `idempotency_required` / `idempotency_conflict` | Missing key, or key reused with another body | | `quota_exceeded` | Shared configuration quota full | | `rate_limit_exceeded`, `service_unavailable` | `429` / `503`, honour `Retry-After` | The spec does not publish a mapping from each code to an HTTP status. See [/xtrader/errors/](/xtrader/errors/). ## FAQ ### How many linked_wallets can a copy-trade config have? Up to 20. Each entry is a unique Solana address of 32-44 characters, and the field defaults to an empty array. ### What does target_already_exists mean on copy-trade create? It is the error code the API uses when the target wallet is already in use by an existing configuration. Find that configuration with `GET /copy-trade/configs?q=
` and update it instead of creating a second one. ### Can I edit linked_wallets while the copy-trade is active? No. Pause the configuration before changing `config` or `linked_wallets`. Only `remark` and `risk_label_id` can change while it is active. ### Does PATCH linked_wallets add to the existing list? No. `linked_wallets` in a PATCH replaces the whole stored array, so send the complete list you want to keep. ### Which scopes does the copy-trade API need? `copytrade:read` for list and get, `copytrade:write` for create, patch, pause and resume, and `copytrade:delete` for delete. --- # X-Trader transfer-snipe API URL: https://docs.blizz.id/xtrader/transfer-snipe/ Updated: 2026-10-16 A transfer-snipe task is a user-scoped X-Trader task that watches one wallet (the spec calls it watch wallet A, one per task) and carries a buy and sell strategy whose `config` schema is identical to the sniper config. It is managed through seven operations under `/transfer-snipe/configs` with the `transfersnipe:read`, `transfersnipe:write` and `transfersnipe:delete` scopes. Like sniper and copy-trade tasks, it is always created paused and runs only after `POST /transfer-snipe/configs/{id}/resume`. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## What the spec says about transfer-snipe - The tag description defines a transfer-snipe task as having one watch wallet A per task, with creation paused by design. - The only wallet field in the request body is `target_wallet`, so that field holds wallet A. It is a single Solana address and is immutable after creation. - `TransferSnipeCreate` states that its config shape matches sniper; the `config` schemas of `SniperCreate` and `TransferSnipeCreate` are the same. - Transfer-snipe tasks share the label set with sniper and copy-trade (`LabelResource.transfer_snipe_count`) and count toward the same configuration quota reported by `GET /me`. The spec does not document which on-chain transfer events from wallet A trigger a buy, nor how the purchased token is chosen. It also defines no transfer-snipe templates. ## Operations | Method and path | Purpose | Scope | Required headers | Success | |---|---|---|---|---| | `GET /transfer-snipe/configs` | List owned tasks | `transfersnipe:read` | `Authorization` | `200` list envelope | | `POST /transfer-snipe/configs` | Create a paused task | `transfersnipe:write` | `Authorization`, `Idempotency-Key` | `201` resource envelope | | `GET /transfer-snipe/configs/{id}` | Read one task | `transfersnipe:read` | `Authorization` | `200` resource envelope | | `PATCH /transfer-snipe/configs/{id}` | Update mutable fields | `transfersnipe:write` | `Authorization`, `If-Match` | `200` resource envelope | | `DELETE /transfer-snipe/configs/{id}` | Delete a paused task | `transfersnipe:delete` | `Authorization`, `If-Match` | `204` no body | | `POST /transfer-snipe/configs/{id}/pause` | Stop execution | `transfersnipe:write` | `Authorization`, `If-Match` | `200` resource envelope | | `POST /transfer-snipe/configs/{id}/resume` | Revalidate and start | `transfersnipe:write` | `Authorization`, `If-Match` | `200` resource envelope | Base URL `https://x-trader.ai/api/v1`. Error statuses listed for every operation: `400`, `401`, `403`, `404`, `409`, `429`, `500`, `503`. No bulk delete. ## Headers | Header | Where | Rule | |---|---|---| | `Authorization` | All | `Bearer xt_live_.` | | `Idempotency-Key` | Create | 8-128 chars, `A-Z a-z 0-9 . _ : -`, retained 24 hours; reuse with another body gives `idempotency_conflict` | | `If-Match` | PATCH, DELETE, pause, resume | Latest `ETag`, `"v"`; stale gives `version_conflict` | | `ETag` (response) | 201/200 single-resource | Send as `If-Match` next time | | `X-Request-Id` (response) | Single-resource and delete | Support correlation | Shared write rules: [/xtrader/write-rules/](/xtrader/write-rules/). ## List: GET /transfer-snipe/configs | Query parameter | Type | Rule | |---|---|---| | `limit` | integer | 1-100, default 20 | | `cursor` | string | Max 2048; opaque `next_cursor`, bound to token and filters | | `status` | string | `active` or `paused` | | `q` | string | Max 64; searches `remark` and `target_wallet` | | `label_id` | string | Owned label UUID or `unlabeled`; foreign or missing UUID returns `404` | Ordering is by `updated_at` and `id`. Response: `data.items`, `data.next_cursor`, `meta.request_id`. See [/xtrader/pagination/](/xtrader/pagination/). ## TransferSnipeCreate fields Top level (strict; `user_id`, `version`, `template` and unknown fields are rejected): | Field | Type | Required | Default | Constraint | |---|---|---|---|---| | `remark` | string | yes | `""` | Max 64 characters, trimmed | | `target_wallet` | string | yes | | Watch wallet A. Solana address, 32-44 characters, immutable | | `risk_label_id` | string (UUID) or null | no | | Label owned by the account; `null` unassigns; foreign ID returns `404` | | `config` | object | yes | | Complete strategy, schema below | `config` (same schema as the sniper config; full reference at [/xtrader/config-sniper/](/xtrader/config-sniper/)): | Field | Type | Required | Default | Constraint and meaning | |---|---|---|---|---| | `buy_amount` | decimal string | yes | | SOL per buy, greater than 0 and at most 1,000,000 | | `jito_mode` | object | yes | | Jito route fees: `tip_amount` (0-1,000 SOL), `priority_fee` (0-1,000 SOL), `slippage` (0-100 %) required; optional `bundle_amount`, `sol_slippage_mode` | | `priority_mode` | object | yes | | Leader/direct route fees, same keys and ranges as `jito_mode` | | `bundle_amount` | decimal string | no | | Legacy top-level fallback; prefer the copy inside `jito_mode` / `priority_mode` | | `selected_dexes` | array | yes | | 1-5 of `pump-fun`, `launchlab`, `believe`, `bags`, `meteora-dynamic-bc` | | `dex_source` | string | yes | `global` | `global` uses the enabled global DEX list (falls back to `selected_dexes`); `config` always uses `selected_dexes` | | `dev_min_amount` | decimal string | no | | Lower bound of the creator buy filter; `""` disables it | | `dev_max_amount` | decimal string | no | | Upper bound; `""` disables it; must be at least `dev_min_amount` when both are set | | `buy_once` | boolean | yes | `false` | Deactivate the task after its first triggered trade | | `sol_slippage_mode` | boolean | yes | `false` | Legacy shared SOL-slippage flag; per-mode flags take precedence | | `snipe_external` | boolean | yes | `false` | Skip the bonding curve and use supported external-pool execution only | | `auto_sell` | object | yes | | TP, SL, trailing TP, follow-sell; see [/xtrader/auto-sell/](/xtrader/auto-sell/) | | `auto_strategy` | object | yes | | Migration, timed sell, protections, sell-fee override; see [/xtrader/auto-strategy/](/xtrader/auto-strategy/) | | `timed_strategy` | object | yes | | Up to 20 delayed sell orders; see [/xtrader/timed-strategy/](/xtrader/timed-strategy/) | All amounts, fees, percentages and durations are decimal strings (at most 12 fractional digits, max 32 characters), never JSON numbers. Every nested object is strict. `TransferSnipeResource` adds the server fields `id`, `is_active`, `version`, `created_at` and `updated_at`, and returns `remark`, `target_wallet`, `risk_label_id` and `config`. ## Create ```bash curl --request POST 'https://x-trader.ai/api/v1/transfer-snipe/configs' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: create-transfer-20261016-001' \ --data '{ "remark": "Watch wallet A", "target_wallet": "11111111111111111111111111111111", "config": { "buy_amount": "0.1", "jito_mode": {"tip_amount": "0.01", "priority_fee": "0.0005", "slippage": "15", "bundle_amount": ""}, "priority_mode": {"tip_amount": "0.001", "priority_fee": "0.005", "slippage": "15", "bundle_amount": ""}, "selected_dexes": ["pump-fun"], "dex_source": "global", "dev_min_amount": "", "dev_max_amount": "", "buy_once": false, "sol_slippage_mode": false, "snipe_external": false, "auto_sell": { "enabled": false, "tab": "tpsl", "expiry_time": "10", "tp_orders": [{"trigger": "90", "sell": "100"}], "sl_orders": [], "follow_sell": false, "proportional_sell": false, "trailing_take_profit": {"enabled": false, "tiers": []} }, "auto_strategy": {"migration_clear": false, "timed_sell": false, "sell_delay": "30"}, "timed_strategy": {"enabled": false, "orders": []} } }' ``` Response `201 Created` (headers `ETag: "v1"`, `X-Request-Id`; IDs and timestamps illustrative): ```json { "data": { "id": "c4e7a2b0-91d3-4f5e-a6b8-0d2c9e1f3a75", "remark": "Watch wallet A", "target_wallet": "11111111111111111111111111111111", "is_active": false, "risk_label_id": null, "version": 1, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:00:00Z", "config": { "buy_amount": "0.1", "...": "stored config as sent" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` ## Get, patch, delete - `GET /transfer-snipe/configs/{id}`: missing and foreign IDs both return `404 not_found`. The stored snapshot is returned as is, possibly with legacy fields. - `PATCH /transfer-snipe/configs/{id}` (`TransferSnipePatch`): at least one of `remark`, `risk_label_id`, `config`. While active, only `remark` and `risk_label_id` may change; pause before changing `config`. A `config` in the patch replaces the whole stored config and must pass the full schema. `target_wallet` cannot be patched. The operation text also mentions `linked_wallets`, but `TransferSnipePatch` has no such field and unknown fields are rejected. - `DELETE /transfer-snipe/configs/{id}`: only paused tasks; `204` on success. ## Pause and resume Pause updates the stored state immediately; execution nodes pick it up eventually. Resume revalidates the complete stored config before activation and will not activate invalid historical data. ```bash curl --request POST 'https://x-trader.ai/api/v1/transfer-snipe/configs/c4e7a2b0-91d3-4f5e-a6b8-0d2c9e1f3a75/resume' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'If-Match: "v1"' ``` Response `200 OK` with the new `ETag` header (values illustrative): ```json { "data": { "id": "c4e7a2b0-91d3-4f5e-a6b8-0d2c9e1f3a75", "remark": "Watch wallet A", "target_wallet": "11111111111111111111111111111111", "is_active": true, "risk_label_id": null, "version": 2, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:01:10Z", "config": { "buy_amount": "0.1", "...": "unchanged stored config" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed", "propagation": "eventual" } } ``` ### Python: pause, change buy amount, resume ```python import os import requests BASE = "https://x-trader.ai/api/v1" H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} tid = "c4e7a2b0-91d3-4f5e-a6b8-0d2c9e1f3a75" r = requests.get(f"{BASE}/transfer-snipe/configs/{tid}", headers=H) r.raise_for_status() task, etag = r.json()["data"], r.headers["ETag"] if task["is_active"]: r = requests.post(f"{BASE}/transfer-snipe/configs/{tid}/pause", headers={**H, "If-Match": etag}) r.raise_for_status() etag = r.headers["ETag"] config = task["config"] # full object: PATCH replaces config completely config["buy_amount"] = "0.2" # decimal string, not a number r = requests.patch(f"{BASE}/transfer-snipe/configs/{tid}", json={"config": config}, headers={**H, "If-Match": etag}) r.raise_for_status() r = requests.post(f"{BASE}/transfer-snipe/configs/{tid}/resume", headers={**H, "If-Match": r.headers["ETag"]}) r.raise_for_status() ``` If the stored snapshot contains legacy web-UI fields, the PATCH or resume can fail with `validation_error`; remove the fields listed in `error.fields` and send a config that matches the current schema. ## Lifecycle 1. Create (paused) with an `Idempotency-Key`. 2. Resume with the `ETag` from the create response. 3. To change the strategy: pause, PATCH the full `config`, resume. 4. `remark` and `risk_label_id` can change without pausing (still with `If-Match`). 5. To remove: pause, then delete. ## Errors Error body: `{ "error": { "code", "message", "fields"? }, "request_id" }`. | Code | Typical cause on /transfer-snipe/configs | |---|---| | `validation_error` | Unknown field, number instead of decimal string, empty PATCH, failed resume revalidation | | `insufficient_scope` | Token lacks the needed `transfersnipe:*` scope | | `not_found` | Unknown or foreign task ID, foreign label | | `config_must_be_paused` | `config` change or delete while active | | `version_conflict` | Stale `If-Match` | | `idempotency_required` / `idempotency_conflict` | Missing key, or key reused with another body | | `quota_exceeded` | Shared configuration quota full | | `rate_limit_exceeded`, `service_unavailable` | `429` / `503` with `Retry-After` | The spec does not say whether `target_already_exists` applies to transfer-snipe. Full list: [/xtrader/errors/](/xtrader/errors/). ## FAQ ### What is a transfer-snipe task in the X-Trader API? It is a user-scoped task with exactly one watch wallet (wallet A), stored in `target_wallet`, plus a strategy `config` whose shape matches the sniper config. Like other tasks it is created paused. ### Which scopes does transfer-snipe use? `transfersnipe:read` for list and get, `transfersnipe:write` for create, patch, pause and resume, and `transfersnipe:delete` for delete. ### Can one transfer-snipe task watch several wallets? No. The spec defines one watch wallet A per task, and `target_wallet` is a single immutable address. Create one task per wallet. ### Is the transfer-snipe config different from the sniper config? No. The `config` object in `TransferSnipeCreate` and `TransferSnipePatch` has the same schema as the sniper config, including `buy_amount`, `jito_mode`, `priority_mode` and the sell objects. ### Are there transfer-snipe templates? The spec defines templates only for sniper and copy-trade. There are no `/transfer-snipe/templates` endpoints. --- # X-Trader sniper and copy-trade templates API URL: https://docs.blizz.id/xtrader/templates/ Updated: 2026-10-16 X-Trader templates are reusable, named strategy snapshots owned by the current user: a template holds a `name` and a complete `config` in the same shape as a sniper or copy-trade task config. The API exposes them at `/sniper/templates` and `/copy-trade/templates` with list, create, get, patch and delete operations, guarded by the `sniper:*` and `copytrade:*` scopes respectively. Both kinds share a limit of 1,000 templates, and changing or deleting a template never rewrites the config stored in existing tasks. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Operations | Method and path | Scope | Required headers | Success | |---|---|---|---| | `GET /sniper/templates` | `sniper:read` | `Authorization` | `200` list envelope | | `POST /sniper/templates` | `sniper:write` | `Authorization`, `Idempotency-Key` | `201` resource envelope | | `GET /sniper/templates/{id}` | `sniper:read` | `Authorization` | `200` resource envelope | | `PATCH /sniper/templates/{id}` | `sniper:write` | `Authorization`, `If-Match` | `200` resource envelope | | `DELETE /sniper/templates/{id}` | `sniper:delete` | `Authorization`, `If-Match` | `204` no body | | `GET /copy-trade/templates` | `copytrade:read` | `Authorization` | `200` list envelope | | `POST /copy-trade/templates` | `copytrade:write` | `Authorization`, `Idempotency-Key` | `201` resource envelope | | `GET /copy-trade/templates/{id}` | `copytrade:read` | `Authorization` | `200` resource envelope | | `PATCH /copy-trade/templates/{id}` | `copytrade:write` | `Authorization`, `If-Match` | `200` resource envelope | | `DELETE /copy-trade/templates/{id}` | `copytrade:delete` | `Authorization`, `If-Match` | `204` no body | Base URL `https://x-trader.ai/api/v1`. All operations also list `400`, `401`, `403`, `404`, `409`, `429`, `500` and `503`. There are no pause or resume operations for templates (a template does not execute), and there are no transfer-snipe templates. ## Headers | Header | Where | Rule | |---|---|---| | `Authorization` | All | `Bearer xt_live_.` | | `Idempotency-Key` | POST create | 8-128 chars from `A-Z a-z 0-9 . _ : -`, retained 24 hours; same key with another body gives `idempotency_conflict` | | `If-Match` | PATCH, DELETE | Latest `ETag` such as `"v3"`; stale gives `version_conflict` | | `ETag` (response) | 201/200 single-template responses | Version for the next write | | `X-Request-Id` (response) | Single-template and delete responses | Support correlation | ## Fields ### Create body (`SniperTemplateCreate`, `CopyTradeTemplateCreate`) | Field | Type | Required | Rule | |---|---|---|---| | `name` | string | yes | 1-64 characters, leading and trailing spaces removed | | `config` | object | yes | Complete strategy object. Sniper templates use the sniper config schema ([/xtrader/config-sniper/](/xtrader/config-sniper/)); copy-trade templates use the copy-trade config schema ([/xtrader/config-copy-trade/](/xtrader/config-copy-trade/)). | Rejected on create: `user_id`, `version`, `risk_label_id`, and any unknown property. Templates never accept or return `risk_label_id`, so labels apply to tasks only (see [/xtrader/labels/](/xtrader/labels/)). Task-level fields such as `remark`, `target_wallet` and `linked_wallets` are not part of the template schemas. ### Patch body (`SniperTemplatePatch`, `CopyTradeTemplatePatch`) At least one of `name` and `config`. `config`, when present, is a complete replacement that must pass the full schema; it is not merged into the stored config. ### Resource (`SniperTemplateResource`, `CopyTradeTemplateResource`) | Field | Type | Meaning | |---|---|---| | `id` | UUID | Server-generated template ID | | `name` | string | Display name | | `version` | integer | Concurrency version, also returned as `ETag` | | `created_at` | string | ISO 8601 with UTC offset | | `updated_at` | string | ISO 8601 with UTC offset | | `config` | object | Stored strategy snapshot | ## List `GET /sniper/templates` and `GET /copy-trade/templates` return only the caller's templates. | Query parameter | Type | Rule | |---|---|---| | `limit` | integer | 1-100, default 20 | | `cursor` | string | Max 2048; opaque `next_cursor`, bound to the token and the search query | | `q` | string | Max 64; searches template names only | There are no `status` or `label_id` filters on template lists. Response: `data.items`, `data.next_cursor` (string or null), `meta.request_id`. See [/xtrader/pagination/](/xtrader/pagination/). ## Create ```bash curl --request POST 'https://x-trader.ai/api/v1/sniper/templates' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: template-create-001' \ --data '{ "name": "Pump launch defaults", "config": { "buy_amount": "0.1", "jito_mode": {"tip_amount": "0.01", "priority_fee": "0.0005", "slippage": "15", "bundle_amount": ""}, "priority_mode": {"tip_amount": "0.001", "priority_fee": "0.005", "slippage": "15", "bundle_amount": ""}, "selected_dexes": ["pump-fun"], "dex_source": "global", "dev_min_amount": "", "dev_max_amount": "", "buy_once": false, "sol_slippage_mode": false, "snipe_external": false, "auto_sell": { "enabled": false, "tab": "tpsl", "expiry_time": "10", "tp_orders": [{"trigger": "90", "sell": "100"}], "sl_orders": [], "follow_sell": false, "proportional_sell": false, "trailing_take_profit": {"enabled": false, "tiers": []} }, "auto_strategy": {"migration_clear": false, "timed_sell": false, "sell_delay": "30"}, "timed_strategy": {"enabled": false, "orders": []} } }' ``` Response `201 Created` (headers `ETag`, `X-Request-Id`; IDs and timestamps illustrative): ```json { "data": { "id": "5b9d3e1a-2c7f-4a8b-9e6d-4f1a0c3b7d92", "name": "Pump launch defaults", "version": 1, "created_at": "2026-10-16T09:00:00Z", "updated_at": "2026-10-16T09:00:00Z", "config": { "buy_amount": "0.1", "...": "stored config as sent" } }, "meta": { "request_id": "req_0123456789abcdef", "persistence": "committed" } } ``` A copy-trade template is created the same way at `/copy-trade/templates`, with a copy-trade `config` (starting with `buy_config_mode`, `buy_config_universal`, `buy_config_tiers` and so on). ## Patch semantics `PATCH /{kind}/templates/{id}` can rename the template, replace its full config, or both. The spec states explicitly that existing tasks are not rewritten automatically: a task keeps the config snapshot it was created or last patched with. To roll a template change out to tasks, pause each task, PATCH its `config` with the new template config, and resume it. ```bash curl --request PATCH 'https://x-trader.ai/api/v1/sniper/templates/5b9d3e1a-2c7f-4a8b-9e6d-4f1a0c3b7d92' \ --header "Authorization: Bearer $X_TRADER_API_TOKEN" \ --header 'Content-Type: application/json' \ --header 'If-Match: "v1"' \ --data '{"name": "Updated defaults"}' ``` ## Delete semantics `DELETE /{kind}/templates/{id}` removes the template and detaches linked tasks (the link is cleared with `ON DELETE SET NULL`). Tasks are not deleted, and their stored config snapshots are not changed. Templates need no pause before delete. Success is `204` with no body. ## Name conflicts and the 1,000 limit - `template_name_exists` is the error code for a name clash. The spec does not state the uniqueness scope (per kind or across both kinds); treat names as unique per account and pick a different `name` or rename the existing template. - Sniper and copy-trade templates share one limit of 1,000 templates in total. The spec does not name the error code returned at the limit; `quota_exceeded` is the only quota code in the error enum. - Templates are separate from the task configuration quota reported by `GET /me`, which covers sniper, copy-trade and transfer-snipe tasks. ## How a task relates to a template The spec mentions linked tasks (delete detaches them) but the public API exposes no field to set or read that link: `SniperCreate` and `CopyTradeCreate` explicitly reject a `template` property, and task resources return no template ID. Through the API, a template therefore works as a copy source: ```python import os import uuid import requests BASE = "https://x-trader.ai/api/v1" H = {"Authorization": f"Bearer {os.environ['X_TRADER_API_TOKEN']}"} tpl = requests.get(f"{BASE}/copy-trade/templates/5b9d3e1a-2c7f-4a8b-9e6d-4f1a0c3b7d92", headers=H) tpl.raise_for_status() config = tpl.json()["data"]["config"] task = requests.post( f"{BASE}/copy-trade/configs", json={ "remark": "From template", "target_wallet": "11111111111111111111111111111111", "linked_wallets": [], "config": config, }, headers={**H, "Idempotency-Key": f"copy-{uuid.uuid4()}"}, ) task.raise_for_status() print(task.json()["data"]["id"], task.headers["ETag"]) # created paused ``` Because template list and get return the stored snapshot as is, an older template may contain legacy fields that the current task schema rejects; in that case the create fails with `validation_error` and `error.fields` points to the offending paths. Task endpoints: [/xtrader/sniper/](/xtrader/sniper/), [/xtrader/copy-trade/](/xtrader/copy-trade/). ## Errors | Code | Typical cause on template endpoints | |---|---| | `validation_error` | Empty or too long `name`, unknown field, `risk_label_id` in body, config not matching the schema, empty PATCH | | `template_name_exists` | Name already used | | `quota_exceeded` | Likely code at the shared 1,000-template limit (not stated in the spec) | | `insufficient_scope` | Missing `sniper:*` or `copytrade:*` scope for the operation | | `not_found` | Unknown template ID or one owned by another user | | `version_conflict` | Stale `If-Match` on PATCH or DELETE | | `idempotency_required` / `idempotency_conflict` | Missing key on create, or key reused with another body | | `rate_limit_exceeded`, `service_unavailable` | `429` / `503` with `Retry-After` | Error body shape and handling: [/xtrader/errors/](/xtrader/errors/). ## FAQ ### Does updating a template change tasks created from it? No. A template PATCH replaces the template's own `name` or `config` only; existing task config snapshots are not rewritten. ### What happens to tasks when I delete a template? Linked tasks are detached from the template, not deleted, and their stored config snapshots stay as they were. ### How many templates can an account have? Sniper and copy-trade templates share one limit of 1,000 templates in total. ### How do I create a task from a template through the API? The public task schemas have no template reference field and reject a `template` property. GET the template, copy its `config` into the task create body, and POST it as a normal sniper or copy-trade create. ### Which scopes do templates need? Sniper templates use `sniper:read`, `sniper:write` and `sniper:delete`. Copy-trade templates use `copytrade:read`, `copytrade:write` and `copytrade:delete`. --- # X-Trader labels API URL: https://docs.blizz.id/xtrader/labels/ Updated: 2026-10-16 X-Trader labels are a per-account set of up to 20 named, coloured tags shared by sniper, copy-trade and transfer-snipe tasks. They are managed at `/labels` (list, create, get, update, delete) and bound to tasks through the `risk_label_id` field. Label names are unique per account, labels do not use `ETag`/`If-Match`, and templates never take a label. Primary source: [X-Trader API reference](https://x-trader.ai/api/docs). This page is an independent explanation of X-Trader Public API v1.0.0; it is not affiliated with X-Trader. ## Endpoints | Method and path | Required scope | Headers | Success | |---|---|---|---| | `GET /labels` | `sniper:read` or `copytrade:read` | `Authorization` | `200`, all labels | | `POST /labels` | `sniper:write` or `copytrade:write` | `Authorization`, `Idempotency-Key` | `201`, created label | | `GET /labels/{id}` | `sniper:read` or `copytrade:read` | `Authorization` | `200`, one label | | `PATCH /labels/{id}` | `sniper:write` or `copytrade:write` | `Authorization` | `200`, updated label | | `DELETE /labels/{id}` | `sniper:write` or `copytrade:write` | `Authorization` | `204`, empty | Either product scope is sufficient. The spec lists no `transfersnipe:*` scope for label endpoints, even though transfer-snipe tasks can be labelled, so a transfer-snipe-only token cannot manage labels. Deletion uses a `:write` scope; there is no label `:delete` scope. Responses carry `X-Request-Id` but no `ETag`. ## Label fields ### LabelResource (returned) | Field | Type | Meaning | |---|---|---| | `id` | UUID | Server-generated label id | | `name` | string, 1-32 characters | Display name, trimmed | | `color` | string, `^#[0-9A-Fa-f]{6}$` | Colour as `#RRGGBB` | | `sort_order` | integer | Display order (0-32767 on input) | | `sniper_count` | integer, 0 or more | Sniper configs using this label | | `copy_trade_count` | integer, 0 or more | Copy-trade configs using this label | | `transfer_snipe_count` | integer, 0 or more | Transfer-snipe tasks using this label | | `created_at` | ISO 8601 date-time with UTC offset | Creation time | | `updated_at` | ISO 8601 date-time with UTC offset | Last change | ### LabelCreate (POST body) | Field | Type | Default | Constraints | |---|---|---|---| | `name` | string | none | 1-32 characters after trimming leading and trailing spaces; unique per account | | `color` | string | `#6366f1` | `#RRGGBB` | | `sort_order` | integer | `0` | 0-32767 | The schema lists all three as required while also giving defaults for `color` and `sort_order`; the spec's own curl example sends only `name` and `color`. Sending all three avoids any ambiguity. ### LabelPatch (PATCH body) Any of `name`, `color`, `sort_order` with the same constraints; at least one field is required. Unknown fields and `user_id` are rejected with `validation_error`. ## Rules and limits | Rule | Behaviour | |---|---| | Maximum labels per account | 20; the 21st create returns `quota_exceeded` | | Unique names | Duplicate name on create or `PATCH` returns `label_name_exists` | | Shared set | One set for sniper, copy-trade and transfer-snipe tasks | | List order | `sort_order`, then `created_at` | | Pagination | None; `GET /labels` returns all labels in `data.items` | | Concurrency | No `ETag` / `If-Match` on labels; last write applies | | Idempotency | `Idempotency-Key` required on `POST /labels` (8-128 chars, `A-Za-z0-9._:-`, kept 24 h) | | Renaming | Bound tasks keep the same label id | | Delete | Tasks are unassigned (`ON DELETE SET NULL`), not deleted | | Foreign id | Label owned by another account returns `404 not_found` | ## Binding labels to tasks with risk_label_id Sniper, copy-trade and transfer-snipe configs have a nullable `risk_label_id` field. | Action | How | |---|---| | Bind on create | Include `"risk_label_id": "