Skill: ghsmc/kalshi-trading (version 1.0.0)

--- name: kalshi description: Trade on Kalshi prediction markets (CFTC-regulated prediction market exchange). Check portfolio, search markets, analyze orderbooks, place/cancel orders. Use when working with Kalshi API, trading binary contracts, checking market prices, managing positions, or researching prediction markets on politics, economics, crypto, weather, sports, technology events. homepage: https://kalshi.com/docs/api metadata: { "openclaw": { "emoji": "📈", "requires": { "bins": ["node"], "env": ["KALSHI_API_KEY_ID", "KALSHI_PRIVATE_KEY_PATH"] }, "primaryEnv": "KALSHI_API_KEY_ID", }, } --- # Kalshi Trade on Kalshi prediction markets via a self-contained CLI script. Supports market search, portfolio tracking, and full order lifecycle (place/cancel/monitor). ## Quick Start All commands route through a single script. Output is JSON. ## CLI **Primary script:** ```bash {baseDir}/scripts/kalshi-cli.mjs <command> [args...] ``` **Helper script:** ```bash {baseDir}/scripts/quick-analysis.mjs <ticker> ``` Combines market details + orderbook in a single call for fast analysis. ## Commands | Command | Description | | ----------------------------------------- | ------------------------------------------------ | | `balance` | Get account balance (cash + portfolio value) | | `portfolio` | Get balance + all open positions | | `trending` | Top markets by 24h volume | | `search <query>` | Search markets by keyword | | `market <ticker>` | Get details for a single market | | `orderbook <ticker>` | Get bid/ask levels for a market | | `buy <ticker> <yes\|no> <count> <price>` | Place a buy order (price in cents 1-99) | | `sell <ticker> <yes\|no> <count> <price>` | Place a sell order (price in cents 1-99) | | `cancel <orderId>` | Cancel a resting order | | `orders [resting\|canceled\|executed]` | List orders, optionally filtered by status | | `fills [ticker]` | List recent fills, optionally filtered by ticker | ## Examples ```bash # Check balance {baseDir}/scripts/kalshi-cli.mjs balance # See what's trending {baseDir}/scripts/kalshi-cli.mjs trending # Search for markets about bitcoin {baseDir}/scripts/kalshi-cli.mjs search "bitcoin" # Get details on a specific market {baseDir}/scripts/kalshi-cli.mjs market KXBTCD-26FEB14-B55500 # Check orderbook {baseDir}/scripts/kalshi-cli.mjs orderbook KXBTCD-26FEB14-B55500 # Buy 5 YES contracts at 65 cents {baseDir}/scripts/kalshi-cli.mjs buy KXBTCD-26FEB14-B55500 yes 5 65 # Sell 5 YES contracts at 70 cents {baseDir}/scripts/kalshi-cli.mjs sell KXBTCD-26FEB14-B55500 yes 5 70 # Check open orders {baseDir}/scripts/kalshi-cli.mjs orders resting # Check recent fills {baseDir}/scripts/kalshi-cli.mjs fills ``` ## Output All commands output JSON to stdout. Parse the result to present it to the user. ## Trading Rules **Critical:** ALWAYS confirm with the user before placing any buy or sell order. Before executing a trade, show the user: - Ticker - Side (YES or NO) - Count (number of contracts) - Price (in cents) - Total cost = count × price cents = $X.XX **Price format:** - Prices are in cents (1-99) - 65 cents = $0.65 per contract - Minimum: 1 cent, Maximum: 99 cents **Payouts:** - All contracts pay $1.00 (100 cents) if correct, $0 if wrong - YES at 65¢: costs 65¢, pays $1.00 if YES wins → 35¢ profit per contract - NO at 35¢: costs 35¢, pays $1.00 if NO wins → 65¢ profit per contract - YES price + NO price ≈ 100¢ (spreads cause small deviations) **Before selling:** Verify the user holds the position by checking portfolio first ## Reference Documentation - **[setup-guide.md](references/setup-guide.md)** - Getting API credentials, configuration, troubleshooting - **[trading-guide.md](references/trading-guide.md)** - Market mechanics, strategy tips, risk management - **[api-notes.md](references/api-notes.md)** - Technical API details, data formats, common patterns ## Environment Variables Required: - `KALSHI_API_KEY_ID` — your Kalshi API key UUID - `KALSHI_PRIVATE_KEY_PATH` — absolute path to your RSA private key PEM file See [setup-guide.md](references/setup-guide.md) for detailed configuration instructions.

path: references/api-notes.md
content: # Kalshi API Notes

## Authentication

Uses RSA-PSS request signing:

1. Generate message: `${timestampMs}${METHOD}${pathWithPrefix}`
   - Timestamp in milliseconds
   - Method uppercase (GET, POST, DELETE)
   - Path includes /trade-api/v2 prefix
   - Query params stripped before signing

2. Sign with RSA-PSS (SHA-256, MGF1-SHA256, saltlen=digest)

3. Headers:
   - `KALSHI-ACCESS-KEY`: API key UUID
   - `KALSHI-ACCESS-TIMESTAMP`: millisecond timestamp
   - `KALSHI-ACCESS-SIGNATURE`: base64 signature

## Base URLs

- **Production**: `https://api.elections.kalshi.com/trade-api/v2`
- **Demo**: `https://demo-api.kalshi.co/trade-api/v2`

The CLI uses production by default.

## Data Format Conventions

### Monetary Fields

**Balance/Portfolio API** returns integers in CENTS:

- `balance: 42069` = $420.69
- `portfolio_value: 50000` = $500.00

**Market/Order APIs** use FixedPointDollars (strings, 4dp):

- `yes_bid_dollars: "0.6500"` = 65¢
- `no_ask_dollars: "0.3600"` = 36¢
- `last_price_dollars: "0.6200"` = 62¢

### Volume/Quantity Fields

**FixedPointCount** (strings, 2dp):

- `volume_fp: "12345.00"` = 12,345 contracts
- `open_interest_fp: "8500.50"` = 8,500.5 contracts

### Timestamps

ISO 8601 format:

- `close_time: "2025-03-15T16:00:00Z"`
- `created_time: "2025-02-13T22:30:15Z"`

## Common API Patterns

### Pagination

Endpoints that return lists use cursor-based pagination:

```json
{
  "cursor": "eyJza2lwIjoxMDB9",
  "markets": [ ... ]
}
```

Pass `cursor` param to get next page. When `cursor` is empty/null, you've reached the end.

### Status Filters

Markets have lifecycle statuses:

- `open` - Currently tradeable
- `closed` - Trading ended, awaiting resolution
- `settled` - Resolved and paid out

Use `status` param to filter.

### Market Tickers

Format: `{SERIES}-{DATE}-{STRIKE}`

- Series: Event category (e.g., KXBTCD = Bitcoin daily)
- Date: Expiration (e.g., 26FEB14 = Feb 14, 2026)
- Strike: Threshold (e.g., B55500 = "Bitcoin > $55,500")

Always uppercase.

## Rate Limits

Not explicitly documented, but be reasonable:

- Batch requests when possible
- Cache trending/search results (they don't change second-to-second)
- Don't poll order status in tight loops

## Error Handling

API returns structured errors:

```json
{
  "code": "INSUFFICIENT_BALANCE",
  "message": "Account balance too low for this order",
  "service": "orders"
}
```

Common codes:

- `INSUFFICIENT_BALANCE` - Not enough cash
- `MARKET_CLOSED` - Can't trade on closed market
- `INVALID_ORDER` - Bad params (price out of range, etc.)
- `ORDER_NOT_FOUND` - Order ID doesn't exist or already canceled

## Gotchas

1. **Price validation** - Must be 1-99 cents. 0 and 100 are invalid.

2. **Position tracking** - You must track what you own. The API won't prevent you from selling more than you have (it'll just fail).

3. **Market resolution** - Markets can resolve early under certain conditions. Check `can_close_early` field.

4. **Fractional contracts** - Some markets support fractional trading (`fractional_trading_enabled`). Most don't.

5. **Order fills** - Limit orders may partially fill. Check `fill_count` vs `initial_count`.

## Web URLs

Market web pages follow pattern:
`https://kalshi.com/markets/{series_ticker_lowercase}`

Example:

- `event_ticker: "KXBTCD-26FEB14"`
- `series_ticker: "KXBTCD"`
- URL: `https://kalshi.com/markets/kxbtcd`

The CLI doesn't generate these, but they're useful for sharing.


path: references/setup-guide.md
content: # Kalshi API Setup Guide

## Getting API Credentials

1. **Sign up for Kalshi** at https://kalshi.com if you don't have an account

2. **Generate API credentials:**
   - Log in to Kalshi
   - Go to Settings → API
   - Click "Generate API Key"
   - Save your API Key ID (UUID format)
   - Download the private key PEM file

3. **Save the private key:**

   ```bash
   # Move the downloaded key to a secure location
   mv ~/Downloads/kalshi_private_key.pem ~/.openclaw/kalshi_private_key.pem
   chmod 600 ~/.openclaw/kalshi_private_key.pem
   ```

4. **Set environment variables:**

   Add to your shell config file (`~/.zshrc`, `~/.bashrc`, etc.):

   ```bash
   export KALSHI_API_KEY_ID="your-api-key-uuid-here"
   export KALSHI_PRIVATE_KEY_PATH="$HOME/.openclaw/kalshi_private_key.pem"
   ```

   Or in OpenClaw's config:

   ```yaml
   env:
     KALSHI_API_KEY_ID: "your-api-key-uuid-here"
     KALSHI_PRIVATE_KEY_PATH: "/Users/yourusername/.openclaw/kalshi_private_key.pem"
   ```

5. **Reload your shell:**

   ```bash
   source ~/.zshrc  # or ~/.bashrc
   ```

6. **Test the connection:**
   ```bash
   node /path/to/kalshi-cli.mjs balance
   ```

## Security Notes

- **Never commit your private key** to git or share it publicly
- The private key is your authentication - treat it like a password
- API keys can be revoked from the Kalshi settings page
- Use file permissions to restrict access: `chmod 600 kalshi_private_key.pem`

## Demo vs Production

The CLI uses **production** by default: `https://api.elections.kalshi.com`

For testing, Kalshi offers a demo environment at `https://demo-api.kalshi.co`

To use demo:

1. Get separate demo API credentials from Kalshi
2. Modify `BASE_URL` in `kalshi-cli.mjs` line 21

## Rate Limits

Kalshi doesn't publish explicit rate limits, but best practices:

- Don't poll endpoints in tight loops
- Cache results when appropriate (trending markets don't change second-to-second)
- Batch operations when possible

## Troubleshooting

**Error: "Missing env vars"**

- Verify `KALSHI_API_KEY_ID` and `KALSHI_PRIVATE_KEY_PATH` are set
- Check: `echo $KALSHI_API_KEY_ID`

**Error: "ENOENT: no such file or directory"**

- Verify the private key path is correct
- Check: `ls -l $KALSHI_PRIVATE_KEY_PATH`

**Error: "401 Unauthorized" or signature errors**

- Your API key may be invalid or revoked
- Generate a new key pair from Kalshi settings
- Make sure the private key matches the API key ID

**Error: "INSUFFICIENT_BALANCE"**

- You need to deposit funds at https://kalshi.com
- Check balance with: `node kalshi-cli.mjs balance`


path: references/trading-guide.md
content: # Kalshi Trading Guide

## How Kalshi Works

Kalshi is a CFTC-regulated prediction market where users trade on real-world events. Markets are binary YES/NO contracts.

### Pricing Mechanics

- Each contract pays out $1.00 (100¢) if correct, $0 if wrong
- Prices are in cents (1-99)
- Buying YES at 65¢ means you think there's >65% chance the event happens
- Buying NO at 35¢ means you think there's <65% chance (inverse of YES price)
- YES price + NO price ≈ 100¢ (spreads cause small variations)

### Profit Calculation

**YES contract bought at 65¢:**

- Cost: 65¢ per contract
- If YES wins: receive $1.00 → profit = 35¢ per contract
- If NO wins: lose 65¢ per contract

**NO contract bought at 35¢:**

- Cost: 35¢ per contract
- If NO wins: receive $1.00 → profit = 65¢ per contract
- If YES wins: lose 35¢ per contract

## Market Categories

Kalshi covers:

- **Politics** - Elections, policy outcomes, appointments
- **Economics** - Fed rates, inflation, GDP, unemployment
- **Crypto** - Bitcoin, Ethereum prices at specific dates
- **Weather** - Temperature, precipitation, seasonal forecasts
- **Sports** - Game outcomes, championships
- **Technology** - Product launches, company metrics

## Order Types

### Limit Orders

- Specify exact price you're willing to pay
- May not fill immediately if no counterparty at that price
- Good for getting specific entry points

### Market Orders

- Execute immediately at best available price
- Guarantees fill but not price
- Use for liquid markets only

## Trading Strategy Tips

1. **Check liquidity** - Use `orderbook` command to see depth
   - Thin orderbooks = wide spreads, harder to enter/exit
   - Deep orderbooks = tight spreads, easier trading

2. **Understand the clock** - Markets have close times
   - Check `close_time` field
   - Some markets can close early based on resolution

3. **Position sizing** - Calculate risk before trading
   - Max loss = price paid per contract × count
   - Example: 10 YES contracts at 65¢ = $6.50 at risk

4. **Watch volume** - High volume = more informed pricing
   - `volume_24h` shows recent activity
   - Low volume markets may have stale prices

5. **Read the rules** - Check `rules_primary` field
   - Resolution criteria matter
   - Some markets have specific thresholds or conditions

## Common Patterns

### Fade the Extreme

When a market is priced at 95¢ YES or 5¢ NO, small probability shifts create large % moves. Higher risk, higher reward.

### News Trading

Breaking news can move markets before prices adjust. Use web search to correlate events with market movements.

### Mean Reversion

Some markets overreact to noise. If fundamentals haven't changed, consider fading the move.

### Correlation Plays

Related markets often move together (e.g., different Fed rate meetings). Use one to inform the other.

## Risk Management

1. **Diversify** - Don't put everything in one market
2. **Size appropriately** - Risk only what you can afford to lose
3. **Set stops mentally** - Know your exit price before entering
4. **Track P&L** - Use `portfolio` command regularly
5. **Understand resolution** - Make sure you know when/how market resolves

## Fees

Kalshi charges:

- No fees on resting orders (maker)
- Fees on filled orders (taker)
- Check order confirmation for exact fees

## Best Practices

- **Always confirm before trading** - Double-check ticker, side, count, price
- **Verify you're selling what you own** - Check portfolio before selling
- **Use descriptive search terms** - "fed rate march" works better than "rates"
- **Monitor resting orders** - Cancel and replace if price moves away
- **Read market titles carefully** - Similar tickers can have different strike prices or dates


path: scripts/kalshi-cli.mjs
content: #!/usr/bin/env node

/**
 * Kalshi CLI — self-contained, zero-dependency Node.js script (ESM).
 *
 * Usage: node kalshi-cli.mjs <command> [args...]
 *
 * Commands:
 *   balance                          Account balance
 *   portfolio                        Balance + open positions
 *   trending                         Top markets by 24h volume
 *   search <query>                   Search markets by keyword
 *   market <ticker>                  Single market details
 *   orderbook <ticker>               Bid/ask levels
 *   buy <ticker> <yes|no> <count> <price>   Place buy order (price in cents)
 *   sell <ticker> <yes|no> <count> <price>  Place sell order (price in cents)
 *   cancel <orderId>                 Cancel resting order
 *   orders [resting|canceled|executed]  List orders
 *   fills [ticker]                   List recent fills
 *
 * Env vars:
 *   KALSHI_API_KEY_ID         API key UUID
 *   KALSHI_PRIVATE_KEY_PATH   Path to RSA private key PEM
 */

import crypto from "node:crypto";
import fs from "node:fs";

// ---------------------------------------------------------------------------
// Configuration
// ---------------------------------------------------------------------------

const BASE_URL = "https://api.elections.kalshi.com/trade-api/v2";
const TIMEOUT_MS = 15_000;

function loadCredentials() {
  const apiKeyId = process.env.KALSHI_API_KEY_ID;
  const keyPath = process.env.KALSHI_PRIVATE_KEY_PATH;
  if (!apiKeyId || !keyPath) {
    throw new Error("Missing env vars: KALSHI_API_KEY_ID and KALSHI_PRIVATE_KEY_PATH are required");
  }
  const pem = fs.readFileSync(keyPath, "utf-8");
  const privateKey = crypto.createPrivateKey(pem);
  return { apiKeyId, privateKey };
}

// ---------------------------------------------------------------------------
// RSA-PSS Signing
// ---------------------------------------------------------------------------

function signRequest(credentials, method, path, timestampMs) {
  const ts = String(timestampMs ?? Date.now());
  // Strip query params for signing
  const idx = path.indexOf("?");
  const cleanPath = idx === -1 ? path : path.substring(0, idx);
  const message = `${ts}${method.toUpperCase()}${cleanPath}`;

  const signature = crypto.sign("sha256", Buffer.from(message, "utf-8"), {
    key: credentials.privateKey,
    padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
    saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
  });

  return {
    "KALSHI-ACCESS-KEY": credentials.apiKeyId,
    "KALSHI-ACCESS-TIMESTAMP": ts,
    "KALSHI-ACCESS-SIGNATURE": signature.toString("base64"),
  };
}

// ---------------------------------------------------------------------------
// HTTP Client
// ---------------------------------------------------------------------------

async function request(credentials, method, endpoint, params, body) {
  const url = new URL(`${BASE_URL}${endpoint}`);
  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (value !== undefined && value !== null) {
        url.searchParams.set(key, String(value));
      }
    }
  }

  const signingPath = url.pathname;
  const authHeaders = signRequest(credentials, method.toUpperCase(), signingPath);

  const headers = {
    ...authHeaders,
    Accept: "application/json",
  };

  const init = {
    method: method.toUpperCase(),
    headers,
    signal: AbortSignal.timeout(TIMEOUT_MS),
  };

  if (body !== undefined) {
    headers["Content-Type"] = "application/json";
    init.body = JSON.stringify(body);
  }

  const res = await fetch(url.toString(), init);

  if (!res.ok) {
    let errorBody;
    try {
      errorBody = await res.json();
    } catch {
      errorBody = await res.text();
    }
    const msg =
      typeof errorBody === "string" ? errorBody : `${errorBody.code}: ${errorBody.message}`;
    throw new Error(`Kalshi API ${res.status} on ${endpoint}: ${msg}`);
  }

  return res.json();
}

// ---------------------------------------------------------------------------
// API Methods
// ---------------------------------------------------------------------------

async function getBalance(creds) {
  const raw = await request(creds, "GET", "/portfolio/balance");
  return {
    balance_cents: raw.balance,
    portfolio_value_cents: raw.portfolio_value,
    balance_dollars: (raw.balance / 100).toFixed(2),
    portfolio_value_dollars: (raw.portfolio_value / 100).toFixed(2),
  };
}

async function getPositionsAll(creds) {
  const allMarket = [];
  const allEvent = [];
  let cursor;
  do {
    const params = { limit: 1000 };
    if (cursor) params.cursor = cursor;
    const res = await request(creds, "GET", "/portfolio/positions", params);
    allMarket.push(...(res.market_positions || []));
    allEvent.push(...(res.event_positions || []));
    cursor = res.cursor || undefined;
  } while (cursor);
  return { market_positions: allMarket, event_positions: allEvent };
}

async function getPortfolio(creds) {
  const [balance, positions] = await Promise.all([getBalance(creds), getPositionsAll(creds)]);
  return { ...balance, positions: positions.market_positions };
}

async function getEvents(creds, params) {
  return request(creds, "GET", "/events", params);
}

async function getTrendingMarkets(creds, limit = 15) {
  const allEntries = [];
  let cursor;
  let page = 0;
  do {
    const params = {
      status: "open",
      with_nested_markets: true,
      limit: 200,
    };
    if (cursor) params.cursor = cursor;
    const data = await getEvents(creds, params);
    for (const event of data.events || []) {
      for (const m of event.markets || []) {
        allEntries.push({
          market: m,
          series_ticker: event.series_ticker,
          vol24h: parseFloat(m.volume_24h_fp || "0") || 0,
        });
      }
    }
    cursor = data.cursor || undefined;
    page++;
  } while (cursor && page < 3);

  // Deduplicate: pick highest vol24h market per event
  const byEvent = new Map();
  for (const entry of allEntries) {
    const key = entry.market.event_ticker;
    if (!byEvent.has(key) || entry.vol24h > byEvent.get(key).vol24h) {
      byEvent.set(key, entry);
    }
  }

  const sorted = [...byEvent.values()].sort((a, b) => b.vol24h - a.vol24h);
  return sorted.slice(0, limit).map((e) => ({
    ticker: e.market.ticker,
    event_ticker: e.market.event_ticker,
    title: e.market.title,
    subtitle: e.market.subtitle,
    yes_bid: e.market.yes_bid_dollars,
    yes_ask: e.market.yes_ask_dollars,
    last_price: e.market.last_price_dollars,
    volume_24h: e.market.volume_24h_fp,
    volume: e.market.volume_fp,
    close_time: e.market.close_time,
  }));
}

async function searchMarkets(creds, query, limit = 20) {
  const data = await getEvents(creds, {
    limit: 200,
    with_nested_markets: true,
    status: "open",
  });

  const queryLower = query.toLowerCase();
  const queryTerms = queryLower.split(/\s+/).filter((t) => t.length > 0);
  const scored = [];

  for (const event of data.events || []) {
    const eventTitle = (event.title || "").toLowerCase();
    for (const m of event.markets || []) {
      const title = (m.title || "").toLowerCase();
      const subtitle = (m.subtitle || "").toLowerCase();
      const ticker = (m.ticker || "").toLowerCase();
      const combined = `${eventTitle} ${title} ${subtitle} ${ticker}`;

      const matchCount = queryTerms.filter((t) => combined.includes(t)).length;
      if (matchCount === 0) continue;

      let score = matchCount / queryTerms.length;
      if (matchCount === queryTerms.length) score += 1;
      if (combined.includes(queryLower)) score += 1;

      scored.push({ market: m, score });
    }
  }

  const volumeOf = (m) => parseFloat(m.volume_fp || "0") || 0;
  scored.sort((a, b) => b.score - a.score || volumeOf(b.market) - volumeOf(a.market));

  return scored.slice(0, limit).map((s) => ({
    ticker: s.market.ticker,
    event_ticker: s.market.event_ticker,
    title: s.market.title,
    subtitle: s.market.subtitle,
    yes_bid: s.market.yes_bid_dollars,
    yes_ask: s.market.yes_ask_dollars,
    last_price: s.market.last_price_dollars,
    volume_24h: s.market.volume_24h_fp,
    close_time: s.market.close_time,
  }));
}

async function getMarket(creds, ticker) {
  const data = await request(creds, "GET", `/markets/${encodeURIComponent(ticker)}`);
  const m = data.market;
  return {
    ticker: m.ticker,
    event_ticker: m.event_ticker,
    title: m.title,
    subtitle: m.subtitle,
    status: m.status,
    result: m.result,
    yes_bid: m.yes_bid_dollars,
    yes_ask: m.yes_ask_dollars,
    no_bid: m.no_bid_dollars,
    no_ask: m.no_ask_dollars,
    last_price: m.last_price_dollars,
    volume: m.volume_fp,
    volume_24h: m.volume_24h_fp,
    open_interest: m.open_interest_fp,
    liquidity: m.liquidity_dollars,
    close_time: m.close_time,
    rules_primary: m.rules_primary,
  };
}

async function getOrderbook(creds, ticker) {
  const data = await request(creds, "GET", `/markets/${encodeURIComponent(ticker)}/orderbook`, {
    depth: 10,
  });
  const book = data.orderbook;
  const parseLevel = (arr) => {
    if (!Array.isArray(arr)) return [];
    return arr.map((level) => ({ price: level[0], quantity: level[1] }));
  };
  return { ticker, yes: parseLevel(book.yes), no: parseLevel(book.no) };
}

async function createOrder(creds, ticker, action, side, count, priceCents) {
  const body = {
    ticker,
    action,
    side,
    count: parseInt(count, 10),
    type: "limit",
  };
  if (side === "yes") {
    body.yes_price = parseInt(priceCents, 10);
  } else {
    body.no_price = parseInt(priceCents, 10);
  }
  return request(creds, "POST", "/portfolio/orders", undefined, body);
}

async function cancelOrder(creds, orderId) {
  return request(creds, "DELETE", `/portfolio/orders/${encodeURIComponent(orderId)}`);
}

async function getOrders(creds, status) {
  const params = { limit: 100 };
  if (status) params.status = status;
  return request(creds, "GET", "/portfolio/orders", params);
}

async function getFills(creds, ticker) {
  const params = { limit: 100 };
  if (ticker) params.ticker = ticker;
  return request(creds, "GET", "/portfolio/fills", params);
}

// ---------------------------------------------------------------------------
// CLI
// ---------------------------------------------------------------------------

async function main() {
  const args = process.argv.slice(2);
  const command = args[0];

  if (!command) {
    console.error(
      "Usage: kalshi-cli.mjs <command> [args...]\n" +
        "Commands: balance, portfolio, trending, search, market, orderbook, buy, sell, cancel, orders, fills",
    );
    process.exit(1);
  }

  const creds = loadCredentials();

  let result;

  switch (command) {
    case "balance":
      result = await getBalance(creds);
      break;

    case "portfolio":
      result = await getPortfolio(creds);
      break;

    case "trending":
      result = await getTrendingMarkets(creds, parseInt(args[1] || "15", 10));
      break;

    case "search":
      if (!args[1]) {
        console.error("Usage: kalshi-cli.mjs search <query>");
        process.exit(1);
      }
      result = await searchMarkets(creds, args.slice(1).join(" "));
      break;

    case "market":
      if (!args[1]) {
        console.error("Usage: kalshi-cli.mjs market <ticker>");
        process.exit(1);
      }
      result = await getMarket(creds, args[1]);
      break;

    case "orderbook":
      if (!args[1]) {
        console.error("Usage: kalshi-cli.mjs orderbook <ticker>");
        process.exit(1);
      }
      result = await getOrderbook(creds, args[1]);
      break;

    case "buy":
    case "sell": {
      const [, ticker, side, count, price] = args;
      if (!ticker || !side || !count || !price) {
        console.error(`Usage: kalshi-cli.mjs ${command} <ticker> <yes|no> <count> <price>`);
        process.exit(1);
      }
      if (side !== "yes" && side !== "no") {
        console.error('Side must be "yes" or "no"');
        process.exit(1);
      }
      result = await createOrder(creds, ticker, command, side, count, price);
      break;
    }

    case "cancel":
      if (!args[1]) {
        console.error("Usage: kalshi-cli.mjs cancel <orderId>");
        process.exit(1);
      }
      result = await cancelOrder(creds, args[1]);
      break;

    case "orders":
      result = await getOrders(creds, args[1]);
      break;

    case "fills":
      result = await getFills(creds, args[1]);
      break;

    default:
      console.error(`Unknown command: ${command}`);
      process.exit(1);
  }

  console.log(JSON.stringify(result, null, 2));
}

main().catch((err) => {
  console.error(JSON.stringify({ error: err.message }));
  process.exit(1);
});


path: scripts/quick-analysis.mjs
content: #!/usr/bin/env node

/**
 * Quick market analysis - combines market info + orderbook in one call
 * Usage: quick-analysis.mjs <ticker>
 */

import { spawn } from "node:child_process";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const CLI = join(__dirname, "kalshi-cli.mjs");

function exec(args) {
  return new Promise((resolve, reject) => {
    const proc = spawn("node", [CLI, ...args], { stdio: "pipe" });
    let stdout = "";
    let stderr = "";
    proc.stdout.on("data", (data) => (stdout += data.toString()));
    proc.stderr.on("data", (data) => (stderr += data.toString()));
    proc.on("close", (code) => {
      if (code !== 0) {
        reject(new Error(stderr || "Command failed"));
      } else {
        try {
          resolve(JSON.parse(stdout));
        } catch (e) {
          reject(new Error(`Failed to parse JSON: ${e.message}`));
        }
      }
    });
  });
}

async function main() {
  const ticker = process.argv[2];
  if (!ticker) {
    console.error("Usage: quick-analysis.mjs <ticker>");
    process.exit(1);
  }

  try {
    const [market, orderbook] = await Promise.all([
      exec(["market", ticker]),
      exec(["orderbook", ticker]),
    ]);

    const analysis = {
      ticker: market.ticker,
      title: market.title,
      subtitle: market.subtitle,
      status: market.status,
      pricing: {
        yes_bid: market.yes_bid,
        yes_ask: market.yes_ask,
        no_bid: market.no_bid,
        no_ask: market.no_ask,
        last_price: market.last_price,
        spread: (parseFloat(market.yes_ask) - parseFloat(market.yes_bid)).toFixed(4),
      },
      volume: {
        total: market.volume,
        last_24h: market.volume_24h,
        open_interest: market.open_interest,
      },
      liquidity: market.liquidity,
      orderbook: {
        yes_depth: orderbook.yes.length,
        no_depth: orderbook.no.length,
        yes_levels: orderbook.yes.slice(0, 3),
        no_levels: orderbook.no.slice(0, 3),
      },
      timing: {
        close_time: market.close_time,
      },
      rules: market.rules_primary,
    };

    console.log(JSON.stringify(analysis, null, 2));
  } catch (error) {
    console.error(JSON.stringify({ error: error.message }));
    process.exit(1);
  }
}

main();


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