> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cabalspy.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Copy trading

> Let your users copy the traders in their Trade Feed. For every trade of a copied trader, CabalSpy ID sends your app a ready-to-execute instruction sized by the user's settings. Your app executes it with the user's wallet and reports the result.

Copy trading lets your users copy the traders in their [Trade Feed](https://docs.cabalspy.xyz/cabalspy-id/trade-feed.md). Whenever a copied trader buys or sells, CabalSpy ID sends your app a **copy instruction** for each user who copies them: which token, on which chain, buy or sell, how much, and the price limit. Your app executes the instruction with the user's wallet and reports back whether it was executed, failed or skipped.

> [!WARNING]
>
> CabalSpy ID never holds keys and never executes trades. It only tells your app what to do. Your platform executes every trade with the user's own wallet, under your own rules, and stays responsible for it.

Copy trading is available to every instance on every plan, there is nothing to switch on. A key only reaches the copy settings and instructions of its own instance.

## Who can copy

Copying always happens in the name of one of your users:

* a [platform user](https://docs.cabalspy.xyz/cabalspy-id/white-label.md), by `external_id` or `csid_id`,
* or a person who signed in to your app with CabalSpy ID and granted the scope `trade_feed`, by `csid_id`.

The user must be within your plan's user limit. Users without the scope get 403 [consent\_required](https://docs.cabalspy.xyz/cabalspy-id/errors.md#consent_required). Platform users always have it.

A user can copy any trader they can add to their Trade Feed: a CabalSpy ID user whose trading is public, or another of your own platform users. Copy settings are stored per user and trader, and switching copying on adds the trader to the user's Trade Feed if they are not there yet.

## How it works

**1. The user switches copying on**

Your backend calls [POST /v1/copy-trading/settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md) with the user, the trader and how much to copy. From now on your instance copies this trader for this user.

**2. The trader trades**

CabalSpy ID sees the trade on one of the trader's verified wallets, the same moment it reaches the Trade Feed.

**3. CabalSpy ID sends an instruction**

For each user who copies the trader, CabalSpy ID sizes the trade by the user's settings and sends a [copy instruction](#the-instruction) as the `copy_trade.instruction` [webhook](https://docs.cabalspy.xyz/cabalspy-id/webhooks.md#copy-trading-events) and on the [copy trading stream](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-stream.md). You can also poll [GET /v1/copy-trading/instructions](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-instructions.md).

**4. Your app executes it**

Within 120 seconds, your app swaps with the user's wallet, respecting `max_price_usd` (buys) or `min_price_usd` (sells).

**5. Your app reports the result**

Call [POST /v1/copy-trading/instructions/status](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-status.md) with `executed`, `failed` or `skipped`, and the transaction signature when there is one. The result decides whether later sells of that token are copied.

## One platform at a time

A user can use several platforms with CabalSpy ID, but each trader is copied by **one platform at a time**, so a user is never copied twice. The platform that switches copying on for a trader receives that trader's instructions for that user, and nobody else does.

While your platform copies a trader for a user, another platform cannot switch copying on for the same trader and gets 409 [copy\_managed\_elsewhere](https://docs.cabalspy.xyz/cabalspy-id/errors.md#copy_managed_elsewhere). It sees `managed_by_other_platform: true` in [GET /v1/copy-trading/settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md#read-the-settings). Once the first platform switches copying off, another platform can take over.

## Sizing

Each user has their own settings per trader. See [Copy settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md) for all fields and limits.

**Buys.** The amount in USD comes from `mode`:

| Mode | Amount of each copied buy |
| - | - |
| `fixed_usd` (default) | Always `amount_usd`, default \$50. |
| `percent` | `percent` % of the trader's buy in USD. 100 (default) is the same size as the trader, 10 is a tenth, up to 1,000. A buy whose USD value is unknown is not copied. |

`max_usd`, if set, caps every buy. Buys that come out **below \$1** are not sent. The instruction carries the amount in USD (`amount_usd`) and, where the price of the chain's native coin is known, in the native coin (`amount_native`, for example SOL).

**Sells.** With `sides: "both"` (default), sells are copied as a share of the user's copied position, not as an amount:

* The trader sold part of their position: sell the same share. A trader who sold 50 % of their tokens sends `sell_percent: 50`.
* The trader closed their position: `sell_percent: 100`.
* A sell is only sent when the user has a **copied buy** in that token from that trader that you did not report as `failed` or `skipped`. A user who never copied the entry does not get the exit.
* Sells whose entry is unknown, for example of tokens the trader bought before CabalSpy ID tracked their wallet, are not copied.

With `sides: "buys"`, only buys are copied and the user decides when to sell.

**Price limit.** Every instruction carries the trader's price (`price_usd`) and the user's slippage (`slippage_bps`, default 300 = 3 %). For a buy, `max_price_usd` is the trader's price plus the slippage, for a sell `min_price_usd` is the trader's price minus the slippage. Do not execute beyond that limit; report `skipped` instead.

**Example.** A user copies with `mode: "fixed_usd"`, `amount_usd: 25` and `max_usd: 20`. The trader buys 1,000 tokens for 1 SOL ($150), so at $0.15 per token. The user's instruction: buy for \*\*$20** (capped), 0.133333333 SOL, at most $0.1545 per token (+3 %).

Copying does **not** depend on the user's alert filters. A trader the user muted in their Trade Feed, or with buy or sell alerts off or a minimum trade size, is still copied in full.

## The instruction

One instruction per user and trade. The same trade is never sent twice for the same user: the `id` is fixed per user and trade, so deduplicate on it.

```json copy_trade.instruction theme={null}
{
  "id": "9d4b2f6e-1c3a-5e7f-8a9b-0c1d2e3f4a5b",
  "type": "copy_instruction",
  "status": "pending",
  "created_at": "2026-11-01T18:02:11Z",
  "expires_at": "2026-11-01T18:04:11Z",
  "csid_id": "3f9a1c2e-5b7d-4e8f-a1b2-c3d4e5f60718",
  "external_id": "user_8412",
  "trader": {
    "csid_id": "7c2e9a14-6b3d-4f81-9e0a-5d4c3b2a1f09",
    "username": "MazeCCC",
    "name": "Maze",
    "image_url": "https://pbs.twimg.com/profile_images/1/maze_normal.jpg",
    "kol": true,
    "profile_url": "https://id.cabalspy.xyz/u/MazeCCC"
  },
  "action": {
    "blockchain": "solana",
    "mint": "3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
    "symbol": "CPT",
    "name": "Copycat",
    "logo": "https://...",
    "side": "buy",
    "amount_usd": 20.0,
    "amount_native": 0.133333333,
    "currency": "SOL",
    "sell_percent": null,
    "price_usd": 0.15,
    "max_price_usd": 0.1545,
    "min_price_usd": null,
    "slippage_bps": 300
  },
  "source_trade": {
    "blockchain": "solana",
    "mint": "3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
    "symbol": "CPT",
    "name": "Copycat",
    "logo": "https://...",
    "side": "buy",
    "label": "new",
    "sold_percent": null,
    "amount_native": 1.0,
    "currency": "SOL",
    "amount_usd": 150.0,
    "token_amount": 1000.0,
    "price_usd": 0.15,
    "pnl_usd": null,
    "pnl_percent": null,
    "wallet": "7j7AA3HZR2zEjwAQEKPFh2qucLY4fqZpB9iodf39w8xW",
    "wallet_url": "https://solscan.io/account/7j7AA3HZR2zEjwAQEKPFh2qucLY4fqZpB9iodf39w8xW",
    "tx_signature": "5xr8...",
    "tx_url": "https://solscan.io/tx/5xr8..."
  },
  "settings": {
    "enabled": true,
    "mode": "fixed_usd",
    "amount_usd": 25.0,
    "percent": 100.0,
    "max_usd": 20.0,
    "sides": "both",
    "slippage_bps": 300,
    "updated_at": "2026-11-01T17:58:40Z"
  },
  "links": {
    "token": "https://app.cabalspy.xyz/token/3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
    "terminal": "https://app.cabalspy.xyz/token/3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
    "profile": "https://id.cabalspy.xyz/u/MazeCCC"
  },
  "tx_signature": null,
  "executed_amount_usd": null,
  "note": null,
  "updated_at": "2026-11-01T18:02:11Z"
}
```

A copied sell looks the same, with `"side": "sell"`, `sell_percent` set, `amount_usd` and `amount_native` null and `min_price_usd` instead of `max_price_usd`:

```json action of a sell theme={null}
{
  "blockchain": "solana",
  "mint": "3Pkrq4MmLvDXyn1fa3sz5MekKRkZkc1iLDcz5AzWpump",
  "symbol": "CPT",
  "name": "Copycat",
  "logo": "https://...",
  "side": "sell",
  "amount_usd": null,
  "amount_native": null,
  "currency": "SOL",
  "sell_percent": 50.0,
  "price_usd": 0.225,
  "max_price_usd": null,
  "min_price_usd": 0.21825,
  "slippage_bps": 300
}
```

**id** `string`

The instruction id, fixed per user and trade. Deduplicate on it.

**type** `string`

Always `copy_instruction`.

**status** `string`

`pending`, `executed`, `failed`, `skipped` or `expired`. See [Statuses](#statuses).

**created\_at, expires\_at** `string`

When the instruction was made, and until when it is meant to be executed: 120 seconds later.

**csid\_id** `string`

The user who copies.

**external\_id** `string`

Your own id of the user when they are a platform user, otherwise null. Only in the webhook and on the stream; look the user up by `csid_id` in REST responses.

**trader** `object`

The trader who traded: csid\_id, username, name, image\_url, kol and profile\_url. username and profile\_url are null for platform users.

**action** `object`

What to execute.

**action**

**blockchain** `string`

solana, bnb, base, eth or rh.

**mint** `string`

The token address.

**symbol, name, logo** `string`

Token info, any of them can be null.

**side** `string`

`buy` or `sell`.

**amount\_usd** `number`

Buys: how much to buy, in USD. Null for sells.

**amount\_native** `number`

Buys: the same amount in the chain's native coin (`currency`), at the trader's rate. Null for sells, and when the rate is unknown.

**currency** `string`

The chain's native coin: SOL, BNB or ETH.

**sell\_percent** `number`

Sells: the share of the user's copied position to sell, 0 to 100. 100 when the trader closed their position. Null for buys.

**price\_usd** `number`

The trader's price per token in USD, or null when unknown.

**max\_price\_usd** `number`

Buys: the highest price per token to accept, the trader's price plus `slippage_bps`. Null for sells.

**min\_price\_usd** `number`

Sells: the lowest price per token to accept, the trader's price minus `slippage_bps`. Null for buys.

**slippage\_bps** `integer`

The user's slippage in basis points, 300 = 3 %.

**source\_trade** `object`

The trader's own trade, the same trade object as in the [Trade Feed](https://docs.cabalspy.xyz/cabalspy-id/trade-feed-api.md#the-trade-item): side, label (`new`, `add`, `partial`, `close`), sold\_percent, amounts in the native coin and USD, token amount, price, realized PnL of a sell, the trader's wallet and the transaction with explorer links.

**settings** `object`

The user's [copy settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md) for this trader at the time of the instruction.

**links** `object`

token (the token page), terminal (the CabalSpy Terminal on Solana and BNB Chain, otherwise null) and profile (the trader's profile).

**tx\_signature, executed\_amount\_usd, note** `string`

What your app reported, see [Report the result](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-status.md). Null until then.

**updated\_at** `string`

When the instruction was made or last reported.

## Statuses

| Status | Meaning |
| - | - |
| `pending` | Waiting for your app. |
| `executed` | You reported that the trade went through. |
| `failed` | You reported that the trade failed, for example for lack of balance. |
| `skipped` | You decided not to execute it, for example because the price moved beyond the limit. |
| `expired` | Still pending 120 seconds after it was made. You can still report it, for example `executed` when your transaction confirmed late. |

`executed`, `failed` and `skipped` are final. A copied buy reported as `failed` or `skipped` means later sells of that token are not copied for that user. A buy still `pending` or `expired` counts as a position.

## Quickstart

**1. Switch copying on**

Copy `@MazeCCC` for one of your platform users with $25 per buy, at most $20:

```bash theme={null}
curl -X POST "https://api-id.cabalspy.xyz/v1/copy-trading/settings" \
  -H "Authorization: Bearer YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "user_8412", "trader": "MazeCCC", "enabled": true, "mode": "fixed_usd", "amount_usd": 25, "max_usd": 20}'
```

**2. Receive instructions**

Subscribe your webhook to `copy_trade.instruction` on the instance's [Webhooks](https://docs.cabalspy.xyz/cabalspy-id/dashboard.md#webhooks) tab, or connect to the [copy trading stream](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-stream.md). Verify the signature as described in [Webhooks](https://docs.cabalspy.xyz/cabalspy-id/webhooks.md#verify-the-signature) and answer `2xx` right away.

**3. Execute**

Look up the user by `external_id` or `csid_id`, check `expires_at`, and swap with their wallet within the price limit. For sells, sell `sell_percent` % of what the user holds from their copied buys of this token.

**4. Report**

Report every instruction, also the ones you did not execute.

```python Python theme={null}
import requests
from flask import Flask, request

API = "https://api-id.cabalspy.xyz"
H = {"Authorization": "Bearer YOUR_KEY"}
app = Flask(__name__)
seen = set()

@app.post("/webhooks/csid")
def webhook():
    # verify the Standard Webhooks signature first, see Webhooks
    event = request.get_json()
    if event["type"] != "copy_trade.instruction" or event["data"]["id"] in seen:
        return "", 204
    i = event["data"]
    seen.add(i["id"])
    queue_copy(i)          # do the work outside the request
    return "", 204

def execute(i):
    a = i["action"]
    try:
        if a["side"] == "buy":
            # your swap: spend a["amount_usd"] (or a["amount_native"] of a["currency"])
            # on a["mint"], at most a["max_price_usd"] per token
            sig, usd = my_buy(i["external_id"] or i["csid_id"], a)
        else:
            # sell a["sell_percent"] % of the user's copied position, at least a["min_price_usd"]
            sig, usd = my_sell(i["external_id"] or i["csid_id"], a)
        body = {"id": i["id"], "status": "executed", "tx_signature": sig, "executed_amount_usd": usd}
    except PriceMoved:
        body = {"id": i["id"], "status": "skipped", "note": "price above the limit"}
    except Exception as e:
        body = {"id": i["id"], "status": "failed", "note": str(e)[:300]}
    requests.post(f"{API}/v1/copy-trading/instructions/status", json=body, headers=H)
```

```javascript JavaScript theme={null}
import express from "express";

const API = "https://api-id.cabalspy.xyz";
const H = { Authorization: "Bearer YOUR_KEY", "Content-Type": "application/json" };
const app = express();
const seen = new Set();

app.post("/webhooks/csid", express.json(), (req, res) => {
  // verify the Standard Webhooks signature first, see Webhooks
  const event = req.body;
  res.sendStatus(204);
  if (event.type !== "copy_trade.instruction" || seen.has(event.data.id)) return;
  seen.add(event.data.id);
  execute(event.data);
});

async function execute(i) {
  const a = i.action;
  let body;
  try {
    const { sig, usd } = a.side === "buy"
      ? await myBuy(i.external_id ?? i.csid_id, a)   // a.amount_usd on a.mint, at most a.max_price_usd
      : await mySell(i.external_id ?? i.csid_id, a); // a.sell_percent % of the copied position, at least a.min_price_usd
    body = { id: i.id, status: "executed", tx_signature: sig, executed_amount_usd: usd };
  } catch (e) {
    body = { id: i.id, status: e.priceMoved ? "skipped" : "failed", note: String(e.message).slice(0, 300) };
  }
  await fetch(`${API}/v1/copy-trading/instructions/status`, { method: "POST", headers: H, body: JSON.stringify(body) });
}
```

If your app was down, list what is still open with `GET /v1/copy-trading/instructions?status=pending` and decide whether to execute or skip each one.

## Stopping

Copying of a trader stops for a user when:

* you switch it off with `"enabled": false` in [POST /v1/copy-trading/settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md),
* the trader is removed from the user's Trade Feed,
* the user revokes your app's access or the scope `trade_feed`, or falls outside your plan's user limit,
* the trader makes their trading private.

Instructions already sent stay readable and can still be reported.

## Dashboard

The instance's [Copy trading](https://docs.cabalspy.xyz/cabalspy-id/dashboard.md#copy-trading) tab shows how many users copy how many traders, the instructions of the last 24 hours by status, the executed volume and the newest instructions.

## Endpoints

| Method | Path | Description |
| - | - | - |
| GET, POST | [/v1/copy-trading/settings](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-settings.md) | A user's copy settings per trader. Switch copying on or off. |
| GET | [/v1/copy-trading/instructions](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-instructions.md) | Instructions of your instance, newest first, by user and status. |
| GET | [/v1/copy-trading/instructions/item](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-instructions.md#get-one-instruction) | One instruction. |
| POST | [/v1/copy-trading/instructions/status](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-status.md) | Report executed, failed or skipped. |
| WS | [/v1/copy-trading/stream](https://docs.cabalspy.xyz/cabalspy-id/copy-trading-stream.md) | Every new instruction, live. |
| Webhook | [copy\_trade.instruction](https://docs.cabalspy.xyz/cabalspy-id/webhooks.md#copy-trading-events) | Every new instruction, pushed to your backend. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.