# 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_<id>.<secret>`. 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<number>"`, 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.
