Skill: player-1101/snaptrade-api (version 1.0.3)

--- name: snaptrade-trading description: > Execute trades and retrieve account data via the SnapTrade API using the snaptrade-python-sdk. Use this skill whenever OpenClaw needs to place a buy or sell order, check account balances, get current positions, retrieve order history, fetch symbol quotes, cancel an open order, or refresh account holdings through SnapTrade. Triggers on any trading action or account data request routed through SnapTrade — even if the user just says "buy X shares", "what's my balance", "check my positions", "cancel that order", or "did my trade go through". Always use this skill when brokerage account interaction of any kind is needed. credentials: - name: SNAPTRADE_CLIENT_ID description: SnapTrade partner client ID. Used to authenticate all API requests. Treat as sensitive. required: true - name: SNAPTRADE_CONSUMER_KEY description: SnapTrade partner consumer key. Used to sign all API requests. Treat as sensitive — do not share or log. required: true - name: SNAPTRADE_USER_ID description: SnapTrade user ID for the connected brokerage user. Grants access to that user's account data and trading permissions. required: true - name: SNAPTRADE_USER_SECRET description: SnapTrade user secret for the connected brokerage user. Acts as a per-user API key. Treat as sensitive — rotate via SnapTrade dashboard if compromised. required: true warnings: - This skill can place real trades and cancel orders on connected brokerage accounts. - Default to requiring user confirmation per trade. Automated mode is supported if explicitly configured — enforce symbol allowlists, notional caps, position limits, and daily loss limits before enabling. - Use paper trading or a low-limit account during testing. - Store credentials in a .env file or secret manager — never hardcode, log, or pass them through untrusted channels. - Rotate SNAPTRADE_USER_SECRET immediately via the SnapTrade dashboard if there is any chance it was exposed. - Prefer a dedicated SnapTrade user with limited brokerage permissions for automated trading rather than using your primary account credentials. compatibility: - python >= 3.8 - snaptrade-python-sdk == 11.0.187 - python-dotenv == 1.2.2 - Run scripts/setup.sh before first use --- # SnapTrade Trading Skill SnapTrade connects OpenClaw to the user's brokerage (IBKR, Questrade, Alpaca, and more) through a unified API. This skill handles all account data retrieval and order execution. For market price data and OHLCV bars, use the `massive-data-feed` skill — SnapTrade does not provide chart data. --- ## Setup Run once before first use: ```bash bash scripts/setup.sh source .venv-snaptrade/bin/activate ``` See `README.md` for full onboarding — how to get credentials, register a user, and connect a brokerage account. --- ## SDK Initialization Always run this first before any other call: ```python import os from snaptrade_client import SnapTrade snaptrade = SnapTrade( consumer_key=os.environ["SNAPTRADE_CONSUMER_KEY"], client_id=os.environ["SNAPTRADE_CLIENT_ID"], ) user_id = os.environ["SNAPTRADE_USER_ID"] user_secret = os.environ["SNAPTRADE_USER_SECRET"] ``` --- ## What to do next — read the right reference file Each reference file covers one domain. Read only what you need. | Task | Reference file | |---|---| | Get accounts, balances, positions, orders, historical value | `references/account-data.md` | | Resolve a ticker to a symbol ID + get quotes | `references/symbol-resolution.md` | | Place an equity trade (limit, market, bracket) | `references/place-orders.md` | | Place an options order (single or multi-leg) | `references/options-trading.md` | | Place a crypto order | `references/crypto-trading.md` | | Cancel an order or refresh holdings | `references/cancel-refresh.md` | | Get historical transactions / activity log | `references/historical-data.md` | --- ## Key constraints to keep in mind - `trade_id` from an impact check expires in **5 minutes** — place the order immediately after getting it, don't do other work in between - SnapTrade does **not** provide OHLCV or price history — use `massive-data-feed` for that - Not all brokerages support trading — some are data-only connections - Always resolve symbol IDs fresh — don't cache them long term, they can change - After any order placement or cancellation, trigger a manual refresh so the account state stays current (see `references/cancel-refresh.md`)

path: references/symbol-resolution.md
content: # Symbol Resolution

Before placing any order, you need the `universal_symbol_id` for the instrument.
This is SnapTrade's internal ID that uniquely identifies a security + exchange combination.

---

## Step 1: Resolve Ticker → universal_symbol_id

```python
response = snaptrade.reference_data.get_symbols_by_ticker(
    query=ticker,  # e.g. "AAPL", "MSFT", "CNR.TO"
)
symbol = response.body[0]
universal_symbol_id = symbol["id"]
```

**Ticker format (Yahoo Finance convention):**
- NYSE / NASDAQ — no suffix: `AAPL`, `MSFT`, `TSLA`
- TSX — `.TO` suffix: `CNR.TO`, `SHOP.TO`
- Other exchanges — check Yahoo Finance for the correct suffix

---

## Step 2: Confirm with a Quote

After resolving the symbol ID, confirm it and get the latest brokerage quote.
Also use this to double-check you have the right instrument before placing an order.

```python
response = snaptrade.trading.get_user_account_quotes(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    symbols=universal_symbol_id,  # comma-separated string for multiple symbols
)
quotes = response.body
# Each quote: symbol (with id), bid_price, ask_price, last_trade_price, bid_size, ask_size
```

---

## Notes

- Quotes from this endpoint may be **delayed** depending on the brokerage
- For real-time prices used in strategy logic (RSI, MACD triggers etc.), use a
  dedicated market data provider such as Polygon.io or Yahoo Finance
- Symbol IDs can change if a stock changes exchanges — do not cache long-term


path: references/crypto-trading.md
content: # Crypto Trading

Assumes SDK is initialized. See SKILL.md.

⚠️ Crypto trading requires a brokerage connection that supports crypto. Some
brokerages also require re-authorization with trading permissions enabled.

---

## Step 1: Get Tradable Crypto Pairs

Returns all cryptocurrency pairs available to trade in the account.
Omit `base` and `quote` to get the full list.

```python
response = snaptrade.trading.search_cryptocurrency_pair_instruments(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    base="BTC",   # optional — base currency e.g. "BTC", "ETH"
    quote="USD",  # optional — quote currency e.g. "USD", "USDC"
)
pairs = response.body["items"]
# Each pair: symbol (e.g. "BTC-USD"), base, quote, increment
```

Use the `symbol` field (e.g. `"BTC-USD"`) when placing crypto orders.

---

## Step 2: Get Crypto Pair Quote

Returns the current bid/ask for a crypto pair.

```python
response = snaptrade.trading.get_cryptocurrency_pair_quote(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    instrument=crypto_symbol,  # e.g. "BTC-USD"
)
quote = response.body
# Fields: bid_price, ask_price, last_trade_price
```

---

## Step 3: Preview Crypto Order (Optional)

Simulates the order before placing. Returns estimated fees and impact.

```python
response = snaptrade.trading.preview_crypto_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    instrument={
        "symbol": "BTC-USD",
        "type": "CRYPTOCURRENCY_PAIR",
    },
    side="BUY",           # "BUY" or "SELL"
    type="MARKET",        # see order types below
    time_in_force="GTC",
    amount="0.01",        # amount of base currency (e.g. 0.01 BTC)
)
preview = response.body
```

---

## Step 4: Place Crypto Order

Places the order directly — no separate "place after preview" step.

```python
response = snaptrade.trading.place_crypto_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    instrument={
        "symbol": "BTC-USD",
        "type": "CRYPTOCURRENCY_PAIR",
    },
    side="BUY",                     # "BUY" or "SELL"
    type="LIMIT",                   # see order types below
    time_in_force="GTC",            # "GTC", "FOK", "IOC", "GTD"
    amount="0.01",                  # base currency amount
    limit_price="95000.00",         # required for LIMIT, STOP_LOSS_LIMIT, TAKE_PROFIT_LIMIT
    stop_price="90000.00",          # required for STOP_LOSS_*, TAKE_PROFIT_*
    post_only=False,                # LIMIT only — if True, rejects orders that would fill immediately (avoids taker fees)
    # expiration_date="2026-06-01T00:00:00Z"  # required if time_in_force is GTD
)
order = response.body
brokerage_order_id = order["brokerage_order_id"]
```

### Crypto Order Types

| Type | Description |
|---|---|
| `MARKET` | Execute immediately at market price |
| `LIMIT` | Execute at specified price or better |
| `STOP_LOSS_MARKET` | Market order triggered at stop price |
| `STOP_LOSS_LIMIT` | Limit order triggered at stop price |
| `TAKE_PROFIT_MARKET` | Market order triggered when profit target hit |
| `TAKE_PROFIT_LIMIT` | Limit order triggered when profit target hit |

### Time in Force (Crypto)

| Value | Meaning |
|---|---|
| `GTC` | Good Till Canceled |
| `FOK` | Fill or Kill |
| `IOC` | Immediate or Cancel |
| `GTD` | Good Till Date (requires `expiration_date`) |

---

## Cancel a Crypto Order

Same cancel endpoint as equities:

```python
snaptrade.trading.cancel_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    brokerage_order_id=brokerage_order_id,
)
```

---

## Notes

- `amount` is in units of the **base** currency (e.g. BTC in BTC-USD)
- Tickers vary by exchange — e.g. BTC may be `BTC`, `XBT`, or `XXBT` depending
  on the brokerage. Always use `get_crypto_pairs` to find the correct symbol
- Crypto markets trade 24/7 — no market hours restriction


path: references/options-trading.md
content: # Options Trading

Assumes SDK is initialized. See SKILL.md.

⚠️ Option trading is only supported on certain brokerages. Check the SnapTrade
broker support matrix before attempting option orders.

---

## Get Option Positions

Returns all open option positions in the account.

```python
response = snaptrade.options.list_option_holdings(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
positions = response.body
# Each position: option_symbol (ticker, option_type, strike_price,
#   expiration_date, is_mini_option, underlying_symbol), units, price
```

---

## Get Option Quote (BETA)

Returns a quote for a specific option contract.

```python
response = snaptrade.trading.get_user_account_option_quotes(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    option_symbol_id=option_symbol_id,  # UUID from option position or search
)
quote = response.body
```

---

## Check Option Order Impact (BETA)

Simulates an option order before placing it. Returns estimated cash impact.

```python
response = snaptrade.trading.get_option_impact(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    option_symbol_id=option_symbol_id,
    action="BUY_TO_OPEN",   # See actions below
    units=1,
    order_type="Limit",
    time_in_force="Day",
    price=2.50,             # per share (x100 per contract)
)
```

---

## Place Option Order (Multi-Leg)

Places a single or multi-leg option order using OCC symbol format.

**OCC symbol format:** `AAPL  261218C00240000`
= underlying + expiry (YYMMDD) + type (C/P) + strike * 1000 (8 digits)

```python
response = snaptrade.trading.place_mleg_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    order_type="LIMIT",         # "MARKET", "LIMIT", "STOP_LOSS_MARKET",
                                # "STOP_LOSS_LIMIT"
    time_in_force="Day",        # "Day", "GTC", "FOK", "IOC"
    limit_price="2.50",         # required for LIMIT, STOP_LOSS_LIMIT
    stop_price="2.00",          # required for STOP_LOSS_MARKET, STOP_LOSS_LIMIT
    price_effect="DEBIT",       # "CREDIT", "DEBIT", "EVEN" — for limit orders
    legs=[
        {
            "instrument": {
                "symbol": "AAPL  261218C00240000",  # OCC symbol
                "instrument_type": "OPTION",
            },
            "action": "BUY_TO_OPEN",  # See actions below
            "units": 1,
        }
    ],
)
order = response.body
brokerage_order_id = order["brokerage_order_id"]
```

### Option Actions

| Action | Meaning |
|---|---|
| `BUY_TO_OPEN` | Buy to open a new long position |
| `BUY_TO_CLOSE` | Buy to close an existing short position |
| `SELL_TO_OPEN` | Sell to open a new short position |
| `SELL_TO_CLOSE` | Sell to close an existing long position |

### Multi-Leg Example (Vertical Spread)

```python
legs=[
    {
        "instrument": {"symbol": "AAPL  261218C00240000", "instrument_type": "OPTION"},
        "action": "BUY_TO_OPEN",
        "units": 1,
    },
    {
        "instrument": {"symbol": "AAPL  261218C00250000", "instrument_type": "OPTION"},
        "action": "SELL_TO_OPEN",
        "units": 1,
    },
]
```

---

## Notes

- Option prices are per share — multiply by 100 for cost per contract
  (10 for mini options where `is_mini_option=True`)
- `trade_id` from impact check (BETA) expires in 5 minutes
- Not all brokerages support multi-leg orders — single-leg is more widely supported


path: references/account-data.md
content: # Account Data

Assumes SDK is initialized and `user_id` / `user_secret` are set. See SKILL.md.

---

## List All Accounts

Returns all connected brokerage accounts. `account_id` is required for all
subsequent calls — always fetch this first.

```python
response = snaptrade.account_information.list_user_accounts(
    user_id=user_id,
    user_secret=user_secret,
)
accounts = response.body
# Each account: id, name, number, institution_name, meta.type
account_id = accounts[0]["id"]
```

---

## Get Account Detail

Returns account name, number, total market value, and sync status.

```python
response = snaptrade.account_information.get_user_account_details(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
detail = response.body
# Fields: id, name, number, institution_name, meta.type, sync_status, balance
```

---

## Get Balances

Returns cash holdings and buying power, broken out by currency.

```python
response = snaptrade.account_information.get_user_account_balance(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
balances = response.body
# Each balance: {currency: {code, name}, cash: float}
```

---

## Get Positions

Returns stock/ETF/crypto/mutual fund positions. Does not include options.

```python
response = snaptrade.account_information.get_user_account_positions(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
positions = response.body
# Each position: symbol, units, price, average_purchase_price, open_pnl, currency
# units > 0 = long, units < 0 = short
```

## Get Option Positions

```python
response = snaptrade.options.list_option_holdings(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
option_positions = response.body
```

---

## Get All Holdings at Once

Returns positions + balances + orders + total value in one call.
Slower than individual endpoints — use only when you need everything.

```python
response = snaptrade.account_information.get_user_holdings(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
holdings = response.body
# Keys: account, balances, positions, orders, total_value
```

---

## Get Orders

Returns all orders including pending, filled, and cancelled.

```python
response = snaptrade.account_information.get_user_account_orders(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
orders = response.body
# Each order: brokerage_order_id, status, action, units, price, symbol, time_in_force
```

---

## Get Recent Orders (Last 24 Hours)

Returns only orders placed in the last 24 hours. Faster than the full order list.

```python
response = snaptrade.account_information.get_user_account_recent_orders(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
)
recent_orders = response.body
```

---

## Get Single Order Detail

Returns full detail for a specific order by its brokerage order ID.

```python
response = snaptrade.account_information.get_user_account_order_detail(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    brokerage_order_id=brokerage_order_id,
)
order = response.body
```

---

## Get Historical Account Total Value (BETA)

Returns the total account value over time — useful for performance tracking.

```python
response = snaptrade.account_information.get_account_balance_history(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    start_date="2024-01-01",   # optional
    end_date="2024-12-31",     # optional
)
history = response.body
# Each entry: date, total_value, currency
```

---

## Data Freshness

Data is cached and refreshed once daily by default. For real-time data, either
use a paid API key or trigger a manual refresh — see `references/cancel-refresh.md`.


path: references/historical-data.md
content: # Historical Data (Activities / Transactions)

Activities are the historical record of all transactions that resulted in a
position change — buys, sells, dividends, contributions, withdrawals, etc.

**Activities vs Orders:**
- **Orders** — includes pending and cancelled orders, granular timestamps, short history
- **Activities** — completed position-changing transactions only, years of history

For most historical analysis, use activities.

---

## Fetch Activities

Paginated. Default page size: 1000. Returns in reverse chronological order.

```python
response = snaptrade.transactions_and_reporting.get_activities(
    user_id=user_id,
    user_secret=user_secret,
    accounts=account_id,       # single account_id string
    start_date="2024-01-01",   # optional, ISO date string YYYY-MM-DD
    end_date="2024-12-31",     # optional
)
activities = response.body
```

### Multiple accounts

```python
response = snaptrade.transactions_and_reporting.get_activities(
    user_id=user_id,
    user_secret=user_secret,
    accounts=",".join([account_id_1, account_id_2]),
)
```

---

## Activity Object Fields

| Field | Description |
|---|---|
| `type` | Transaction type: `BUY`, `SELL`, `DIVIDEND`, `CONTRIBUTION`, `WITHDRAWAL`, etc. |
| `amount` | Cash impact. Positive = cash gained. Negative = cash spent. |
| `units` | Position impact. Positive = shares gained (BUY). Negative = shares lost (SELL). |
| `price` | Price per share at time of transaction. |
| `symbol` | Universal symbol object (ticker, exchange, currency). |
| `currency` | Currency of the transaction. |
| `trade_date` | ISO-8601 datetime of the transaction. |
| `description` | Human-readable description from the brokerage. |

**Interpreting amount/units:**
- BUY: `units > 0`, `amount < 0` (gained shares, spent cash)
- SELL: `units < 0`, `amount > 0` (lost shares, gained cash)
- DIVIDEND: `units = 0`, `amount > 0` (cash received, no position change)

---

## History Depth

History depth varies by brokerage. Examples:
- Schwab: up to 4 years
- Most others: full account history since inception

Check the SnapTrade Broker Support Matrix for specifics on each brokerage.

---

## Pagination

If an account has a very large number of transactions, the initial sync may
take time. If no activities are returned immediately after connecting, retry
after a short delay — the initial indexing may still be in progress.


path: references/place-orders.md
content: # Placing Orders

**Confirmation mode — two supported patterns:**

- **Manual mode (default):** Before every order or cancellation, show the full
  order details (symbol, action, quantity, order type, price) and wait for the
  user to confirm. Use this unless the user has explicitly opted into automated mode.

- **Automated mode:** Only use if you have explicitly configured OpenClaw for
  autonomous/algorithmic trading AND have all of the following enforced in your
  decision engine before any order endpoint is called:
  - **Symbol allowlist** — only trade pre-approved symbols
  - **Notional cap** — maximum dollar value per order (e.g. $500/trade)
  - **Position limit** — maximum units or total exposure per symbol
  - **Daily loss limit** — halt trading if daily P&L exceeds a threshold
  - **Paper trading first** — validate the full automated flow on a paper/sandbox
    account before connecting a live brokerage

  Automated mode without these guardrails is not recommended and may result in
  unintended financial loss.

The checked order flow (impact check → place) is the recommended default in
both modes. The direct/force order flow is available in either mode.

Assumes `universal_symbol_id` is already resolved. If not, see `references/symbol-resolution.md`.

---

## Order Parameters Reference

| Parameter | Options |
|---|---|
| `action` | `"BUY"`, `"SELL"` |
| `order_type` | `"Market"`, `"Limit"`, `"Stop"`, `"StopLimit"` |
| `time_in_force` | `"Day"`, `"GTC"`, `"FOK"`, `"IOC"` |
| `price` | Required for `Limit` and `StopLimit` |
| `stop` | Required for `Stop` and `StopLimit` |
| `units` | Number of shares (decimal ok for fractional) |
| `notional_value` | Dollar amount — use instead of `units` for dollar-based orders |

---

## Flow 1: Checked Order (Recommended)

Simulates the order first, then places it. Safer — confirms cash impact before execution.
**`trade_id` expires after 5 minutes. Place immediately after impact check.**

### Step 1 — Check Impact

```python
response = snaptrade.trading.get_order_impact(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    universal_symbol_id=universal_symbol_id,
    action="BUY",
    order_type="Limit",
    time_in_force="Day",
    price=150.00,
    units=10,
)
trade_id = response.body["trade"]["id"]
remaining_cash = response.body["trade_impacts"][0]["remaining_cash"]
estimated_commissions = response.body["trade_impacts"][0]["estimated_commissions"]
```

### Step 2 — Place Checked Order

```python
response = snaptrade.trading.place_order(
    trade_id=trade_id,
    user_id=user_id,
    user_secret=user_secret,
)
order = response.body
brokerage_order_id = order["brokerage_order_id"]
status = order["status"]
```

---

## Flow 2: Direct Order (Force Order)

Skips the impact check. Available in both manual and automated mode.
In manual mode, confirm order details with the user before calling.
In automated mode, ensure your decision engine has validated the order
(symbol, side, quantity, price, notional limits) before calling.

```python
response = snaptrade.trading.place_force_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    universal_symbol_id=universal_symbol_id,
    action="BUY",
    order_type="Market",
    time_in_force="Day",
    units=10,
    # price=150.00       # include for Limit/StopLimit
    # stop=145.00        # include for Stop/StopLimit
    # notional_value=1500.00  # use instead of units for dollar-based
)
order = response.body
brokerage_order_id = order["brokerage_order_id"]
```

---

## Flow 3: Bracket Order

Places an entry order with an automatic stop loss and take profit in one call.

```python
response = snaptrade.trading.place_bracket_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    universal_symbol_id=universal_symbol_id,
    action="BUY",
    units=10,
    entry_price=150.00,
    stop_loss_price=145.00,
    take_profit_price=160.00,
    time_in_force="GTC",
)
order = response.body
```

---

## Error Handling

Common 400 errors:
- `Markets are not open`
- `Not enough cash to place trade`
- `Exchange does not support market orders`
- `Invalid symbol for this brokerage`

```python
try:
    response = snaptrade.trading.place_force_order(...)
except Exception as e:
    # Log and surface to decision engine for retry or skip logic
    raise
```

---

## After Placing

Always trigger a manual holdings refresh after placing an order so account
state reflects the new position. See `references/cancel-refresh.md`.


path: references/cancel-refresh.md
content: # Cancel Orders & Refresh Holdings

---

## Cancel an Order

Cancels an open order. Works for all asset types (equity, options, crypto).
Only open/pending orders can be cancelled — filled or already-cancelled orders will be rejected.

```python
response = snaptrade.trading.cancel_order(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
    brokerage_order_id=brokerage_order_id,  # from place order response or order list
)
```

To get `brokerage_order_id` for an open order, fetch orders first:
```python
orders = snaptrade.account_information.get_user_account_orders(
    user_id=user_id,
    user_secret=user_secret,
    account_id=account_id,
).body
open_orders = [o for o in orders if o["status"] == "PENDING"]
```

---

## Manual Holdings Refresh

Triggers a fresh sync of positions, balances, and orders from the brokerage.
Call this after placing or cancelling any order to ensure account state is current.

```python
# Step 1: get the authorization (connection) ID
connections = snaptrade.connections.list_brokerage_authorizations(
    user_id=user_id,
    user_secret=user_secret,
).body
authorization_id = connections[0]["id"]

# Step 2: trigger refresh
snaptrade.connections.refresh_brokerage_authorization(
    authorization_id=authorization_id,
    user_id=user_id,
    user_secret=user_secret,
)
```

**Note:** If the user has multiple connections (multiple brokerages), iterate
over all connections and refresh each one, or target only the relevant one by
matching `connection["brokerage"]["slug"]` to the brokerage you traded on.

---

## Connection Health Check

Before trading, verify the connection is not disabled:

```python
connections = snaptrade.connections.list_brokerage_authorizations(
    user_id=user_id,
    user_secret=user_secret,
).body

for conn in connections:
    if conn["disabled"]:
        # Connection is broken — cannot access live data or place trades
        # User must re-authenticate via the Connection Portal
        print(f"Connection {conn['id']} is disabled: {conn['brokerage']['name']}")
```


path: README.md
content: # SnapTrade Trading Skill — Setup Guide

This skill lets OpenClaw trade and retrieve account data through the
[SnapTrade API](https://docs.snaptrade.com), which connects to your brokerage
(IBKR, Alpaca, Questrade, and more).

Before using this skill you need four credentials. This guide walks you through
getting them and putting them in the right place.

---

## Step 1 — Create a SnapTrade Account and Get Your API Key

Your `SNAPTRADE_CLIENT_ID` and `SNAPTRADE_CONSUMER_KEY` come from the SnapTrade
partner dashboard. These identify your application to the API.

1. Go to [https://dashboard.snaptrade.com](https://dashboard.snaptrade.com) and create a free account
2. Verify your email
3. Go to **API Key** in the dashboard and generate your free API key
4. You will see two values — copy both:
   - **Client ID** → this is your `SNAPTRADE_CLIENT_ID`
   - **Consumer Key** → this is your `SNAPTRADE_CONSUMER_KEY`

> ⚠️ Your `consumerKey` is sensitive. Never share it, commit it to git, or put
> it in any logs. If it is compromised, regenerate it from the dashboard.

Full guide: https://docs.snaptrade.com/docs/getting-started

---

## Step 2 — Register a SnapTrade User and Get Your User Credentials

SnapTrade uses a per-user credentials system. Even if you are the only person
using this skill, you still need to register a "user" under your API key.
This gives you a `SNAPTRADE_USER_ID` and `SNAPTRADE_USER_SECRET`.

You can do this with a quick Python script once your API key is set up:

```python
from snaptrade_client import SnapTrade

snaptrade = SnapTrade(
    consumer_key="YOUR_CONSUMER_KEY",
    client_id="YOUR_CLIENT_ID",
)

# Pick any unique string as your user ID — do NOT use an email address
response = snaptrade.authentication.register_snap_trade_user(
    body={"userId": "my-openclaw-user"}
)

print("userId:", response.body["userId"])
print("userSecret:", response.body["userSecret"])
```

- **userId** → this is your `SNAPTRADE_USER_ID` (you chose this string)
- **userSecret** → this is your `SNAPTRADE_USER_SECRET` (randomly generated by SnapTrade)

> ⚠️ Save the `userSecret` immediately — SnapTrade will not show it again.
> If you lose it, rotate it using the dashboard or the rotate endpoint.

API reference: https://docs.snaptrade.com/reference/Authentication/Authentication_registerSnapTradeUser

---

## Step 3 — Connect Your Brokerage Account

After registering a user, you need to connect a brokerage account to it.
This is done through the SnapTrade Connection Portal — a secure login flow
hosted by SnapTrade.

```python
response = snaptrade.authentication.login_snap_trade_user(
    user_id="my-openclaw-user",
    user_secret="YOUR_USER_SECRET",
    body={"connectionType": "trade"},  # "trade" enables order placement
)

print("Open this URL in your browser:")
print(response.body["redirectURI"])
```

Open the URL, log in to your brokerage, and authorize SnapTrade. Once done,
your brokerage account is connected and ready to use.

> 💡 For testing without a live account, select **Alpaca Paper** in the
> Connection Portal. This gives you a free simulated trading account with no
> real money at risk.

---

## Step 4 — Set Your Credentials

Once you have all four values, add them to a `.env` file in the same directory
where you run OpenClaw:

```
SNAPTRADE_CLIENT_ID=your_client_id_here
SNAPTRADE_CONSUMER_KEY=your_consumer_key_here
SNAPTRADE_USER_ID=my-openclaw-user
SNAPTRADE_USER_SECRET=your_user_secret_here
```

Or export them directly in your shell session:

```bash
export SNAPTRADE_CLIENT_ID=your_client_id_here
export SNAPTRADE_CONSUMER_KEY=your_consumer_key_here
export SNAPTRADE_USER_ID=my-openclaw-user
export SNAPTRADE_USER_SECRET=your_user_secret_here
```

> ⚠️ Never commit your `.env` file to git. Add it to `.gitignore`.

---

## Step 5 — Run Setup

From the skill directory, run:

```bash
bash scripts/setup.sh
```

This creates a virtual environment and installs the pinned SnapTrade SDK.
Then activate it:

```bash
source .venv-snaptrade/bin/activate
```

You are now ready to use the skill.

---

## Credential Summary

| Variable | Where to get it | Sensitive |
|---|---|---|
| `SNAPTRADE_CLIENT_ID` | SnapTrade Dashboard → API Key | Yes |
| `SNAPTRADE_CONSUMER_KEY` | SnapTrade Dashboard → API Key | Yes — never share |
| `SNAPTRADE_USER_ID` | You choose when registering a user | No |
| `SNAPTRADE_USER_SECRET` | Returned when registering a user | Yes — save immediately |

---

## Useful Links

- Getting Started guide: https://docs.snaptrade.com/docs/getting-started
- Full API Reference: https://docs.snaptrade.com/reference
- SnapTrade Dashboard: https://dashboard.snaptrade.com
- Supported brokerages: https://support.snaptrade.com/brokerages-table
- Discord support: https://discord.gg/rkYWBxb8Qu

path: scripts/setup.sh
content: #!/bin/bash
# SnapTrade Trading Skill - Environment Setup
# Run this once before using the skill for the first time.
# Uses a virtual environment to avoid modifying system Python packages.

set -e

VENV_DIR=".venv-snaptrade"

echo "Creating virtual environment at $VENV_DIR ..."
python3 -m venv "$VENV_DIR"

echo "Installing pinned dependencies..."
"$VENV_DIR/bin/pip" install --quiet \
  "snaptrade-python-sdk==11.0.187" \
  "python-dotenv==1.2.2"

echo "Verifying installed versions..."
SNAPTRADE_VER=$("$VENV_DIR/bin/pip" show snaptrade-python-sdk | grep "^Version:" | awk '{print $2}')
DOTENV_VER=$("$VENV_DIR/bin/pip" show python-dotenv | grep "^Version:" | awk '{print $2}')
echo "  snaptrade-python-sdk: $SNAPTRADE_VER (expected 11.0.187)"
echo "  python-dotenv:        $DOTENV_VER (expected 1.2.2)"

if [ "$SNAPTRADE_VER" != "11.0.187" ] || [ "$DOTENV_VER" != "1.2.2" ]; then
  echo ""
  echo "ERROR: Installed versions do not match expected. Aborting."
  echo "For high-security environments, verify package hashes at:"
  echo "  https://pypi.org/project/snaptrade-python-sdk/#files"
  echo "  https://pypi.org/project/python-dotenv/#files"
  exit 1
fi

echo ""
echo "Setup complete. Activate the environment with:"
echo "  source $VENV_DIR/bin/activate"
echo ""
echo "Next: set the following environment variables (or add to a .env file):"
echo "  SNAPTRADE_CLIENT_ID=<your clientId>"
echo "  SNAPTRADE_CONSUMER_KEY=<your consumerKey>"
echo "  SNAPTRADE_USER_ID=<userId for this user>"
echo "  SNAPTRADE_USER_SECRET=<userSecret for this user>"


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)