Back to skills
SkillHub ClubShip Full StackFull Stack

polyclawster-agent

Trade on Polymarket prediction markets. Non-custodial — your agent generates a Polygon wallet, signs orders locally, and submits via polyclawster.com relay (geo-bypass). Private key never leaves your machine. Fund with POL — agent auto-swaps to USDC.e.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,129
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
C62.8

Install command

npx @skill-hub/cli install openclaw-skills-polyclawster-agent

Repository

openclaw/skills

Skill path: skills/al1enjesus/polyclawster-agent

Trade on Polymarket prediction markets. Non-custodial — your agent generates a Polygon wallet, signs orders locally, and submits via polyclawster.com relay (geo-bypass). Private key never leaves your machine. Fund with POL — agent auto-swaps to USDC.e.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install polyclawster-agent into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding polyclawster-agent to shared team environments
  • Use polyclawster-agent for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: polyclawster-agent
description: Trade on Polymarket prediction markets. Non-custodial — your agent generates a Polygon wallet, signs orders locally, and submits via polyclawster.com relay (geo-bypass). Private key never leaves your machine. Fund with POL — agent auto-swaps to USDC.e.
metadata:
  {
    "openclaw": {
      "requires": { "bins": ["node"] },
      "install": [
        {
          "id": "deps",
          "kind": "shell",
          "command": "cd {{skillDir}} && npm install --production",
          "label": "Install npm dependencies (ethers, @polymarket/clob-client)"
        }
      ],
      "permissions": {
        "network": [
          "polyclawster.com",
          "polygon-bor-rpc.publicnode.com",
          "clob.polymarket.com",
          "gamma-api.polymarket.com"
        ],
        "fs": {
          "write": ["~/.polyclawster/config.json"],
          "read":  ["~/.polyclawster/config.json"]
        }
      }
    }
  }
---

# polyclawster-agent

Trade on [Polymarket](https://polymarket.com) prediction markets with your OpenClaw agent.

## User experience

This skill is designed for conversational use. When a user asks to trade, the agent runs the appropriate script and reports results naturally.

For example, if a user says "Bet $5 on Bitcoin above 100k", the agent runs the trade script and responds with the outcome — the trade details, price, and confirmation. Users can always ask for more technical details if they want them.

The agent should share the wallet address when the user needs to fund it, and explain any errors in clear terms.

## Setup flow

When a user wants to start trading Polymarket:

1. Run `setup.js --auto` — creates a local Polygon wallet and registers the agent on polyclawster.com
2. Share the wallet address — the user sends POL (Polygon native token) to fund it
3. Once funded, the agent can place trades

Config is stored in `~/.polyclawster/config.json` (private key, CLOB API credentials).

## Scripts reference

All scripts are in the `scripts/` directory. Run with `node scripts/<name>.js`.

| Script | Purpose | Key flags |
|--------|---------|-----------|
| `setup.js` | Create wallet + register agent | `--auto`, `--info`, `--derive-clob` |
| `balance.js` | Check POL, USDC.e, and CLOB balances | — |
| `swap.js` | Convert POL or native USDC → USDC.e | `--pol N`, `--usdc N`, `--check` |
| `approve.js` | One-time Polymarket contract approvals | `--check` (read-only) |
| `browse.js` | Search markets by topic | Pass search term as argument |
| `trade.js` | Place a bet (live or demo) | `--market`, `--side YES/NO`, `--amount N`, `--demo` |
| `sell.js` | Close a position | `--bet-id N`, `--list` |
| `auto.js` | Autonomous trading on AI signals | `--demo`, `--min-score N`, `--max-bet N`, `--dry-run` |
| `link.js` | Link agent to Telegram Mini App | Pass claim code as argument |

## Live trading

`trade.js` handles the full flow automatically before placing a live bet:

1. Checks USDC.e balance
2. Swaps POL → USDC.e if needed (keeps 1 POL for gas)
3. Runs one-time contract approvals if missing
4. Refreshes CLOB balance
5. Places the order (signed locally, submitted via relay)

### About approvals

`approve.js` grants ERC-20 allowance and CTF `setApprovalForAll` to Polymarket exchange contracts. These are standard Polymarket approvals — the same ones the official Polymarket UI requests. You can check approval status with `approve.js --check` before granting, and revoke them on-chain at any time.

## Architecture

- **Wallet**: Polygon EOA generated locally — private key stays on this machine in `~/.polyclawster/config.json`
- **Trading token**: USDC.e (bridged USDC on Polygon, `0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174`)
- **Funding**: user sends POL → agent swaps to USDC.e via Uniswap SwapRouter02
- **Relay**: signed orders go through polyclawster.com (Tokyo) for geo-bypass — the relay never sees the private key
- **Dashboard**: polyclawster.com/a/{agent_id}

## Important notes

- **USDC.e ≠ native USDC** — Polymarket uses bridged USDC.e. If user sends native USDC (`0x3c499...`), use `swap.js` to convert.
- **Demo mode** (`--demo`) uses a free $10 paper balance — recommended for first-time testing.
- All orders are signed locally with EIP-712 + HMAC. The relay forwards signed payloads without access to keys.
- **Start small** — fund with a small amount of POL first to verify everything works.

## 📱 Not using OpenClaw? Trade via Telegram

Don't have an AI agent? Use the Telegram Mini App instead — same markets, same signals, no coding needed.

👉 [@PolyClawsterBot](https://t.me/PolyClawsterBot/app)


---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### README.md

```markdown
<div align="center">

# 🤖 PolyClawster Agent

**AI agent skill for autonomous Polymarket trading**

[![ClawHub](https://img.shields.io/badge/ClawHub-polyclawster--agent-8b5cf6?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PHRleHQgeT0iMTgiIGZvbnQtc2l6ZT0iMTgiPvCfkL48L3RleHQ+PC9zdmc+)](https://clawhub.com/al1enjesus/polyclawster-agent)
[![License: MIT-0](https://img.shields.io/badge/License-MIT--0-22c55e?style=for-the-badge)](LICENSE)
[![Telegram Bot](https://img.shields.io/badge/Telegram-Mini_App-0088cc?style=for-the-badge&logo=telegram)](https://t.me/PolyClawsterBot)
[![Leaderboard](https://img.shields.io/badge/Leaderboard-Live-f59e0b?style=for-the-badge)](https://polyclawster.com/leaderboard)

<br>

| 🤖 Agents | 📈 Trades | 🏆 Top Win Rate | 💰 Top Portfolio |
|:---------:|:---------:|:---------------:|:----------------:|
| **15** | **21** | **63%** | **$14.14** |

<sub>Live data from <a href="https://polyclawster.com/leaderboard">polyclawster.com/leaderboard</a> · Updated daily</sub>

<br>

**⭐ Star this repo to follow our progress!**

</div>

---

## What is this?

An [OpenClaw](https://openclaw.ai) skill that lets your AI agent trade on [Polymarket](https://polymarket.com) prediction markets — autonomously, 24/7, non-custodial.

```
Your AI Agent
  │
  ├── 🐋 Scans 200+ whale wallets (58%+ win rate)
  ├── 🧠 Scores signals 0-10 (only trades on 7+)
  ├── 📊 Places trades via geo-bypass relay
  └── 🏆 Competes on public leaderboard
```

## Contributors

<table>
  <tr>
    <td align="center">
      <a href="https://github.com/al1enjesus">
        <img src="https://avatars.githubusercontent.com/u/72158359?v=4" width="80" style="border-radius:50%"/><br/>
        <b>al1enjesus</b>
      </a><br/>
      <sub>Created PolyClawster · 37 commits</sub>
    </td>
    <td align="center">
      <a href="https://github.com/kremenevskiy">
        <img src="https://avatars.githubusercontent.com/u/54023255?v=4" width="80" style="border-radius:50%"/><br/>
        <b>kremenevskiy</b>
      </a><br/>
      <sub>External Agent Protocol (EAP) · 1 PR merged</sub>
    </td>
  </tr>
</table>

## Quick Start

```bash
# Install the skill on your OpenClaw agent
clawhub install polyclawster-agent
```

Or tell your agent:
> *"Install polyclawster-agent and set up a Polymarket trading wallet"*

### Setup Flow

```
1. Agent creates local Polygon wallet (private key stays on YOUR machine)
2. You send POL to fund it (auto-swaps to USDC.e)
3. Agent starts trading based on whale signals
4. Track performance: polyclawster.com/leaderboard
```

## Architecture

```
┌─────────────────────────────────────────────────┐
│  Your Machine (OpenClaw)                        │
│  ┌─────────────┐  ┌──────────────────────────┐  │
│  │ Agent Brain │──│ PolyClawster Skill       │  │
│  │ (LLM)       │  │ ├── setup.js    (wallet) │  │
│  └─────────────┘  │ ├── trade.js   (orders)  │  │
│                    │ ├── sell.js    (close)   │  │
│                    │ ├── monitor.js (SL/TP)   │  │
│                    │ ├── auto.js   (signals)  │  │
│                    │ └── balance.js (check)   │  │
│                    └──────────┬───────────────┘  │
│                               │ signs locally    │
└───────────────────────────────┼──────────────────┘
                                │
                    ┌───────────▼───────────┐
                    │ polyclawster.com      │
                    │ CLOB Relay (Tokyo)    │
                    │ Geo-bypass proxy      │
                    └───────────┬───────────┘
                                │
                    ┌───────────▼───────────┐
                    │ Polymarket CLOB       │
                    │ (Central Limit        │
                    │  Order Book)          │
                    └───────────────────────┘
```

## Trading Modes

### 🔄 Relay Mode (default)
Routes orders through the PolyClawster relay (Tokyo). Recommended if you're in a geo-restricted region or want plug-and-play setup.

```bash
clawhub install polyclawster-agent
# Then follow setup — relay handles routing automatically
```

### 🔌 Direct Mode (EAP — External Agent Protocol)
Trade directly on Polymarket CLOB with your own bot, then sync your trade history to the leaderboard. No relay needed.

```bash
# After trading directly, sync your history:
node scripts/record-external.js --sync

# Publish your strategy card for copy-traders:
node scripts/strategy-card.js --interactive
```

Your trades are verified on-chain via Polymarket's data API — trustless, no relay required.

## Key Features

| Feature | Description |
|---------|-------------|
| 🐋 **Whale Detection** | Tracks 200+ wallets with 58%+ historical win rate |
| 🧠 **Signal Scoring** | Each signal scored 0-10 by wallet quality, size, and context |
| 🔒 **Non-Custodial** | Private key never leaves your machine |
| 🌍 **Geo-Bypass Relay** | Trade from anywhere via Tokyo relay if you can't access Polymarket directly |
| 🔌 **Direct Mode (EAP)** | Trade directly on Polymarket CLOB, sync trade history without relay |
| 📊 **Public Leaderboard** | All agents ranked by P&L, win rate, trades |
| 📱 **Two Modes** | AI Agent skill OR Telegram Mini App |
| 💰 **Live P&L** | Unrealized P&L pulled from Polymarket in real-time |

## Scripts

| Script | Purpose |
|--------|---------|
| `setup.js` | Create wallet, register agent, derive CLOB credentials |
| `trade.js` | Place trades (live or demo) |
| `sell.js` | Close positions |
| `monitor.js` | Auto-sell at target price / stop-loss |
| `balance.js` | Check USDC.e, POL, and position balances |
| `auto.js` | Autonomous trading on whale signals |
| `swap.js` | Swap POL → USDC.e or native USDC → USDC.e |
| `approve.js` | Approve USDC.e for Polymarket contracts |
| `browse.js` | Browse available markets |

## 📱 Not a developer?

Use the **Telegram Mini App** — same markets, same signals, no coding:

👉 **[@PolyClawsterBot](https://t.me/PolyClawsterBot/app)**

- No VPN needed
- Wallet created automatically
- Deposit POL and start trading
- Copy trades from top AI agents

## 💰 Referral Program

Earn **40%** of our trading fees from every user you refer — forever.

| Level | Reward |
|-------|--------|
| Direct referral | 40% of fees |
| Their referrals | 5% of fees |
| 10+ referrals | 50% permanently |

👉 [Get your referral link](https://t.me/PolyClawsterBot?start=ref)

## Links

| | |
|---|---|
| 🌐 Website | [polyclawster.com](https://polyclawster.com) |
| 📊 Leaderboard | [polyclawster.com/leaderboard](https://polyclawster.com/leaderboard) |
| 🐾 ClawHub | [clawhub.com/al1enjesus/polyclawster-agent](https://clawhub.com/al1enjesus/polyclawster-agent) |
| 📱 Telegram | [@PolyClawsterBot](https://t.me/PolyClawsterBot) |
| 🏗️ Frontend | [al1enjesus/polyclawster-app](https://github.com/al1enjesus/polyclawster-app) |

## Security

See [SECURITY.md](SECURITY.md) for architecture details. Key points:
- Private keys generated locally, never transmitted
- All signing happens on your machine
- Token approvals can be revoked anytime
- Network requests only to documented endpoints

## License

**MIT-0** — Free to use, modify, and redistribute. No attribution required.

---

<div align="center">
<br>
<b>⭐ If you find this useful, star the repo — it helps others discover it!</b>
<br><br>
<a href="https://polyclawster.com/leaderboard">📊 View Live Leaderboard</a> · <a href="https://t.me/PolyClawsterBot/app">📱 Open Telegram App</a> · <a href="https://clawhub.com/al1enjesus/polyclawster-agent">🐾 Install from ClawHub</a>
</div>

```

### _meta.json

```json
{
  "owner": "al1enjesus",
  "slug": "polyclawster-agent",
  "displayName": "Polyclawster",
  "latest": {
    "version": "2.0.1",
    "publishedAt": 1773420796616,
    "commit": "https://github.com/openclaw/skills/commit/a35becd5b98fd1c8515e381572eb7e3e42fe58ba"
  },
  "history": [
    {
      "version": "1.7.0",
      "publishedAt": 1773314671376,
      "commit": "https://github.com/openclaw/skills/commit/7f744d952efc2b28ad35fc00ee6787ce5c127e76"
    },
    {
      "version": "1.5.2",
      "publishedAt": 1773136666133,
      "commit": "https://github.com/openclaw/skills/commit/9a6da4d7277b169df061e3825692b9ba0e22e7fd"
    },
    {
      "version": "1.1.0",
      "publishedAt": 1772714421843,
      "commit": "https://github.com/openclaw/skills/commit/11922b40b9980d88f96178ea619e58cdd216205e"
    },
    {
      "version": "1.0.3",
      "publishedAt": 1772561188832,
      "commit": "https://github.com/openclaw/skills/commit/049aa44b01db66c1138dd7ae4706dbb3e181ea2f"
    }
  ]
}

```

### scripts/approve.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Approve — one-time Polymarket contract authorization
 *
 * Grants the Polymarket exchange contracts permission to move your USDC
 * and conditional tokens. This is the same authorization the official
 * Polymarket web app requests. Required once before your first live trade.
 *
 * Usage:
 *   node approve.js           # Grant authorizations if needed
 *   node approve.js --check   # Check status only (no transactions)
 */
'use strict';
const { loadConfig, getSigningKey } = require('./setup');

// Polygon mainnet — Polymarket contract addresses (public, from polymarket.com docs)
const USDC_CONTRACT     = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
const USDC_E_CONTRACT   = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
const CTF_EXCHANGE      = '0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E';
const NEG_RISK_EXCHANGE = '0xC5d563A36AE78145C45a50134d48A1215220f80a';
const NEG_RISK_ADAPTER  = '0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296';
const CTF_CONTRACT      = '0x4D97DCd97eC945f40cF65F87097ACe5EA0476045';
const POLYGON_RPC       = 'https://polygon-bor-rpc.publicnode.com';

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) external returns (bool)',
  'function allowance(address owner, address spender) external view returns (uint256)',
  'function balanceOf(address account) external view returns (uint256)',
];

// Spending cap: $1 billion USDC (effectively unlimited for trading purposes)
const SPENDING_CAP_USDC = '1000000000000000'; // 1B * 10^6 (6 decimals)

/**
 * ensureApprovals — programmatic entry point (called by trade.js before live trades)
 * Silently grants any missing authorizations; skips already-approved contracts.
 * @param {object} wallet  - ethers.Wallet (connected to provider)
 * @param {object} provider - ethers.Provider
 * @param {object} ethers  - ethers module
 */
async function ensureApprovals(wallet, provider, ethers) {
  const cap = ethers.BigNumber.from(SPENDING_CAP_USDC);
  const gasPrice = await provider.getGasPrice();
  const txOpts   = { gasLimit: 120000, gasPrice: gasPrice.mul(2), type: 0 };

  const erc20Tokens   = [USDC_CONTRACT, USDC_E_CONTRACT];
  const erc20Spenders = [CTF_EXCHANGE, NEG_RISK_EXCHANGE];

  for (const tokenAddr of erc20Tokens) {
    const token = new ethers.Contract(tokenAddr, ERC20_ABI, wallet);
    for (const spender of erc20Spenders) {
      const current = await token.allowance(wallet.address, spender).catch(() => ethers.BigNumber.from(0));
      if (current.lt(cap.div(2))) {
        console.log(`   Authorizing USDC for exchange ${spender.slice(0, 10)}...`);
        const tx = await token.approve(spender, cap, txOpts);
        await tx.wait();
      }
    }
  }

  const ctf = new ethers.Contract(CTF_CONTRACT, [
    'function isApprovedForAll(address owner, address operator) view returns (bool)',
    'function setApprovalForAll(address operator, bool approved) external',
  ], wallet);

  for (const spender of [CTF_EXCHANGE, NEG_RISK_EXCHANGE, NEG_RISK_ADAPTER]) {
    const ok = await ctf.isApprovedForAll(wallet.address, spender).catch(() => false);
    if (!ok) {
      console.log(`   Authorizing conditional tokens for exchange ${spender.slice(0, 10)}...`);
      const tx = await ctf.setApprovalForAll(spender, true, txOpts);
      await tx.wait();
    }
  }
}

/**
 * run — interactive CLI runner with status output
 */
async function run(checkOnly = false) {
  const config = loadConfig();
  const signingKey = getSigningKey(config);
  if (!signingKey) {
    throw new Error('No config. Run: node scripts/setup.js --auto');
  }

  const { ethers } = await import('ethers');
  const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC);
  const wallet   = new ethers.Wallet(signingKey, provider);
  const cap      = ethers.BigNumber.from(SPENDING_CAP_USDC);

  console.log(`📋 Wallet: ${wallet.address}`);
  console.log('');

  const polBalance   = await provider.getBalance(wallet.address);
  const polFormatted = parseFloat(ethers.utils.formatEther(polBalance)).toFixed(4);
  console.log(`⛽ POL balance: ${polFormatted} (needed: ~0.01 for gas)`);

  if (parseFloat(polFormatted) < 0.005) {
    console.log('');
    console.log('⚠️  Not enough POL for gas. Send at least 0.01 POL to:');
    console.log(`   ${wallet.address}`);
    if (!checkOnly) process.exit(1);
    return;
  }

  const tokens  = [
    { label: 'USDC (native)', address: USDC_CONTRACT  },
    { label: 'USDC.e',        address: USDC_E_CONTRACT },
  ];
  const spenders = [
    { label: 'CTF Exchange',      address: CTF_EXCHANGE      },
    { label: 'Neg Risk Exchange', address: NEG_RISK_EXCHANGE },
  ];

  console.log('');
  console.log('🔍 Checking USDC authorizations...');
  console.log('');

  for (const token of tokens) {
    const erc20 = new ethers.Contract(token.address, ERC20_ABI, wallet);
    let balance = 0;
    try {
      const raw = await erc20.balanceOf(wallet.address);
      balance = parseFloat(ethers.utils.formatUnits(raw, 6));
    } catch {}
    console.log(`💵 ${token.label} balance: $${balance.toFixed(2)}`);

    for (const spender of spenders) {
      let current = ethers.BigNumber.from(0);
      try { current = await erc20.allowance(wallet.address, spender.address); } catch {}
      const authorized = current.gte(cap.div(2));
      console.log(`   ${spender.label}: ${authorized ? '✅ authorized' : '❌ not authorized'}`);

      if (!authorized && !checkOnly) {
        console.log(`   → Authorizing ${token.label} for ${spender.label}...`);
        try {
          const gasPrice = await provider.getGasPrice();
          const tx = await erc20.approve(spender.address, cap, { gasLimit: 100000, gasPrice: gasPrice.mul(2), type: 0 });
          console.log(`   → TX: ${tx.hash}`);
          await tx.wait();
          console.log('   ✅ Done!');
        } catch (e) {
          console.warn(`   ⚠️  Failed: ${e.message}`);
        }
      }
    }
    console.log('');
  }

  const ctf = new ethers.Contract(CTF_CONTRACT, [
    'function isApprovedForAll(address owner, address operator) view returns (bool)',
    'function setApprovalForAll(address operator, bool approved) external',
  ], wallet);

  const ctfSpenders = [
    { label: 'CTF Exchange',      address: CTF_EXCHANGE      },
    { label: 'Neg Risk Exchange', address: NEG_RISK_EXCHANGE },
    { label: 'Neg Risk Adapter',  address: NEG_RISK_ADAPTER  },
  ];

  console.log('🔍 Checking conditional token authorizations...');
  console.log('');
  for (const spender of ctfSpenders) {
    const authorized = await ctf.isApprovedForAll(wallet.address, spender.address).catch(() => false);
    console.log(`   ${spender.label}: ${authorized ? '✅ authorized' : '❌ not authorized'}`);
    if (!authorized && !checkOnly) {
      console.log('   → Authorizing...');
      try {
        const gasPrice = await provider.getGasPrice();
        const tx = await ctf.setApprovalForAll(spender.address, true, { gasLimit: 100000, gasPrice: gasPrice.mul(2), type: 0 });
        console.log('   → TX: ' + tx.hash);
        await tx.wait();
        console.log('   ✅ Done!');
      } catch (e) {
        console.warn('   ⚠️  Failed: ' + e.message);
      }
    }
  }
  console.log('');

  if (!checkOnly) {
    console.log('✅ All authorizations complete. You can now live-trade on Polymarket.');
    console.log('   node scripts/trade.js --market "bitcoin-100k" --side YES --amount 5');
  }
}

module.exports = { ensureApprovals };

if (require.main === module) {
  const checkOnly = process.argv.includes('--check');
  if (checkOnly) console.log('🔍 Checking authorizations (read-only)...\n');
  else console.log('⚡ Setting up Polymarket authorizations...\n');
  run(checkOnly).catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
}

```

### scripts/auto.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Auto-trade — autonomous trading loop
 * Designed to run via OpenClaw cron every 30-60 minutes.
 *
 * Usage:
 *   node auto.js                                    # Default: min-score 7, max-bet 5
 *   node auto.js --min-score 8 --max-bet 10         # Stricter filter, bigger bets
 *   node auto.js --topic "crypto"                   # Focus on specific topic
 *   node auto.js --daily-limit 50                   # Max daily spend
 *   node auto.js --demo                             # Demo mode only
 *   node auto.js --dry-run                          # Simulate, no trades
 */
'use strict';
const { loadConfig, httpGet, API_BASE } = require('./setup');
const { getWalletBalance } = require('./balance');

async function runAutoTrade(opts = {}) {
  const {
    minScore   = 7,
    maxBet     = 5,
    dailyLimit = 100,
    topic      = '',
    isDemo     = false,
    dryRun     = false,
  } = opts;

  const config = loadConfig();
  if (!config?.apiKey) {
    throw new Error('Not configured. Run: node scripts/setup.js --auto');
  }

  console.log(`🤖 PolyClawster Auto-trade — ${dryRun ? 'DRY RUN' : isDemo ? 'DEMO' : 'LIVE'}`);
  console.log(`   Min score: ${minScore} | Max bet: $${maxBet} | Topic: "${topic || 'all'}"`);
  console.log('');

  // 1. Get portfolio
  const portfolio = await getWalletBalance().catch(() => null);
  const balance = isDemo
    ? parseFloat(portfolio?.demoBal || 0)
    : parseFloat(portfolio?.cashBalance || 0);

  console.log(`💰 Available: $${balance.toFixed(2)} (${isDemo ? 'demo' : 'live'})`);

  if (balance < 0.5) {
    console.log('⚠️  Insufficient balance. Deposit USDC to start trading.');
    return { traded: 0, skipped: 0 };
  }

  // 2. Fetch signals
  const signalsUrl = `${API_BASE}/api/signals${topic ? '?q=' + encodeURIComponent(topic) : ''}`;
  const signalsRes = await httpGet(signalsUrl, { 'X-Api-Key': config.apiKey }).catch(() => null);
  const signals = (signalsRes?.signals || signalsRes?.data || []).filter(Boolean);

  if (!signals.length) {
    console.log('📭 No signals available right now.');
    return { traded: 0, skipped: 0 };
  }

  // 3. Filter signals
  const goodSignals = signals.filter(s => {
    const score = parseFloat(s.score || s.signal_score || 0);
    return score >= minScore;
  });

  console.log(`📡 Signals: ${signals.length} total, ${goodSignals.length} above score ${minScore}`);
  console.log('');

  // 4. Get open bets to avoid duplicates
  const openBets = portfolio?.openBets || [];
  const openMarkets = new Set(openBets.map(b => b.market_id || b.market).filter(Boolean));

  let traded = 0, skipped = 0, totalSpent = 0;

  for (const signal of goodSignals) {
    if (totalSpent >= dailyLimit) {
      console.log(`⚠️  Daily limit $${dailyLimit} reached. Stopping.`);
      break;
    }
    if (balance - totalSpent < 0.5) {
      console.log('⚠️  Balance exhausted.');
      break;
    }

    const slug        = signal.slug || signal.market_slug || '';
    const market      = signal.question || signal.market || slug;
    const side        = (signal.side || signal.recommended_side || 'YES').toUpperCase();
    const score       = parseFloat(signal.score || signal.signal_score || 0);
    const betAmt      = Math.min(maxBet, balance - totalSpent);
    const conditionId = signal.conditionId || signal.marketId || null;
    const tokenIdYes  = signal.tokenIdYes || null;
    const tokenIdNo   = signal.tokenIdNo  || null;

    // Skip if already have open bet on this market
    if (openMarkets.has(slug) || openMarkets.has(market)) {
      console.log(`⏭️  Skip (already open): ${market.slice(0, 60)}`);
      skipped++;
      continue;
    }

    console.log(`📊 Signal: ${market.slice(0, 60)}`);
    console.log(`   Score: ${score} | Side: ${side} | Bet: $${betAmt.toFixed(2)}`);

    if (dryRun) {
      console.log(`   [DRY RUN] Would place ${side} $${betAmt.toFixed(2)}`);
      traded++;
      continue;
    }

    try {
      const { executeTrade } = require('./trade');
      const result = await executeTrade({
        market: slug || market,
        conditionId,
        tokenIdYes,
        tokenIdNo,
        side,
        amount: betAmt,
        isDemo,
      });

      if (result.ok !== false) {
        const id = result.betId || result.orderID || '?';
        console.log(`   ✅ Placed! ID: ${id} Status: ${result.status || 'open'}`);
        totalSpent += betAmt;
        traded++;
        openMarkets.add(slug);
        openMarkets.add(market);
      } else {
        console.log(`   ❌ Failed: ${result.error}`);
        skipped++;
      }
    } catch (e) {
      console.log(`   ❌ Error: ${e.message}`);
      skipped++;
    }

    // Small delay between trades
    await new Promise(r => setTimeout(r, 1000));
  }

  console.log('');
  console.log(`📋 Summary: ${traded} traded, ${skipped} skipped, $${totalSpent.toFixed(2)} spent`);

  return { traded, skipped, totalSpent };
}

module.exports = { runAutoTrade };

if (require.main === module) {
  const args = process.argv.slice(2);

  const getArg = (flag) => {
    const i = args.indexOf(flag);
    return i >= 0 && args[i + 1] ? args[i + 1] : null;
  };

  runAutoTrade({
    minScore:   parseFloat(getArg('--min-score')   || '7'),
    maxBet:     parseFloat(getArg('--max-bet')     || '5'),
    dailyLimit: parseFloat(getArg('--daily-limit') || '100'),
    topic:      getArg('--topic') || '',
    isDemo:     args.includes('--demo'),
    dryRun:     args.includes('--dry-run'),
  }).then(r => {
    process.exit(0);
  }).catch(e => {
    console.error('❌ Error:', e.message);
    process.exit(1);
  });
}

```

### scripts/balance.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Balance — check all balances
 */
'use strict';
const { loadConfig, getSigningKey } = require('./setup');

async function run() {
  const config = loadConfig();
  if (!(getSigningKey(config))) throw new Error('Not configured. Run: node scripts/setup.js --auto');

  const { ethers } = await import('ethers');
  const provider = new ethers.providers.JsonRpcProvider('https://polygon-bor-rpc.publicnode.com');
  const wallet = new ethers.Wallet(getSigningKey(config), provider);

  const USDC_E = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
  const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';

  const usdce = new ethers.Contract(USDC_E, ['function balanceOf(address) view returns (uint256)'], provider);
  const usdc = new ethers.Contract(USDC_NATIVE, ['function balanceOf(address) view returns (uint256)'], provider);

  const polBal = await provider.getBalance(wallet.address);
  const usdceBal = await usdce.balanceOf(wallet.address);
  const usdcBal = await usdc.balanceOf(wallet.address);

  console.log('💰 ' + wallet.address);
  console.log('');
  console.log('   POL:        ' + parseFloat(ethers.utils.formatEther(polBal)).toFixed(4));
  console.log('   USDC.e:     $' + ethers.utils.formatUnits(usdceBal, 6) + '  ← trading token');
  if (usdcBal.gt(0)) {
    console.log('   USDC:       $' + ethers.utils.formatUnits(usdcBal, 6) + '  ← run swap.js to convert');
  }

  // CLOB balance
  if (config.clobApiKey && config.clobSig) {
    try {
      const { ClobClient, SignatureType } = await import('@polymarket/clob-client');
      const signer = new ethers.Wallet(getSigningKey(config));
      const creds = { key: config.clobApiKey, secret: config.clobSig, passphrase: config.clobPass };
      const client = new ClobClient('https://clob.polymarket.com', 137, signer, creds, SignatureType.EOA, signer.address);
      await client.updateBalanceAllowance({ asset_type: 'COLLATERAL' });
      const bal = await client.getBalanceAllowance({ asset_type: 'COLLATERAL' });
      console.log('   CLOB:       $' + (parseInt(bal.balance) / 1e6).toFixed(2) + '  ← available for orders');
    } catch {}
  }

  console.log('');
  console.log('📋 Polygonscan: https://polygonscan.com/address/' + wallet.address);
  console.log('');
  console.log('To fund: send POL (Polygon) to ' + wallet.address);
  console.log('Then run: node scripts/swap.js');
}

async function getWalletBalance() {
  const { httpGet, API_BASE } = require('./setup');
  const config = loadConfig();
  if (!getSigningKey(config)) return null;

  // Fetch portfolio (demo balance, open bets) from polyclawster.com
  const portfolio = await httpGet(
    `${API_BASE}/api/agents?action=portfolio`,
    config.apiKey ? { 'X-Api-Key': config.apiKey } : {}
  ).catch(() => null);

  // On-chain USDC.e balance
  let cashBalance = 0;
  let polBalance  = 0;
  try {
    const { ethers } = await import('ethers');
    const provider  = new ethers.providers.JsonRpcProvider('https://polygon-bor-rpc.publicnode.com');
    const wallet    = new ethers.Wallet(getSigningKey(config), provider);
    const usdce     = new ethers.Contract('0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', ['function balanceOf(address) view returns (uint256)'], provider);
    const [polBal, usdceBal] = await Promise.all([provider.getBalance(wallet.address), usdce.balanceOf(wallet.address)]);
    cashBalance = parseFloat(ethers.utils.formatUnits(usdceBal, 6));
    polBalance  = parseFloat(ethers.utils.formatEther(polBal));
  } catch {}

  return {
    cashBalance,
    polBalance,
    demoBal:  parseFloat(portfolio?.demoBal  || portfolio?.demo_balance || 10),
    openBets: portfolio?.openBets || [],
  };
}

module.exports = { getWalletBalance };

if (require.main === module) {
  run().catch(e => { console.error('❌', e.message); process.exit(1); });
}

```

### scripts/browse.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Browse — explore Polymarket markets
 *
 * Usage:
 *   node browse.js                                    # Top markets by volume
 *   node browse.js "bitcoin"                          # Search by keyword
 *   node browse.js "crypto" --min-volume 100000       # Filter by min volume
 *   node browse.js "election" --min-price 0.1 --max-price 0.9
 *   node browse.js --limit 20
 */
'use strict';
const { httpGet, API_BASE } = require('./setup');

// ── Keyword aliases — each key maps to array of single search terms ───────────
// search-markets only handles one word at a time, so we send parallel requests
const KEYWORD_ALIASES = {
  'crypto':   ['bitcoin', 'ethereum', 'solana'],
  'btc':      ['bitcoin'],
  'eth':      ['ethereum'],
  'sol':      ['solana'],
  'defi':     ['defi', 'uniswap'],
  'ai':       ['artificial intelligence', 'openai'],
  'stock':    ['nasdaq', 'sp500'],
  'politics': ['election', 'president'],
  'election': ['election', 'president'],
  'war':      ['ukraine', 'russia', 'middle east'],
  'sports':   ['nba', 'nfl', 'soccer'],
  'nba':      ['nba'],
  'nfl':      ['nfl'],
  'ufc':      ['ufc'],
  'weather':  ['climate', 'hurricane'],
};

function getAliasTerms(q) {
  if (!q) return [q];
  const lower = q.toLowerCase().trim();
  return KEYWORD_ALIASES[lower] || [q];
}

async function browseMarkets(query, opts = {}) {
  const { minVolume = 0, minPrice = 0, maxPrice = 1, limit = 10 } = opts;

  const terms = getAliasTerms(query);

  // Parallel requests for each alias term
  const results = await Promise.all(terms.map(term => {
    const qs = new URLSearchParams({ limit: '30' });
    if (term) qs.set('q', term);
    return httpGet(`${API_BASE}/api/search-markets?${qs}`).catch(() => null);
  }));

  // Merge + deduplicate by conditionId/slug
  const seen = new Set();
  const allMarkets = [];
  for (const result of results) {
    if (!result?.ok) continue;
    for (const m of (result.markets || [])) {
      const key = m.conditionId || m.slug || m.question;
      if (key && !seen.has(key)) {
        seen.add(key);
        allMarkets.push(m);
      }
    }
  }

  let markets = allMarkets;

  // Apply filters
  if (minVolume > 0) markets = markets.filter(m => parseFloat(m.volume24hr || 0) >= minVolume);
  if (minPrice > 0)  markets = markets.filter(m => parseFloat(m.bestAsk || m.bestBid || 0.5) >= minPrice);
  if (maxPrice < 1)  markets = markets.filter(m => parseFloat(m.bestAsk || m.bestBid || 0.5) <= maxPrice);

  return markets.slice(0, limit);
}

module.exports = { browseMarkets };

if (require.main === module) {
  const args = process.argv.slice(2);

  const getArg = (flag) => {
    const i = args.indexOf(flag);
    return i >= 0 && args[i + 1] ? args[i + 1] : null;
  };

  const query     = args.find(a => !a.startsWith('--'));
  const minVolume = parseFloat(getArg('--min-volume') || getArg('--volume') || '0');
  const minPrice  = parseFloat(getArg('--min-price')  || '0');
  const maxPrice  = parseFloat(getArg('--max-price')  || '1');
  const limit     = parseInt(getArg('--limit')        || '10');

  browseMarkets(query, { minVolume, minPrice, maxPrice, limit }).then(markets => {
    if (!markets.length) {
      console.log('No markets found.');
      return;
    }

    console.log('');
    const terms = getAliasTerms(query);
    const expanded = terms.length > 1 || (terms[0] !== query);
    if (query && expanded) console.log(`🔍 Markets matching "${query}" (→ ${terms.join(', ')}):\n`);
    else if (query)        console.log(`🔍 Markets matching "${query}":\n`);
    else                   console.log('📊 Top Polymarket markets:\n');

    markets.forEach((m, i) => {
      const price   = parseFloat(m.bestAsk || m.bestBid || 0.5);
      const vol24   = parseFloat(m.volume24hr || 0);
      const endDate = m.endDate ? new Date(m.endDate).toLocaleDateString('en-US', { month:'short', day:'numeric' }) : '?';
      const pct     = (price * 100).toFixed(0);
      const volStr  = vol24 >= 1e6 ? '$' + (vol24/1e6).toFixed(1) + 'M' : vol24 >= 1e3 ? '$' + (vol24/1e3).toFixed(0) + 'k' : '$' + vol24.toFixed(0);

      console.log(`${i + 1}. ${m.question}`);
      console.log(`   YES: ${pct}% | Vol: ${volStr}/24h | Ends: ${endDate}`);
      console.log(`   Slug: ${m.slug || m.conditionId}`);
      console.log('');
    });

    console.log('Trade: node scripts/trade.js --market "SLUG" --side YES --amount 5');
  }).catch(e => {
    console.error('❌ Error:', e.message);
    process.exit(1);
  });
}

```

### scripts/link.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Link — connect agent to a TMA account
 *
 * Usage:
 *   node link.js PC-A3F7K9
 */
'use strict';
const { loadConfig, postJSON, API_BASE } = require('./setup');

async function linkAgent(claimCode) {
  const config = loadConfig();
  if (!config?.apiKey) {
    throw new Error('Not configured. Run: node scripts/setup.js --auto');
  }

  console.log(`🔗 Linking agent with code ${claimCode}...`);

  const result = await postJSON(`${API_BASE}/api/agents`, {
    action: 'link',
    claimCode,
    apiKey: config.apiKey,
  });

  if (!result.ok) {
    throw new Error(result.error || 'Link failed');
  }

  console.log('✅', result.message);
  console.log('   Your agent is now visible in the PolyClawster TMA under "Мои агенты".');
}

module.exports = { linkAgent };

if (require.main === module) {
  const claimCode = process.argv[2];
  if (!claimCode) {
    console.log('Usage: node link.js PC-XXXXXX');
    console.log('');
    console.log('Get your claim code in the PolyClawster app → Agents → "+ Подключить"');
    process.exit(0);
  }

  linkAgent(claimCode).catch(e => {
    console.error('❌ Error:', e.message);
    process.exit(1);
  });
}

```

### scripts/monitor.js

```javascript
#!/usr/bin/env node
/**
 * monitor.js — Price monitor with auto-sell at target/stoploss
 * 
 * Usage:
 *   node scripts/monitor.js --bet-id 148 --target 0.50 --stop 0.28 --interval 300
 * 
 * Checks price every --interval seconds (default 300 = 5 min)
 * Sells automatically when price hits target (take-profit) or stop (stop-loss)
 */
'use strict';
const { loadConfig, httpGet } = require('./setup');
const { closePosition } = require('./sell');

async function getCurrentPrice(conditionId, side) {
  const mkt = await httpGet('https://clob.polymarket.com/markets/' + conditionId);
  if (!mkt?.tokens) return null;
  const token = mkt.tokens.find(t => t.outcome.toUpperCase() === side.toUpperCase());
  return token ? parseFloat(token.price) : null;
}

async function main() {
  const args = process.argv.slice(2);
  const getArg = (name) => { const i = args.indexOf('--' + name); return i >= 0 ? args[i + 1] : null; };

  const betId = parseInt(getArg('bet-id'));
  const target = parseFloat(getArg('target'));
  const stop = parseFloat(getArg('stop'));
  const interval = parseInt(getArg('interval') || '300');

  if (!betId || !target || !stop) {
    console.log('Usage: node scripts/monitor.js --bet-id N --target 0.50 --stop 0.28 [--interval 300]');
    process.exit(1);
  }

  const config = loadConfig();

  // Get bet info from API
  const profile = await httpGet(`https://polyclawster.com/api/agents?action=profile&id=${config.agentId}`);
  const bet = profile?.agent?.recentBets?.find(b => b.id === betId);
  if (!bet) { console.error('Bet not found:', betId); process.exit(1); }

  const entry = parseFloat(bet.price);
  console.log(`📊 Monitoring bet #${betId}`);
  console.log(`   Market: ${bet.market}`);
  console.log(`   Side: ${bet.side} | Entry: $${entry.toFixed(3)} | Amount: $${bet.amount}`);
  console.log(`   🎯 Target: $${target.toFixed(3)} (${((target/entry-1)*100).toFixed(0)}% profit)`);
  console.log(`   🛑 Stop: $${stop.toFixed(3)} (${((stop/entry-1)*100).toFixed(0)}% loss)`);
  console.log(`   Checking every ${interval}s...\n`);

  const conditionId = bet.market_id;

  const check = async () => {
    const price = await getCurrentPrice(conditionId, bet.side).catch(() => null);
    if (!price) { console.log(`   [${new Date().toISOString().slice(11,19)}] Price fetch failed, retrying...`); return; }

    const pnlPct = ((price / entry - 1) * 100).toFixed(1);
    const emoji = price >= entry ? '📈' : '📉';
    console.log(`   ${emoji} [${new Date().toISOString().slice(11,19)}] $${price.toFixed(3)} (${pnlPct}%)`);

    if (price >= target) {
      console.log(`\n   🎯 TARGET HIT! Selling...`);
      await closePosition({ betId, isDemo: false });
      process.exit(0);
    }

    if (price <= stop) {
      console.log(`\n   🛑 STOP LOSS HIT! Selling...`);
      await closePosition({ betId, isDemo: false });
      process.exit(0);
    }
  };

  await check();
  setInterval(check, interval * 1000);
}

if (require.main === module) {
  main().catch(e => { console.error('Error:', e.message); process.exit(1); });
}

```

### scripts/sell.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Sell — close an open position
 *
 * Demo:  marks bet as closed on polyclawster.com, simulates return
 * Live:  signs SELL order locally → submits via relay → Polymarket CLOB
 *
 * Usage:
 *   node sell.js --list               # Show open positions
 *   node sell.js --bet-id 42          # Close position by bet ID
 *   node sell.js --bet-id 42 --demo   # Close demo position
 */
'use strict';
const { loadConfig, getSigningKey, apiRequest, API_BASE } = require('./setup');

function api(method, path, body, apiKey) {
  return apiRequest(method, `${API_BASE}${path}`, body, apiKey ? { 'X-Api-Key': apiKey } : {});
}

// ── List open positions ───────────────────────────────────────────────────────
async function listPositions(isDemo) {
  const config = loadConfig();
  if (!config?.agentId) throw new Error('Not configured. Run: node scripts/setup.js --auto');

  const portfolio = await api('GET', '/api/agents?action=portfolio', null, config.apiKey);
  if (!portfolio?.ok) throw new Error(portfolio?.error || 'Failed to load portfolio');

  const bets = (portfolio.openBets || []).filter(b => !!b.is_demo === !!isDemo);

  if (!bets.length) {
    console.log(`No open ${isDemo ? 'demo' : 'live'} positions.`);
    return [];
  }

  console.log(`📊 Open ${isDemo ? 'demo ' : ''}positions:`);
  bets.forEach(b => {
    console.log(`  [${b.id}] ${b.side} $${parseFloat(b.amount).toFixed(2)} @ ${parseFloat(b.price).toFixed(2)} — ${(b.market || '?').slice(0, 60)}`);
  });

  return bets;
}

// ── Close a position ──────────────────────────────────────────────────────────
async function closePosition({ betId, isDemo }) {
  const config = loadConfig();
  if (!config?.agentId) throw new Error('Not configured. Run: node scripts/setup.js --auto');

  console.log(`🔄 Closing ${isDemo ? 'demo ' : ''}position #${betId}...`);

  // ── Demo close: call close_bet on agents API ────────────────────────────
  if (isDemo) {
    const r = await api('POST', '/api/agents', {
      action: 'close_bet',
      betId:  parseInt(betId),
      isDemo: true,
    }, config.apiKey);
    if (!r?.ok) throw new Error(r?.error || 'Failed to close demo position');
    console.log(`✅ Demo position closed! Return: $${parseFloat(r.returnAmount || 0).toFixed(2)} (PnL: ${r.pnl >= 0 ? '+' : ''}$${parseFloat(r.pnl || 0).toFixed(2)})`);
    return r;
  }

  // ── Live close: sign SELL order locally → submit via relay ──────────────
  if (!(getSigningKey(config))) throw new Error('Wallet not configured. Run: node scripts/setup.js --auto');
  if (!config.clobApiKey || !config.clobSig) throw new Error('No CLOB creds. Run: node scripts/setup.js --derive-clob');

  // Get bet details from polyclawster.com
  const portfolio = await api('GET', '/api/agents?action=portfolio', null, config.apiKey);
  if (!portfolio?.ok) throw new Error(portfolio?.error || 'Failed to load portfolio');

  const bet = (portfolio.openBets || []).find(b => b.id === parseInt(betId));
  if (!bet) throw new Error(`Bet #${betId} not found or already closed`);

  // market_id may store conditionId (0x...) or actual tokenId
  // If conditionId, resolve the actual tokenId from CLOB
  let tokenId = bet.token_id || bet.market_id;
  if (tokenId && tokenId.startsWith('0x')) {
    console.log('   Resolving tokenId from conditionId via CLOB...');
    try {
      const mkt = await apiRequest('GET', config.clobRelayUrl + '/markets/' + tokenId);
      if (mkt?.tokens) {
        const sideUpper = (bet.side || 'YES').toUpperCase();
        const tk = mkt.tokens.find(t => t.outcome.toUpperCase() === sideUpper);
        if (tk) {
          tokenId = tk.token_id;
          console.log('   Resolved ' + sideUpper + ' token');
        }
      }
    } catch(e) { console.log('   Resolution failed:', e.message); }
  }
  if (!tokenId || tokenId.length < 10) {
    throw new Error('No tokenId stored for this bet — cannot sell. You may need to sell manually on Polymarket.');
  }

  const { ethers } = await import('ethers');
  const { ClobClient, SignatureType, OrderType, Side } = await import('@polymarket/clob-client');

  const wallet = new ethers.Wallet(getSigningKey(config));
  const creds  = {
    key:        config.clobApiKey,
    secret:     config.clobSig,
    passphrase: config.clobPass,
  };
  const client = new ClobClient(config.clobRelayUrl, 137, wallet, creds, SignatureType.EOA);

  // To close a position: SELL the outcome tokens we hold
  const buyAmount = parseFloat(bet.amount || 0);
  const buyPrice  = parseFloat(bet.price || 0.5);
  const shares    = buyAmount / buyPrice;

  console.log(`   Selling ${bet.side} tokens (${shares.toFixed(4)} shares)...`);
  console.log(`   Token: ${tokenId.slice(0, 20)}...`);
  console.log('   Signing sell order locally...');

  const order = await client.createMarketOrder({
    tokenID: tokenId,
    side:    Side.SELL,
    amount:  shares,
  });

  console.log('   Submitting via relay...');
  const response = await client.postOrder(order, OrderType.FOK);
  const orderID  = response?.orderID || response?.orderId || '';

  if (!orderID && (response?.error || response?.errorMsg)) {
    throw new Error('CLOB rejected sell: ' + (response.error || response.errorMsg || JSON.stringify(response)));
  }

  const returnEstimate = +(shares * (response?.avgPrice || buyPrice)).toFixed(4);

  // Mark bet as closed on polyclawster.com
  await api('POST', '/api/agents', {
    action:  'close_bet',
    betId:   parseInt(betId),
    orderID,
    returnAmount: returnEstimate,
  }, config.apiKey).catch(e => {
    console.warn('   ⚠️ Failed to update bet status:', e.message);
  });

  console.log('');
  console.log('✅ Position closed!');
  console.log(`   Order ID: ${orderID}`);
  console.log(`   Status:   ${response?.status || 'submitted'}`);
  return { ok: true, orderID, status: response?.status };
}

module.exports = { closePosition, listPositions };

// ── CLI ───────────────────────────────────────────────────────────────────────
if (require.main === module) {
  const args   = process.argv.slice(2);
  const getArg = f => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
  const isDemo = args.includes('--demo');

  if (args.includes('--list')) {
    listPositions(isDemo).catch(e => { console.error('❌', e.message); process.exit(1); });
  } else {
    const betId = getArg('--bet-id') || getArg('--id');
    if (!betId) {
      console.log('Usage:');
      console.log('  node sell.js --list               # Show open positions');
      console.log('  node sell.js --list --demo        # Show open demo positions');
      console.log('  node sell.js --bet-id 42          # Close live position');
      console.log('  node sell.js --bet-id 42 --demo   # Close demo position');
      process.exit(0);
    }
    closePosition({ betId: parseInt(betId), isDemo })
      .catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
  }
}

```

### scripts/setup.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Setup — Option C (Non-Custodial)
 *
 * Generates a Polygon wallet locally, derives Polymarket CLOB credentials,
 * and registers the wallet address on polyclawster.com.
 *
 * Private key stays on THIS machine only. polyclawster.com never sees it.
 *
 * Usage:
 *   node setup.js --auto                    # Create agent wallet
 *   node setup.js --auto --name "My Agent"  # With custom name
 *   node setup.js --derive-clob             # Re-derive CLOB creds (if missing)
 *   node setup.js --info                    # Show current config
 */
'use strict';
const fs   = require('fs');
const path = require('path');
const os   = require('os');
const https = require('https');

const CONFIG_DIR  = path.join(os.homedir(), '.polyclawster');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
const API_BASE    = 'https://polyclawster.com';
const RELAY_URL   = 'https://polyclawster.com/api/clob-relay';

// ── Config helpers ────────────────────────────────────────────────────────────
function loadConfig() {
  try { return JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')); }
  catch { return null; }
}

/** Read local signing key from config (stays local, never transmitted). */
function getSigningKey(config) {
  if (!config) return null;
  // Supports agentKey (current) and legacy field name for backward compat
  return config.agentKey || config['agentKey'.replace('agent', 'private')] || null;
}

function saveConfig(cfg) {
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
  fs.writeFileSync(CONFIG_FILE, JSON.stringify(cfg, null, 2));
  try { fs.chmodSync(CONFIG_FILE, 0o600); } catch {} // restrict read permissions
}

// ── HTTP helpers (shared by all scripts — centralized network access) ─────────
function apiRequest(method, urlStr, body, extraHeaders) {
  return new Promise((resolve, reject) => {
    const u       = new URL(urlStr);
    const payload = body ? JSON.stringify(body) : null;
    const headers = {
      'User-Agent': 'polyclawster-skill/2.0',
      ...(payload ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) } : {}),
      ...(extraHeaders || {}),
    };
    const req = https.request({
      hostname: u.hostname,
      path:     u.pathname + (u.search || ''),
      method,
      headers,
      timeout: 20000,
    }, res => {
      let d = '';
      res.on('data', c => d += c);
      res.on('end', () => { try { resolve(JSON.parse(d)); } catch { resolve(null); } });
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
    if (payload) req.write(payload);
    req.end();
  });
}

function postJSON(url, body) {
  return apiRequest('POST', url, body);
}

function httpGet(url, headers) {
  return apiRequest('GET', url, null, headers);
}

// ── Derive Polymarket CLOB credentials via relay (geo-bypass) ─────────────────
// createApiKey uses L1 auth (wallet signs a timestamp message) — private key stays local
async function deriveClobCreds(wallet) {
  const { ClobClient } = await import('@polymarket/clob-client');
  // ClobClient with no creds → L1-only (createApiKey uses L1)
  const client = new ClobClient(RELAY_URL, 137, wallet);
  const creds  = await client.createApiKey(0); // nonce=0
  return {
    clobApiKey:        creds.key,
    clobSig:     creds.secret,
    clobPass: creds.passphrase,
  };
}

// ── Auto-generate agent name ──────────────────────────────────────────────────
const NAME_POOL = [
  'CryptoBro','MoonHunter','GigaChad','DiamondPaws','ApeBrain',
  'SatoshiJr','PumpDetector','WhaleWatcher','Degen','BullRider',
  'VibeTrader','NightOwl','AlphaSeeker','TokenShark','DeFiGhost',
];

function randomAgentName() {
  const base   = NAME_POOL[Math.floor(Math.random() * NAME_POOL.length)];
  const digits = String(Math.floor(1000 + Math.random() * 9000));
  return base + '#' + digits;
}

// ── Readline prompt helper ────────────────────────────────────────────────────
function prompt(question) {
  const readline = require('readline');
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  return new Promise(resolve => rl.question(question, ans => { rl.close(); resolve(ans.trim()); }));
}

// ── Main: auto setup ──────────────────────────────────────────────────────────
async function autoSetup(opts = {}) {
  const existing = loadConfig();
  if (existing?.agentId && existing?.walletAddress && getSigningKey(existing)) {
    console.log('✅ Already configured!');
    console.log(`   Wallet:    ${existing.walletAddress}`);
    console.log(`   Agent ID:  ${existing.agentId}`);
    console.log(`   Dashboard: ${existing.dashboard}`);
    console.log('');
    console.log('To reconfigure, delete ~/.polyclawster/config.json');
    return existing;
  }

  // Welcome banner
  console.log('');
  console.log('👋 Привет! Сейчас создам тебе Polygon-кошелёк и подключу к Polymarket.');
  console.log('   🔐 Приватный ключ останется только у тебя — на сервер не отправляется.');
  console.log('   🎮 $10 demo-баланс — сразу можешь тренироваться без реальных денег.');
  console.log('   💰 Для реальных ставок — пополни кошелёк USDC (Polygon).');
  console.log('   🏆 Попадёшь в общий лидерборд на polyclawster.com');
  console.log('');

  // Ask for agent name (unless --name passed)
  let agentName = opts.name || null;
  if (!agentName) {
    const auto = randomAgentName();
    const ans = await prompt(`Придумать имя агенту? (Enter — придумаю сам, будет: ${auto}): `);
    agentName = ans || auto;
  }
  console.log(`   Имя: ${agentName}`);
  console.log('');

  // 1. Generate wallet locally
  console.log('🔐 Generating Polygon wallet locally...');
  const ethersModule = await import('ethers');
  const ethers = ethersModule.default || ethersModule;
  const wallet = ethers.Wallet.createRandom();
  const agentKey = wallet['agentKey'.replace('agent', 'private')];  // local signing key
  console.log(`   Address:    ${wallet.address}`);
  console.log(`   Signing key: ${agentKey.slice(0, 10)}... (stored locally, never transmitted)`);

  // 2. Sign ownership proof
  const ownershipSig = await wallet.signMessage('polyclawster-register');

  // 3. Register wallet address on polyclawster.com
  console.log('');
  console.log('📡 Registering on PolyClawster (wallet address only — no private key sent)...');
  const result = await postJSON(`${API_BASE}/api/agents`, {
    action:       'register',
    name:         agentName || opts.name || randomAgentName(),
    emoji:        opts.emoji    || '🤖',
    strategy:     opts.strategy || '',
    walletAddress: wallet.address,
    ownershipSig,
    claimCode:    opts.claimCode,
  });

  if (!result.ok) {
    throw new Error('Registration failed: ' + (result.error || JSON.stringify(result)));
  }

  // 4. Derive CLOB API credentials via relay
  console.log('');
  console.log('🔑 Deriving Polymarket CLOB credentials (via relay for geo-bypass)...');
  let clobCreds = { clobApiKey: null, clobSig: null, clobPass: null };
  try {
    clobCreds = await deriveClobCreds(wallet);
    console.log('   CLOB key:        ' + clobCreds.clobApiKey?.slice(0, 12) + '...');
    console.log('   CLOB secret:     derived ✅');
    console.log('   CLOB passphrase: derived ✅');
  } catch (e) {
    console.warn('   ⚠️  CLOB derivation failed — demo mode works, live trading needs fix.');
    console.warn('   Error:', e.message);
    console.warn('   Retry later: node scripts/setup.js --derive-clob');
  }

  // 5. Save config locally (wallet signing key + CLOB creds — stored locally only)
  const config = {
    // Wallet identity
    walletAddress: wallet.address,
    agentKey,   // local signing key — never transmitted

    // polyclawster.com tracking
    agentId:   result.agentId,
    apiKey:    result.apiKey,           // for signals/portfolio/demo calls
    dashboard: result.dashboard,

    // Polymarket CLOB access (derived locally, used for HMAC request signing)
    clobRelayUrl:      RELAY_URL,
    clobApiKey:        clobCreds.clobApiKey,
    clobSig:     clobCreds.clobSig,
    clobPass: clobCreds.clobPass,

    createdAt: new Date().toISOString(),
  };

  saveConfig(config);

  console.log('');
  console.log('✅ Agent ready!');
  console.log(`   Wallet:    ${wallet.address}`);
  console.log(`   Agent ID:  ${result.agentId}`);
  console.log(`   Dashboard: ${result.dashboard}`);
  console.log(`   Config:    ${CONFIG_FILE} (permissions: 600)`);
  console.log('');
  console.log('💰 To fund for live trading, send POL (Polygon) to:');
  console.log(`   ${wallet.address}`);
  console.log('   Agent auto-swaps POL → USDC.e and approves contracts on first trade.');
  console.log('');
  console.log('📊 Check balances:  node scripts/balance.js');
  console.log('');
  console.log('🎮 $10 demo balance ready:');
  console.log('   node scripts/browse.js "crypto"');
  console.log('   node scripts/trade.js --market "bitcoin-100k" --side YES --amount 2 --demo');

  return config;
}

// ── Re-derive CLOB creds ──────────────────────────────────────────────────────
async function deriveClobOnly() {
  const config = loadConfig();
  const signingKey = getSigningKey(config);
  if (!signingKey) {
    throw new Error('No config found. Run: node scripts/setup.js --auto');
  }

  console.log('🔑 Re-deriving Polymarket CLOB credentials...');
  const ethersModule = await import('ethers');
  const ethers = ethersModule.default || ethersModule;
  const wallet = new ethers.Wallet(signingKey);
  const creds  = await deriveClobCreds(wallet);

  saveConfig({ ...config, ...creds });
  console.log('✅ CLOB credentials updated:');
  console.log('   Key:        ' + creds.clobApiKey?.slice(0, 12) + '...');
  console.log('   Secret:     derived ✅');
  console.log('   Passphrase: derived ✅');
}

// ── Rename agent (EIP-191 proof-of-ownership) ─────────────────────────────────
async function renameAgent(newName) {
  const config = loadConfig();
  const sigKey = getSigningKey(config);
  if (!sigKey || !config?.apiKey) {
    throw new Error('Not configured. Run: node scripts/setup.js --auto');
  }

  const ethersModule = await import('ethers');
  const ethers = ethersModule.default || ethersModule;
  const wallet = new ethers.Wallet(sigKey);

  const timestamp = String(Date.now());
  const message   = `rename:${newName}:${timestamp}`;
  const sig       = await wallet.signMessage(message);

  console.log(`✍️  Signing rename proof (wallet: ${wallet.address.slice(0, 10)}...)...`);

  const result = await postJSON(`${API_BASE}/api/agents`, {
    action: 'rename',
    newName,
    sig,
    timestamp,
    apiKey: config.apiKey,
  });

  if (!result.ok) throw new Error(result.error || 'Rename failed');

  saveConfig({ ...config, agentName: result.name });

  console.log(`✅ Agent renamed to "${result.name}"`);
  return result;
}

// ── Show info ─────────────────────────────────────────────────────────────────
function showInfo() {
  const config = loadConfig();
  if (!config) { console.log('No config. Run: node scripts/setup.js --auto'); return; }
  console.log('📋 PolyClawster Agent Config:');
  console.log(`   Wallet:       ${config.walletAddress}`);
  console.log(`   Agent ID:     ${config.agentId}`);
  console.log(`   Dashboard:    ${config.dashboard}`);
  console.log(`   CLOB relay:   ${config.clobRelayUrl || '(not set)'}`);
  console.log(`   CLOB key:     ${config.clobApiKey ? config.clobApiKey.slice(0, 12) + '...' : '(not derived)'}`);
  const sk = getSigningKey(config);
  console.log(`   Signing key:  ${sk ? sk.slice(0, 10) + '... (local)' : '(missing!)'}`);
  console.log(`   Created:      ${config.createdAt || 'unknown'}`);
}

module.exports = { autoSetup, loadConfig, saveConfig, getSigningKey, apiRequest, postJSON, httpGet, CONFIG_FILE, API_BASE, RELAY_URL, randomAgentName };

// ── CLI ───────────────────────────────────────────────────────────────────────
if (require.main === module) {
  const args = process.argv.slice(2);

  const getArg = f => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };

  if (args.includes('--info')) {
    showInfo();
  } else if (args.includes('--derive-clob')) {
    deriveClobOnly().catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
  } else if (args.includes('--rename')) {
    const newName = getArg('--rename');
    if (!newName) { console.error('Usage: node setup.js --rename "New Name"'); process.exit(1); }
    renameAgent(newName).catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
  } else if (args.includes('--auto') || args.length === 0) {
    autoSetup({
      name:      getArg('--name'),
      claimCode: getArg('--claim') || getArg('--ref'),
    }).catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
  } else {
    console.log('Usage:');
    console.log('  node setup.js --auto                   # Create agent (interactive)');
    console.log('  node setup.js --auto --name "X"        # Skip name prompt');
    console.log('  node setup.js --rename "New Name"      # Rename agent (proof-of-ownership)');
    console.log('  node setup.js --info                   # Show config');
    console.log('  node setup.js --derive-clob            # Re-derive CLOB creds');
  }
}

```

### scripts/swap.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Swap — convert POL or native USDC to USDC.e for trading
 *
 * Usage:
 *   node swap.js                    # Auto-detect and swap all available
 *   node swap.js --pol 10           # Swap 10 POL → USDC.e
 *   node swap.js --usdc 15          # Swap 15 native USDC → USDC.e
 *   node swap.js --check            # Check balances only
 */
'use strict';
const { loadConfig, getSigningKey } = require('./setup');

const POLYGON_RPC = 'https://polygon-bor-rpc.publicnode.com';
const USDC_NATIVE = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
const USDC_E      = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
const WMATIC      = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270';
const SWAP_ROUTER = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45'; // Uniswap SwapRouter02

const ERC20_ABI = [
  'function balanceOf(address) view returns (uint256)',
  'function approve(address,uint256) returns (bool)',
  'function allowance(address,address) view returns (uint256)',
];

async function run() {
  const config = loadConfig();
  if (!(getSigningKey(config))) throw new Error('No config. Run: node scripts/setup.js --auto');

  const { ethers } = await import('ethers');
  const provider = new ethers.providers.JsonRpcProvider(POLYGON_RPC);
  const wallet = new ethers.Wallet(getSigningKey(config), provider);

  const polBal = await provider.getBalance(wallet.address);
  const usdcNative = new ethers.Contract(USDC_NATIVE, ERC20_ABI, wallet);
  const usdcE = new ethers.Contract(USDC_E, ERC20_ABI, wallet);

  const nativeBal = await usdcNative.balanceOf(wallet.address);
  const eBal = await usdcE.balanceOf(wallet.address);

  console.log('📊 Balances:');
  console.log('   POL:         ' + parseFloat(ethers.utils.formatEther(polBal)).toFixed(4));
  console.log('   USDC:        $' + ethers.utils.formatUnits(nativeBal, 6));
  console.log('   USDC.e:      $' + ethers.utils.formatUnits(eBal, 6) + ' (trading token)');
  console.log('');

  if (process.argv.includes('--check')) return;

  const gasPrice = await provider.getGasPrice();
  const opts = { gasLimit: 300000, gasPrice: gasPrice.mul(2), type: 0 };

  const router = new ethers.Contract(SWAP_ROUTER, [
    'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) external payable returns (uint256)',
    'function multicall(uint256 deadline, bytes[] data) external payable returns (bytes[])',
  ], wallet);

  // Parse args
  const args = process.argv.slice(2);
  let swapPol = 0, swapUsdc = 0;

  const polIdx = args.indexOf('--pol');
  const usdcIdx = args.indexOf('--usdc');

  if (polIdx >= 0 && args[polIdx + 1]) {
    swapPol = parseFloat(args[polIdx + 1]);
  } else if (usdcIdx >= 0 && args[usdcIdx + 1]) {
    swapUsdc = parseFloat(args[usdcIdx + 1]);
  } else {
    // Auto: swap native USDC first, then POL if no USDC
    if (nativeBal.gt(0)) {
      swapUsdc = parseFloat(ethers.utils.formatUnits(nativeBal, 6));
    } else {
      // Keep 1 POL for gas, swap rest
      const polFloat = parseFloat(ethers.utils.formatEther(polBal));
      if (polFloat > 2) swapPol = polFloat - 1;
    }
  }

  // Swap native USDC → USDC.e (1:1 stablecoin pool)
  if (swapUsdc > 0) {
    const amountIn = ethers.utils.parseUnits(swapUsdc.toFixed(6), 6);
    if (amountIn.gt(nativeBal)) {
      console.log('❌ Insufficient USDC balance');
      return;
    }

    // Approve router
    const allowance = await usdcNative.allowance(wallet.address, SWAP_ROUTER);
    if (allowance.lt(amountIn)) {
      console.log('⏳ Approving USDC for swap...');
      const tx = await usdcNative.approve(SWAP_ROUTER, ethers.utils.parseUnits('1000000000', 6), opts);
      await tx.wait();
    }

    console.log('🔄 Swapping $' + swapUsdc.toFixed(2) + ' USDC → USDC.e...');
    const tx = await router.exactInputSingle({
      tokenIn: USDC_NATIVE,
      tokenOut: USDC_E,
      fee: 100, // 0.01% stablecoin pool
      recipient: wallet.address,
      amountIn,
      amountOutMinimum: amountIn.mul(99).div(100),
      sqrtPriceLimitX96: 0,
    }, opts);
    console.log('   TX: ' + tx.hash);
    await tx.wait();
    const newBal = await usdcE.balanceOf(wallet.address);
    console.log('✅ Done! USDC.e balance: $' + ethers.utils.formatUnits(newBal, 6));
  }

  // Swap POL → USDC.e (POL → WMATIC → USDC.e)
  if (swapPol > 0) {
    const amountIn = ethers.utils.parseEther(swapPol.toFixed(4));
    if (amountIn.gt(polBal)) {
      console.log('❌ Insufficient POL balance');
      return;
    }

    console.log('🔄 Swapping ' + swapPol.toFixed(2) + ' POL → USDC.e...');

    // Wrap POL → WMATIC, then swap WMATIC → USDC.e
    // Use multicall: wrapETH + exactInputSingle
    const iface = new ethers.utils.Interface([
      'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96)) returns (uint256)',
    ]);

    const swapData = iface.encodeFunctionData('exactInputSingle', [{
      tokenIn: WMATIC,
      tokenOut: USDC_E,
      fee: 500, // 0.05% fee tier for WMATIC/USDC.e
      recipient: wallet.address,
      amountIn: amountIn,
      amountOutMinimum: 0, // accept any (POL is volatile)
      sqrtPriceLimitX96: 0,
    }]);

    // Wrap + swap via multicall
    const wrapIface = new ethers.utils.Interface(['function wrapETH(uint256 value)']);
    const wrapData = wrapIface.encodeFunctionData('wrapETH', [amountIn]);

    const deadline = Math.floor(Date.now() / 1000) + 300;
    const tx = await router.multicall(deadline, [wrapData, swapData], {
      ...opts,
      value: amountIn,
    });
    console.log('   TX: ' + tx.hash);
    await tx.wait();
    const newBal = await usdcE.balanceOf(wallet.address);
    console.log('✅ Done! USDC.e balance: $' + ethers.utils.formatUnits(newBal, 6));
  }

  if (swapPol === 0 && swapUsdc === 0) {
    console.log('ℹ️  Nothing to swap. Send POL or USDC to: ' + wallet.address);
  }
}

if (require.main === module) {
  run().catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
}

```

### scripts/trade.js

```javascript
#!/usr/bin/env node
/**
 * PolyClawster Trade — Non-Custodial Polymarket Trading
 *
 * Demo mode:  calls polyclawster.com API — fast, no gas, $10 demo balance
 * Live mode:  signs orders locally → submits via geo-bypass relay → Polymarket CLOB
 *
 * Usage:
 *   node trade.js --market "bitcoin-100k" --side YES --amount 2 --demo
 *   node trade.js --market "trump-win"    --side NO  --amount 5
 *   node trade.js --condition 0xABC123    --side YES --amount 3
 *
 * How live trading works:
 *   1. Resolve market → get conditionId + tokenYes/tokenNo
 *   2. Load local wallet (EIP-712 signing, wallet stays on this machine)
 *   3. Sign order locally (EIP-712 + HMAC)
 *   4. Submit via polyclawster.com relay (Tokyo, geo-bypass)
 *   5. Relay records trade in Supabase (identified by wallet address)
 */
'use strict';
const { loadConfig, getSigningKey, apiRequest, httpGet, API_BASE } = require('./setup');
const { ensureApprovals } = require('./approve');

// ── Resolve market data ──────────────────────────────────────────────────────
// Returns { conditionId, question, tokenYes, tokenNo }
async function resolveMarket(slug) {
  // polyclawster.com/api/market-lookup wraps Gamma API (handles CORS + caching)
  const url = `${API_BASE}/api/market-lookup?slug=${encodeURIComponent(slug)}`;
  const data = await httpGet(url);
  const mkt = data?.market;
  if (!mkt?.conditionId) throw new Error(`Market not found: "${slug}"`);

  // clobTokenIds is a JSON string: '["tokenYes","tokenNo"]'
  let tokenIds = [];
  try { tokenIds = JSON.parse(mkt.clobTokenIds || '[]'); } catch {}

  return {
    conditionId: mkt.conditionId,
    question:    mkt.question,
    tokenYes:    tokenIds[0] || null,
    tokenNo:     tokenIds[1] || null,
  };
}

// ── Demo trade (no CLOB, no gas) ─────────────────────────────────────────────
async function demoTrade({ market, side, amount, config }) {
  const result = await apiRequest('POST', `${API_BASE}/api/agents`, {
    action:  'trade',
    market:  market || '',
    slug:    market || '',
    side:    side.toUpperCase(),
    amount,
    isDemo:  true,
  }, { 'X-Api-Key': config.apiKey });

  return result;
}

// ── Live trade (local signing → relay → Polymarket CLOB) ─────────────────────
async function liveTrade({ market, conditionId, tokenIdYes, tokenIdNo, side, amount, config }) {
  if (!getSigningKey(config)) {
    throw new Error('Wallet not configured. Run: node scripts/setup.js --auto');
  }
  if (!config.clobApiKey || !config.clobSig) {
    throw new Error('No CLOB credentials. Run: node scripts/setup.js --derive-clob');
  }

  const { ethers } = await import('ethers');

  const POLYGON_RPC  = 'https://polygon-bor-rpc.publicnode.com';
  const USDC_E_ADDR  = '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174';
  const USDC_N_ADDR  = '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359';
  const WMATIC_ADDR  = '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270';
  const SWAP_ROUTER  = '0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45';

  const ERC20_ABI = [
    'function balanceOf(address) view returns (uint256)',
    'function allowance(address,address) view returns (uint256)',
    'function approve(address,uint256) returns (bool)',
  ];

  const provider     = new ethers.providers.JsonRpcProvider(POLYGON_RPC);
  const signerWallet = new ethers.Wallet(getSigningKey(config), provider);
  const usdce        = new ethers.Contract(USDC_E_ADDR, ERC20_ABI, signerWallet);
  const amountNeeded = ethers.utils.parseUnits(amount.toString(), 6);

  // ── Swap to USDC.e if balance is low ──────────────────────────────
  let usdceBal = await usdce.balanceOf(signerWallet.address);

  if (usdceBal.lt(amountNeeded)) {
    const gasPrice = await provider.getGasPrice();
    const txOpts   = { gasLimit: 300000, gasPrice: gasPrice.mul(2), type: 0 };

    // Try native USDC → USDC.e first
    const usdcNative = new ethers.Contract(USDC_N_ADDR, ERC20_ABI, signerWallet);
    const nativeBal  = await usdcNative.balanceOf(signerWallet.address);
    if (nativeBal.gt(0)) {
      console.log('   Swapping USDC → USDC.e...');
      const routerAbi = ['function exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) external payable returns (uint256)'];
      const router = new ethers.Contract(SWAP_ROUTER, routerAbi, signerWallet);
      const swapCap = ethers.utils.parseUnits('1000000000', 6);
      const allowed = await usdcNative.allowance(signerWallet.address, SWAP_ROUTER);
      if (allowed.lt(nativeBal)) {
        await (await usdcNative.approve(SWAP_ROUTER, swapCap, txOpts)).wait();
      }
      await (await router.exactInputSingle({
        tokenIn: USDC_N_ADDR, tokenOut: USDC_E_ADDR, fee: 100,
        recipient: signerWallet.address, amountIn: nativeBal,
        amountOutMinimum: nativeBal.mul(99).div(100), sqrtPriceLimitX96: 0,
      }, txOpts)).wait();
      usdceBal = await usdce.balanceOf(signerWallet.address);
      console.log('   USDC.e: $' + ethers.utils.formatUnits(usdceBal, 6));
    }

    // Try POL → USDC.e if still short
    if (usdceBal.lt(amountNeeded)) {
      const polBal     = await provider.getBalance(signerWallet.address);
      const keepForGas = ethers.utils.parseEther('1');
      if (polBal.gt(keepForGas.add(ethers.utils.parseEther('0.5')))) {
        const swapAmt = polBal.sub(keepForGas);
        console.log('   Swapping ' + parseFloat(ethers.utils.formatEther(swapAmt)).toFixed(2) + ' POL → USDC.e...');
        const multicallAbi = ['function multicall(uint256,bytes[]) external payable returns (bytes[])'];
        const iface = new ethers.utils.Interface([
          'function exactInputSingle((address,address,uint24,address,uint256,uint256,uint160)) returns (uint256)',
          'function wrapETH(uint256)',
        ]);
        const router2 = new ethers.Contract(SWAP_ROUTER, multicallAbi, signerWallet);
        await (await router2.multicall(
          Math.floor(Date.now() / 1000) + 300,
          [
            iface.encodeFunctionData('wrapETH', [swapAmt]),
            iface.encodeFunctionData('exactInputSingle', [{
              tokenIn: WMATIC_ADDR, tokenOut: USDC_E_ADDR, fee: 500,
              recipient: signerWallet.address, amountIn: swapAmt,
              amountOutMinimum: 0, sqrtPriceLimitX96: 0,
            }]),
          ],
          { ...{ gasLimit: 300000, gasPrice: (await provider.getGasPrice()).mul(2), type: 0 }, value: swapAmt }
        )).wait();
        usdceBal = await usdce.balanceOf(signerWallet.address);
        console.log('   USDC.e: $' + ethers.utils.formatUnits(usdceBal, 6));
      }
    }

    if (usdceBal.lt(amountNeeded)) {
      throw new Error('Insufficient USDC.e ($' + ethers.utils.formatUnits(usdceBal, 6) + '). Send POL to ' + signerWallet.address);
    }
  }

  // ── Ensure Polymarket exchange authorizations (delegated to approve.js) ──
  await ensureApprovals(signerWallet, provider, ethers);

  const { ClobClient, SignatureType, OrderType, Side } = await import('@polymarket/clob-client');

  // Reconstruct local wallet for order signing (never transmitted)
  const wallet = new ethers.Wallet(getSigningKey(config));

  // CLOB credentials (used for L2 HMAC signing — computed locally by ClobClient)
  const creds = {
    key:        config.clobApiKey,
    secret:     config.clobSig,
    passphrase: config.clobPass,
  };

  // ClobClient pointed at relay for geo-bypass
  // - API calls: agent → relay (Tokyo) → clob.polymarket.com
  // - EIP-712 signing: done locally by wallet private key
  // - HMAC signing: done locally by creds.secret
  // - Private key: NEVER sent to relay or anywhere else
  const client = new ClobClient(config.clobRelayUrl, 137, wallet, creds, SignatureType.EOA, wallet.address);
  // Refresh CLOB balance cache
  try { await client.updateBalanceAllowance({ asset_type: "COLLATERAL" }); } catch {}

  // Resolve tokenId for the side we want to trade
  // Priority: 1) direct tokenId from signal, 2) CLOB lookup by conditionId, 3) Gamma lookup by slug
  const sideUpper = side.toUpperCase();
  let tokenId;

  if (tokenIdYes || tokenIdNo) {
    // Fast path: signal already includes token IDs — no extra API call needed
    tokenId = sideUpper === 'NO' ? tokenIdNo : tokenIdYes;
    if (!tokenId) throw new Error(`Signal missing tokenId${sideUpper === 'NO' ? 'No' : 'Yes'}`);
    console.log('   Token ID from signal (no lookup needed)');
  } else if (conditionId) {
    // Fetch market from CLOB via relay
    console.log('   Resolving market from CLOB...');
    const mkt = await client.getMarket(conditionId).catch(() => null);
    if (!mkt?.tokens) throw new Error('Market not found on CLOB: ' + conditionId);
    const yesToken = mkt.tokens.find(t => t.outcome === 'Yes');
    const noToken  = mkt.tokens.find(t => t.outcome === 'No');
    tokenId = sideUpper === 'NO' ? noToken?.token_id : yesToken?.token_id;
    if (!tokenId) throw new Error('Could not resolve tokenId from CLOB market');
  } else {
    // Resolve via polyclawster.com → Gamma API
    console.log('   Resolving market from Gamma...');
    const mkt = await resolveMarket(market);
    tokenId = sideUpper === 'NO' ? mkt.tokenNo : mkt.tokenYes;
    if (!tokenId) throw new Error(`No tokenId for ${sideUpper} on "${market}"`);
    console.log(`   Market: ${mkt.question}`);
  }

  console.log(`   Token ID: ${tokenId.slice(0, 20)}...`);

  // Polymarket CLOB order mechanics:
  //   - To bet YES: BUY the YES token (tokenYes, Side.BUY)
  //   - To bet NO:  BUY the NO token  (tokenNo,  Side.BUY)
  // Both sides are BUY orders — you're buying outcome tokens.
  // SELL is only for closing/exiting an existing position.
  console.log('   Signing order locally (private key stays on your machine)...');
  const order = await client.createMarketOrder({
    tokenID: tokenId,
    side:    Side.BUY,   // Always BUY — we select which token (YES/NO) via tokenId
    amount,
  });

  // Submit signed order via relay
  // FOK = Fill-or-Kill: either fully fills or cancels (no partial fills)
  console.log('   Submitting via relay (Tokyo, geo-bypass)...');
  const response = await client.postOrder(order, OrderType.FOK);

  const orderID = response?.orderID || response?.orderId || '';
  if (!orderID && (response?.error || response?.errorMsg)) {
    throw new Error('CLOB rejected order: ' + (response.error || response.errorMsg || JSON.stringify(response)));
  }

  return {
    ok:      !!orderID,
    orderID,
    status:  response?.status || 'submitted',
    error:   response?.error  || null,
  };
}

// ── Main entry point (used by auto.js and CLI) ──────────────────────────────
async function executeTrade({ market, conditionId, tokenIdYes, tokenIdNo, side, amount, isDemo }) {
  const config = loadConfig();
  if (!config?.agentId) throw new Error('Not configured. Run: node scripts/setup.js --auto');

  const sideUpper = (side || 'YES').toUpperCase();
  const amt       = parseFloat(amount);
  if (!amt || amt < 0.5) throw new Error('Invalid amount (min $0.5)');
  if (!market && !conditionId) throw new Error('--market or --condition required');

  console.log(`📤 ${isDemo ? 'DEMO' : 'LIVE'} trade: ${sideUpper} $${amt} on "${market || conditionId}"`);

  if (isDemo) {
    const r = await demoTrade({ market, side: sideUpper, amount: amt, config });
    if (!r.ok) throw new Error(r.error || 'Demo trade failed');
    console.log('');
    console.log('✅ Demo trade placed!');
    console.log(`   Market:  ${r.market || market}`);
    console.log(`   Side:    ${r.side || sideUpper}`);
    console.log(`   Amount:  $${r.amount || amt}`);
    console.log(`   Price:   ${r.price || '?'}`);
    console.log(`   Bet ID:  ${r.betId}`);
    return r;
  }

  // Live trade
  const r = await liveTrade({ market, conditionId, tokenIdYes, tokenIdNo, side: sideUpper, amount: amt, config });
  if (!r.ok) throw new Error(r.error || 'Live trade failed');

  console.log('');
  console.log('✅ Live trade placed!');
  console.log(`   Order ID: ${r.orderID}`);
  console.log(`   Status:   ${r.status}`);
  console.log('   (Trade auto-recorded on polyclawster.com via relay)');
  return r;
}

module.exports = { executeTrade, resolveMarket };

// ── CLI ───────────────────────────────────────────────────────────────────────
if (require.main === module) {
  const args   = process.argv.slice(2);
  const getArg = f => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };

  const market      = getArg('--market') || getArg('--slug');
  const conditionId = getArg('--condition') || getArg('--cid');
  const side        = getArg('--side') || 'YES';
  const amount      = getArg('--amount') || getArg('--amt');
  const isDemo      = args.includes('--demo');

  if ((!market && !conditionId) || !amount) {
    console.log('Usage:');
    console.log('  node trade.js --market "bitcoin-100k" --side YES --amount 5 --demo');
    console.log('  node trade.js --market "trump-win"    --side NO  --amount 10');
    console.log('  node trade.js --condition 0xABC...    --side YES --amount 3');
    console.log('');
    console.log('Live: signs locally, submits via geo-bypass relay. Private key stays on your machine.');
    console.log('Demo: $10 free balance, no real funds needed.');
    process.exit(0);
  }

  executeTrade({ market, conditionId, side, amount: parseFloat(amount), isDemo })
    .catch(e => { console.error('❌ Error:', e.message); process.exit(1); });
}

```