easyclaw-skill
Run user-facing EasyClaw DEX actions from a self-contained skill folder. Use when an agent needs to submit user orders or check wallet/margin/order balances on EasyClaw without depending on external project directories.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-easyclaw
Repository
Skill path: skills/ice-coldbell/easyclaw
Run user-facing EasyClaw DEX actions from a self-contained skill folder. Use when an agent needs to submit user orders or check wallet/margin/order balances on EasyClaw without depending on external project directories.
Open repositoryBest 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 easyclaw-skill into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding easyclaw-skill to shared team environments
- Use easyclaw-skill for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: easyclaw-skill
description: Run user-facing EasyClaw DEX actions from a self-contained skill folder. Use when an agent needs to submit user orders or check wallet/margin/order balances on EasyClaw without depending on external project directories.
version: 0.1.0
metadata:
openclaw:
homepage: https://github.com/ice-coldbell/easyclaw/tree/main/easyclaw-skill
requires:
env:
- SOLANA_RPC_URL
- ANCHOR_PROVIDER_URL
- KEYPAIR_PATH
- ANCHOR_WALLET
- EASYCLAW_API_BASE_URL
- EASYCLAW_WS_URL
- EASYCLAW_API_TOKEN
- ORDER_ENGINE_PROGRAM_ID
- MARKET_REGISTRY_PROGRAM_ID
- API_BASE_URL
- BACKEND_WS_URL
- WS_URL
- API_AUTH_TOKEN
- API_TOKEN
bins:
- node
- npm
config:
- ~/.config/solana/id.json
primaryEnv: KEYPAIR_PATH
---
# EasyClaw User DEX Skill
Run only user workflows:
- balance and open-order checks
- order submission (place order)
- backend position/order/fill/history/orderbook/chart queries
- authenticated agent/strategy controls and safety kill-switch
- realtime websocket monitoring and signal-driven auto order execution
Do not run admin/bootstrap/keeper workflows in this skill.
## Runtime & Credential Requirements
- Wallet signer source: `KEYPAIR_PATH` or `ANCHOR_WALLET` (fallback `~/.config/solana/id.json`).
- Solana RPC source: `SOLANA_RPC_URL` or `ANCHOR_PROVIDER_URL` (fallback `http://127.0.0.1:8899`).
- Backend endpoint source: `EASYCLAW_API_BASE_URL` / `EASYCLAW_WS_URL` (or alias vars in `backend-common.js`).
- Optional API credential: `EASYCLAW_API_TOKEN` (required for protected backend controls).
- Local process usage: onboarding probes `solana config get` and can spawn child Node.js processes for autotrade execution.
- Local file writes:
- onboarding persists selected wallet envs into `easyclaw-skill/.env`
- strategy onboarding writes files into `easyclaw-skill/state/strategies/`
## Command Interface
Use `{baseDir}/scripts/dex-agent.sh`:
```bash
# toolchain + environment diagnostics
{baseDir}/scripts/dex-agent.sh doctor
# install local skill dependencies
{baseDir}/scripts/dex-agent.sh install
# wallet, USDC, margin, and open orders
{baseDir}/scripts/dex-agent.sh balance
{baseDir}/scripts/dex-agent.sh balance --json
# submit order tx
{baseDir}/scripts/dex-agent.sh order --market-id 1 --side buy --type market --margin 1000000
{baseDir}/scripts/dex-agent.sh order --market-id 2 --side sell --type limit --margin 2000000 --price 3000000000
# backend REST queries
{baseDir}/scripts/dex-agent.sh backend positions --mine --limit 20
{baseDir}/scripts/dex-agent.sh backend position-history --mine --limit 20
{baseDir}/scripts/dex-agent.sh backend chart-candles --market BTCUSDT --timeframe 1m --limit 120
{baseDir}/scripts/dex-agent.sh backend orderbook-heatmap --exchange binance --symbol BTCUSDT --limit 30
{baseDir}/scripts/dex-agent.sh backend portfolio --period 7d
{baseDir}/scripts/dex-agent.sh backend strategy-templates
{baseDir}/scripts/dex-agent.sh backend agent-risk --agent-id agent-001
# realtime WS monitor
{baseDir}/scripts/dex-agent.sh watch --channels "agent.signals,portfolio.updates,market.price.BTCUSDT"
# realtime signal -> auto order execution
{baseDir}/scripts/dex-agent.sh autotrade --market-id 1 --margin 1000000 --min-confidence 0.75
# guided onboarding + strategy capture + autotrade start
{baseDir}/scripts/dex-agent.sh onboard --market-id 1 --margin 1000000
```
## Files
- `scripts/balance.js`: user balance and order summary
- `scripts/order-execute.js`: user order submission helper
- `scripts/backend.js`: backend REST API query helper
- `scripts/ws-watch.js`: backend websocket channel subscriber
- `scripts/realtime-agent.js`: signal-driven auto-order loop
- `scripts/onboard.js`: interactive onboarding flow (wallet selection, registration wait, strategy capture, autotrade kickoff)
- `scripts/backend-common.js`: backend endpoint/auth helpers
- `scripts/common.js`: PDA, signer, tx, and decode utilities
- `package.json`: local runtime dependencies
- `.env.example`: required environment keys
## Setup
1. Copy `.env.example` to `.env`.
2. Fill signer and RPC values.
3. Run `dex-agent.sh install`.
4. Run `dex-agent.sh balance` first to validate access.
5. Run `dex-agent.sh backend doctor` and `dex-agent.sh watch --channel system.status`.
6. Run `dex-agent.sh onboard --market-id <id> --margin <u64>` for guided onboarding.
For env definitions and option details, read [references/dex-env.md](references/dex-env.md).
## Safety
- Keep `KEYPAIR_PATH` and private keys local.
- Use devnet/localnet unless explicitly instructed otherwise.
- Confirm `ORDER_ENGINE_PROGRAM_ID` and `MARKET_REGISTRY_PROGRAM_ID` before placing orders.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/dex-env.md
```markdown
# EasyClaw User DEX Env Reference
This skill is self-contained. Configure env in `easyclaw-skill/.env`.
## Required
- `SOLANA_RPC_URL` or `ANCHOR_PROVIDER_URL`
- `KEYPAIR_PATH` or `ANCHOR_WALLET`
## Optional Program ID Overrides
- `ORDER_ENGINE_PROGRAM_ID`
- `MARKET_REGISTRY_PROGRAM_ID`
If omitted, scripts use embedded defaults:
- `ORDER_ENGINE_PROGRAM_ID=GpMobZUKPtEE1eiZQAADo2ecD54JXhNHPNts5kPGwLtb`
- `MARKET_REGISTRY_PROGRAM_ID=BsA8fuyw8XqBMiUfpLbdiBwbKg8MZMHB1jdZzjs7c46q`
## `balance` Command
No additional env needed.
```bash
./scripts/dex-agent.sh balance
./scripts/dex-agent.sh balance --json
```
## `order` Command Options
Required:
- `--market-id <u64>`
- `--margin <u64>`
Optional:
- `--side buy|sell` (default: `buy`)
- `--type market|limit` (default: `market`)
- `--price <u64>` (required for limit)
- `--ttl <i64>` (default: `300`)
- `--client-order-id <u64>` (default: current unix seconds)
- `--reduce-only`
- `--deposit <u64>` (deposit collateral before placing order)
- `--skip-create-position` (do not auto-create user market position PDA)
Example:
```bash
./scripts/dex-agent.sh order --market-id 1 --side buy --type market --margin 1000000
```
## Backend API / WS Env
- `EASYCLAW_API_BASE_URL` (default: `http://127.0.0.1:8080`)
- `EASYCLAW_WS_URL` (default: derived from `EASYCLAW_API_BASE_URL` + `/ws`)
- `EASYCLAW_API_TOKEN` (optional, used for protected `/v1/*` endpoints)
## `backend` Command
Use backend REST APIs for:
- market/user data: positions, orders, fills, position history, orderbook heatmap, chart candles
- platform data: trades, portfolio, leaderboard, system status
- authenticated controls: agent/strategy/session/risk management and kill switch (`EASYCLAW_API_TOKEN` required)
```bash
./scripts/dex-agent.sh backend doctor
./scripts/dex-agent.sh backend health
./scripts/dex-agent.sh backend chart-candles --market BTCUSDT --timeframe 1m --limit 120
./scripts/dex-agent.sh backend positions --mine --limit 20
./scripts/dex-agent.sh backend position-history --mine --limit 20
./scripts/dex-agent.sh backend orderbook-heatmap --exchange binance --symbol BTCUSDT --limit 30
./scripts/dex-agent.sh backend orderbook-heatmap-aggregated --symbol-key BTCUSDT --limit 30
./scripts/dex-agent.sh backend trades --agent-id agent-001 --limit 50
./scripts/dex-agent.sh backend portfolio --period 7d
./scripts/dex-agent.sh backend leaderboard --metric pnl_pct --period 7d --min-trades 20
./scripts/dex-agent.sh backend system-status
# agent controls (auth required)
./scripts/dex-agent.sh backend agent-create --name "Momentum-01" --strategy-id strategy-001 --risk-profile-json '{"max_position_usdc":10000,"daily_loss_limit_usdc":500,"kill_switch_enabled":true}'
./scripts/dex-agent.sh backend agent --agent-id agent-001
./scripts/dex-agent.sh backend agent-owner-binding --agent-id agent-001
./scripts/dex-agent.sh backend agent-session-start --agent-id agent-001 --mode paper
./scripts/dex-agent.sh backend agent-session-stop --agent-id agent-001 --session-id sess-001
./scripts/dex-agent.sh backend agent-risk --agent-id agent-001
./scripts/dex-agent.sh backend agent-risk-patch --agent-id agent-001 --max-position-usdc 12000 --kill-switch-enabled true
./scripts/dex-agent.sh backend kill-switch --all
# strategy controls (auth required)
./scripts/dex-agent.sh backend strategy-templates
./scripts/dex-agent.sh backend strategy-create --name "My Strategy" --entry-rules-json '{}' --exit-rules-json '{}' --risk-defaults-json '{}'
./scripts/dex-agent.sh backend strategy --strategy-id strategy-001
./scripts/dex-agent.sh backend strategy-patch --strategy-id strategy-001 --name "My Strategy v2"
./scripts/dex-agent.sh backend strategy-publish --strategy-id strategy-001
```
## `watch` Command (WebSocket)
Subscribe to realtime channels from backend `/ws`.
```bash
./scripts/dex-agent.sh watch --channel system.status
./scripts/dex-agent.sh watch --channels "agent.signals,portfolio.updates"
./scripts/dex-agent.sh watch --channel market.price.BTCUSDT --json
./scripts/dex-agent.sh watch --channel chart.ticks.BTCUSDT --json
```
## `autotrade` Command
Subscribe to realtime signal channel and execute orders automatically.
```bash
./scripts/dex-agent.sh autotrade --market-id 1 --margin 1000000 --min-confidence 0.75
./scripts/dex-agent.sh autotrade --market-id 1 --margin 1000000 --dry-run
```
Notes:
- `autotrade` now retries websocket connections automatically on disconnect.
- `--strategy-file <path>` or `--strategy <text>` lets the runner include strategy rationale in execution reports.
## `onboard` Command
Guided flow for user onboarding:
1. discover/select wallet keypair
2. print wallet address for platform registration
3. wait for user confirmation (`done`)
4. collect/save strategy prompt to `state/strategies/`
5. ask final confirmation
6. start `autotrade` with reconnect + order rationale reporting
```bash
./scripts/dex-agent.sh onboard --market-id 1 --margin 1000000
./scripts/dex-agent.sh onboard --wallet-path ~/.config/solana/id.json --market-id 1 --margin 1000000 --dry-run
```
```
### scripts/balance.js
```javascript
#!/usr/bin/env node
const {
PublicKey,
decodeEngineConfig,
decodeOrder,
decodeUserMargin,
deriveEngineConfigPda,
deriveUserMarginPda,
getConnection,
orderStatusName,
orderTypeName,
parseArgs,
programIdsFromEnv,
readSigner,
sideName
} = require("./common");
const { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID } = require("@solana/spl-token");
function usage() {
console.log(`Usage:
node scripts/balance.js
node scripts/balance.js --json
Optional env:
SOLANA_RPC_URL or ANCHOR_PROVIDER_URL
KEYPAIR_PATH or ANCHOR_WALLET
ORDER_ENGINE_PROGRAM_ID
MARKET_REGISTRY_PROGRAM_ID
`);
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
return;
}
const { signer, keypairPath } = readSigner();
const { connection, rpc } = getConnection();
const { orderEngine } = programIdsFromEnv();
const engineConfigPda = deriveEngineConfigPda(orderEngine);
const userMarginPda = deriveUserMarginPda(signer.publicKey, orderEngine);
const engineConfigInfo = await connection.getAccountInfo(engineConfigPda);
if (!engineConfigInfo) {
throw new Error(`Engine config not found: ${engineConfigPda.toBase58()}`);
}
const engineConfig = decodeEngineConfig(engineConfigInfo.data);
const userMarginInfo = await connection.getAccountInfo(userMarginPda);
const userMargin = userMarginInfo ? decodeUserMargin(userMarginInfo.data) : null;
const usdcAta = getAssociatedTokenAddressSync(
engineConfig.usdcMint,
signer.publicKey,
false,
TOKEN_PROGRAM_ID
);
const usdcAtaInfo = await connection.getAccountInfo(usdcAta);
const usdcBalance = usdcAtaInfo
? await connection.getTokenAccountBalance(usdcAta)
: null;
const solLamports = await connection.getBalance(signer.publicKey, "confirmed");
const orderAccounts = await connection.getProgramAccounts(orderEngine, {
filters: [
{
memcmp: {
offset: 16,
bytes: userMarginPda.toBase58()
}
}
]
});
const decodedOrders = [];
for (const account of orderAccounts) {
try {
decodedOrders.push(decodeOrder(account.account.data, account.pubkey));
} catch (_ignored) {
}
}
decodedOrders.sort((a, b) => (a.id > b.id ? -1 : 1));
const openOrders = decodedOrders.filter((order) => order.status === 0);
if (args.json) {
const payload = {
rpc,
keypairPath,
wallet: signer.publicKey.toBase58(),
engineConfig: engineConfigPda.toBase58(),
userMargin: userMarginPda.toBase58(),
usdcMint: engineConfig.usdcMint.toBase58(),
usdcAta: usdcAta.toBase58(),
balances: {
solLamports: solLamports.toString(),
usdcRawAmount: usdcBalance ? usdcBalance.value.amount : "0",
usdcUiAmount: usdcBalance ? usdcBalance.value.uiAmountString : "0",
collateralRawAmount: userMargin ? userMargin.collateralBalance.toString() : "0"
},
orders: {
total: decodedOrders.length,
open: openOrders.length,
items: decodedOrders.map((order) => ({
pubkey: order.pubkey.toBase58(),
id: order.id.toString(),
marketId: order.marketId.toString(),
side: sideName(order.side),
orderType: orderTypeName(order.orderType),
reduceOnly: order.reduceOnly,
margin: order.margin.toString(),
price: order.price.toString(),
status: orderStatusName(order.status),
clientOrderId: order.clientOrderId.toString()
}))
}
};
console.log(JSON.stringify(payload, null, 2));
return;
}
console.log(`rpc: ${rpc}`);
console.log(`signer: ${signer.publicKey.toBase58()}`);
console.log(`keypair: ${keypairPath}`);
console.log(`engine_config: ${engineConfigPda.toBase58()}`);
console.log(`user_margin: ${userMarginPda.toBase58()}`);
console.log(`usdc_mint: ${engineConfig.usdcMint.toBase58()}`);
console.log(`usdc_ata: ${usdcAta.toBase58()}`);
console.log(`sol: ${solLamports} lamports`);
console.log(
`usdc: ${usdcBalance ? usdcBalance.value.amount : "0"} raw (${usdcBalance ? usdcBalance.value.uiAmountString : "0"} UI)`
);
if (!userMargin) {
console.log("margin_account: not initialized");
} else {
console.log(`collateral_balance: ${userMargin.collateralBalance.toString()}`);
console.log(`next_order_nonce: ${userMargin.nextOrderNonce.toString()}`);
console.log(`total_notional: ${userMargin.totalNotional.toString()}`);
}
console.log(`orders: total=${decodedOrders.length}, open=${openOrders.length}`);
for (const order of openOrders.slice(0, 20)) {
console.log(
`- id=${order.id.toString()} market=${order.marketId.toString()} side=${sideName(order.side)} type=${orderTypeName(order.orderType)} margin=${order.margin.toString()} price=${order.price.toString()} status=${orderStatusName(order.status)} pubkey=${order.pubkey.toBase58()}`
);
}
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("fetch failed")) {
console.error(`[error] RPC connection failed: ${message}`);
console.error(
"[hint] Set SOLANA_RPC_URL (or ANCHOR_PROVIDER_URL) to a reachable Solana RPC endpoint."
);
} else {
console.error(error);
}
process.exit(1);
});
```
### scripts/order-execute.js
```javascript
#!/usr/bin/env node
const {
DISCRIMINATORS,
SystemProgram,
TOKEN_PROGRAM_ID,
decodeEngineConfig,
decodeUserMargin,
deriveEngineConfigPda,
deriveGlobalConfigPda,
deriveMarketPda,
deriveOrderPda,
deriveUserMarginPda,
deriveUserMarketPositionPda,
ensureAtaExists,
getConnection,
instruction,
orderData,
parseArgs,
programIdsFromEnv,
readSigner,
sendInstructions,
usageNumberToBigInt,
u64Le
} = require("./common");
function usage() {
console.log(`Usage:
node scripts/order-execute.js --market-id 1 --side buy --type market --margin 1000000
node scripts/order-execute.js --market-id 2 --side sell --type limit --margin 2000000 --price 3000000000
node scripts/order-execute.js --market-id 1 --side buy --type market --margin 1000000 --deposit 5000000
Options:
--market-id <u64> Required
--side <buy|sell> Default: buy
--type <market|limit> Default: market
--margin <u64> Required, collateral amount for the order
--price <u64> Required for limit; optional for market
--ttl <i64> Default: 300
--client-order-id <u64> Default: current unix seconds
--reduce-only Optional flag
--deposit <u64> Optional collateral deposit before placing order
--skip-create-position Skip auto-creation of user market position PDA
`);
}
function sideToIndex(value) {
const normalized = String(value).toLowerCase();
if (normalized === "buy") return 0;
if (normalized === "sell") return 1;
throw new Error(`Invalid --side: ${value}`);
}
function typeToIndex(value) {
const normalized = String(value).toLowerCase();
if (normalized === "market") return 0;
if (normalized === "limit") return 1;
throw new Error(`Invalid --type: ${value}`);
}
function defaultMarketPrice(sideIndex) {
if (sideIndex === 0) {
return 100_000_000_000n;
}
return 1n;
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help) {
usage();
return;
}
if (!args["market-id"]) {
throw new Error("--market-id is required");
}
if (!args.margin) {
throw new Error("--margin is required");
}
const marketId = usageNumberToBigInt("market-id", args["market-id"]);
const side = sideToIndex(args.side || "buy");
const orderType = typeToIndex(args.type || "market");
const margin = usageNumberToBigInt("margin", args.margin);
const ttlSecs = args.ttl ? BigInt(args.ttl) : 300n;
const clientOrderId = args["client-order-id"]
? usageNumberToBigInt("client-order-id", args["client-order-id"])
: BigInt(Math.floor(Date.now() / 1000));
const reduceOnly = Boolean(args["reduce-only"]);
const depositAmount = args.deposit
? usageNumberToBigInt("deposit", args.deposit)
: 0n;
const skipCreatePosition = Boolean(args["skip-create-position"]);
const price = args.price
? usageNumberToBigInt("price", args.price)
: defaultMarketPrice(side);
if (orderType === 1 && price <= 0n) {
throw new Error("--price must be > 0 for limit orders");
}
const { signer, keypairPath } = readSigner();
const { connection, rpc } = getConnection();
const { orderEngine, marketRegistry } = programIdsFromEnv();
const engineConfigPda = deriveEngineConfigPda(orderEngine);
const globalConfigPda = deriveGlobalConfigPda(marketRegistry);
const marketPda = deriveMarketPda(marketId, marketRegistry);
const userMarginPda = deriveUserMarginPda(signer.publicKey, orderEngine);
const userMarketPositionPda = deriveUserMarketPositionPda(
userMarginPda,
marketId,
orderEngine
);
const engineConfigInfo = await connection.getAccountInfo(engineConfigPda);
if (!engineConfigInfo) {
throw new Error(`Engine config not found: ${engineConfigPda.toBase58()}`);
}
const engineConfig = decodeEngineConfig(engineConfigInfo.data);
const setupInstructions = [];
const userMarginInfo = await connection.getAccountInfo(userMarginPda);
if (!userMarginInfo) {
setupInstructions.push(
instruction(
orderEngine,
[
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: engineConfigPda, isSigner: false, isWritable: false },
{ pubkey: userMarginPda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
],
DISCRIMINATORS.CREATE_MARGIN_ACCOUNT
)
);
}
if (!skipCreatePosition) {
const userMarketPositionInfo = await connection.getAccountInfo(
userMarketPositionPda
);
if (!userMarketPositionInfo) {
setupInstructions.push(
instruction(
orderEngine,
[
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: engineConfigPda, isSigner: false, isWritable: false },
{ pubkey: userMarginPda, isSigner: false, isWritable: true },
{ pubkey: userMarketPositionPda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
],
Buffer.concat([
DISCRIMINATORS.CREATE_USER_MARKET_POSITION,
u64Le(marketId)
])
)
);
}
}
const { ata: userUsdcAta, createInstruction: createAtaIx } =
await ensureAtaExists(connection, signer, signer.publicKey, engineConfig.usdcMint);
if (createAtaIx) {
setupInstructions.push(createAtaIx);
}
if (depositAmount > 0n) {
setupInstructions.push(
instruction(
orderEngine,
[
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: engineConfigPda, isSigner: false, isWritable: false },
{ pubkey: userMarginPda, isSigner: false, isWritable: true },
{ pubkey: userUsdcAta, isSigner: false, isWritable: true },
{ pubkey: engineConfig.collateralVault, isSigner: false, isWritable: true },
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false }
],
Buffer.concat([DISCRIMINATORS.DEPOSIT_COLLATERAL, u64Le(depositAmount)])
)
);
}
const setupSig = await sendInstructions(connection, signer, setupInstructions);
if (setupSig) {
console.log(`[ok] setup_tx: ${setupSig}`);
}
const refreshedUserMarginInfo = await connection.getAccountInfo(userMarginPda);
if (!refreshedUserMarginInfo) {
throw new Error("User margin account is still missing after setup");
}
const userMargin = decodeUserMargin(refreshedUserMarginInfo.data);
const orderPda = deriveOrderPda(userMarginPda, userMargin.nextOrderNonce, orderEngine);
const placeIx = instruction(
orderEngine,
[
{ pubkey: signer.publicKey, isSigner: true, isWritable: true },
{ pubkey: engineConfigPda, isSigner: false, isWritable: false },
{ pubkey: marketRegistry, isSigner: false, isWritable: false },
{ pubkey: globalConfigPda, isSigner: false, isWritable: false },
{ pubkey: marketPda, isSigner: false, isWritable: false },
{ pubkey: userMarginPda, isSigner: false, isWritable: true },
{ pubkey: orderPda, isSigner: false, isWritable: true },
{ pubkey: SystemProgram.programId, isSigner: false, isWritable: false }
],
orderData({
marketId,
side,
orderType,
reduceOnly,
margin,
price,
ttlSecs,
clientOrderId
})
);
const placeSig = await sendInstructions(connection, signer, [placeIx]);
console.log(`[ok] place_order: ${placeSig}`);
console.log(`rpc: ${rpc}`);
console.log(`keypair: ${keypairPath}`);
console.log(`wallet: ${signer.publicKey.toBase58()}`);
console.log(`market_id: ${marketId.toString()}`);
console.log(`order_pda: ${orderPda.toBase58()}`);
console.log(`order_nonce: ${userMargin.nextOrderNonce.toString()}`);
console.log(`side: ${side === 0 ? "buy" : "sell"}`);
console.log(`type: ${orderType === 0 ? "market" : "limit"}`);
console.log(`margin: ${margin.toString()}`);
console.log(`price: ${price.toString()}`);
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
if (message.includes("fetch failed")) {
console.error(`[error] RPC connection failed: ${message}`);
console.error(
"[hint] Set SOLANA_RPC_URL (or ANCHOR_PROVIDER_URL) to a reachable Solana RPC endpoint."
);
} else {
console.error(error);
}
process.exit(1);
});
```
### scripts/backend.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const {
deriveUserMarginPda,
parseArgs,
programIdsFromEnv,
readSigner
} = require("./common");
const { apiBaseUrl, apiRequest, wsUrl } = require("./backend-common");
function usage() {
console.log(`Usage:
node scripts/backend.js doctor
node scripts/backend.js health
node scripts/backend.js system-status
node scripts/backend.js chart-candles [--market <symbol>] [--timeframe <1m|5m|15m|1h|4h|1d>] [--limit <n>]
node scripts/backend.js positions [--mine] [--user-margin <pda>] [--market-id <id>] [--limit <n>] [--offset <n>]
node scripts/backend.js orders [--mine] [--user-margin <pda>] [--user-pubkey <pk>] [--status <Open|Executed|...>] [--market-id <id>]
node scripts/backend.js fills [--mine] [--user-margin <pda>] [--user-pubkey <pk>] [--market-id <id>]
node scripts/backend.js position-history [--mine] [--user-margin <pda>] [--market-id <id>] [--limit <n>] [--offset <n>]
node scripts/backend.js orderbook-heatmap --exchange <binance|okx|...> --symbol <pair> [--from <unix>] [--to <unix>] [--limit <n>] [--offset <n>]
node scripts/backend.js orderbook-heatmap-aggregated --symbol-key <key> [--from <unix>] [--to <unix>] [--limit <n>] [--offset <n>]
node scripts/backend.js trades [--agent-id <id>] [--from <unix>] [--to <unix>] [--limit <n>] [--offset <n>]
node scripts/backend.js portfolio [--period <7d|30d|all>]
node scripts/backend.js agent-portfolio --agent-id <id> [--period <7d|30d|all>]
node scripts/backend.js leaderboard [--metric <win_rate|pnl_pct>] [--period <all_time|30d|7d>] [--min-trades <n>]
node scripts/backend.js agents
node scripts/backend.js agent-create --name <text> --strategy-id <id> [--risk-profile-json <json>]
node scripts/backend.js agent --agent-id <id>
node scripts/backend.js agent-owner-binding --agent-id <id>
node scripts/backend.js agent-owner-rebind --agent-id <id> --challenge-id <id> --signature <sig>
node scripts/backend.js agent-session-start --agent-id <id> --mode <paper|live>
node scripts/backend.js agent-session-stop --agent-id <id> --session-id <id>
node scripts/backend.js agent-risk --agent-id <id>
node scripts/backend.js agent-risk-patch --agent-id <id> [--max-position-usdc <n>] [--daily-loss-limit-usdc <n>] [--kill-switch-enabled <true|false>]
node scripts/backend.js kill-switch [--all] [--agent-id <id>] [--agent-ids <csv>]
node scripts/backend.js strategy-templates
node scripts/backend.js strategy-create --name <text> [--entry-rules-json <json>] [--exit-rules-json <json>] [--risk-defaults-json <json>]
node scripts/backend.js strategy --strategy-id <id>
node scripts/backend.js strategy-patch --strategy-id <id> [--name <text>] [--entry-rules-json <json>] [--exit-rules-json <json>]
node scripts/backend.js strategy-publish --strategy-id <id>
node scripts/backend.js auth-challenge --wallet-pubkey <pk> --intent <owner_bind|session|live_stepup>
node scripts/backend.js auth-verify --challenge-id <id> --signature <sig> --wallet-pubkey <pk>
node scripts/backend.js auth-refresh [--token <session_token>]
Options:
--json Print raw JSON output
--token <jwt> Override EASYCLAW_API_TOKEN for auth endpoints
`);
}
function parseMainArgs(argv) {
const command = argv[0] || "help";
const args = parseArgs(argv.slice(1));
return { command, args };
}
function parseBoolFlag(value) {
if (value === true) return true;
if (typeof value !== "string") return false;
const normalized = value.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes";
}
function maybeNumber(raw) {
if (raw === undefined || raw === null || String(raw).trim() === "") {
return undefined;
}
const value = Number(raw);
if (!Number.isFinite(value)) {
throw new Error(`Invalid numeric value: ${raw}`);
}
return value;
}
function maybeBoolean(raw, name) {
if (raw === undefined || raw === null || String(raw).trim() === "") {
return undefined;
}
if (raw === true) {
return true;
}
if (typeof raw !== "string") {
throw new Error(`Invalid boolean value for ${name}: ${raw}`);
}
const normalized = raw.trim().toLowerCase();
if (normalized === "1" || normalized === "true" || normalized === "yes") {
return true;
}
if (normalized === "0" || normalized === "false" || normalized === "no") {
return false;
}
throw new Error(`Invalid boolean value for ${name}: ${raw}`);
}
function parseJSONString(raw, name) {
if (raw === undefined || raw === null || String(raw).trim() === "") {
return undefined;
}
const value = String(raw).trim();
try {
return JSON.parse(value);
} catch (_error) {
throw new Error(`Invalid JSON for ${name}`);
}
}
function parseJSONObject(raw, name, fallback) {
const value = parseJSONString(raw, name);
if (value === undefined) {
return fallback;
}
if (value === null || Array.isArray(value) || typeof value !== "object") {
throw new Error(`${name} must be a JSON object`);
}
return value;
}
function requiredString(raw, flagName) {
const value = String(raw || "").trim();
if (!value) {
throw new Error(`${flagName} is required`);
}
return value;
}
function parseCSV(raw) {
if (raw === undefined || raw === null || String(raw).trim() === "") {
return [];
}
return String(raw)
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function mineIdentity() {
const { signer } = readSigner();
const { orderEngine } = programIdsFromEnv();
const userMargin = deriveUserMarginPda(signer.publicKey, orderEngine);
return {
userPubkey: signer.publicKey.toBase58(),
userMargin: userMargin.toBase58()
};
}
function jsonOrPrint(payload, outputAsJSON) {
if (outputAsJSON) {
console.log(JSON.stringify(payload, null, 2));
return;
}
if (payload === null || payload === undefined) {
console.log("(empty)");
return;
}
if (typeof payload === "string") {
console.log(payload);
return;
}
console.log(JSON.stringify(payload, null, 2));
}
async function runCommand(command, args) {
const outputAsJSON = parseBoolFlag(args.json);
const token = args.token;
switch (command) {
case "doctor": {
const payload = {
apiBaseUrl: apiBaseUrl(),
wsUrl: wsUrl(),
hasAuthToken:
Boolean(process.env.EASYCLAW_API_TOKEN) ||
Boolean(process.env.API_AUTH_TOKEN) ||
Boolean(args.token),
timestamp: Math.floor(Date.now() / 1000)
};
jsonOrPrint(payload, outputAsJSON);
return;
}
case "health": {
const payload = await apiRequest("/healthz");
jsonOrPrint(payload, outputAsJSON);
return;
}
case "system-status": {
const payload = await apiRequest("/v1/system/status");
jsonOrPrint(payload, outputAsJSON);
return;
}
case "chart-candles": {
const payload = await apiRequest("/v1/chart/candles", {
query: {
market: args.market,
timeframe: args.timeframe,
limit: args.limit
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "positions": {
const mine = parseBoolFlag(args.mine);
const identity = mine ? mineIdentity() : null;
const payload = await apiRequest("/api/v1/positions", {
query: {
user_margin: args["user-margin"] || (identity ? identity.userMargin : undefined),
market_id: args["market-id"],
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "orders": {
const mine = parseBoolFlag(args.mine);
const identity = mine ? mineIdentity() : null;
const payload = await apiRequest("/api/v1/orders", {
query: {
user_margin: args["user-margin"] || (identity ? identity.userMargin : undefined),
user_pubkey: args["user-pubkey"] || (identity ? identity.userPubkey : undefined),
market_id: args["market-id"],
status: args.status,
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "fills": {
const mine = parseBoolFlag(args.mine);
const identity = mine ? mineIdentity() : null;
const payload = await apiRequest("/api/v1/fills", {
query: {
user_margin: args["user-margin"] || (identity ? identity.userMargin : undefined),
user_pubkey: args["user-pubkey"] || (identity ? identity.userPubkey : undefined),
market_id: args["market-id"],
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "position-history": {
const mine = parseBoolFlag(args.mine);
const identity = mine ? mineIdentity() : null;
const payload = await apiRequest("/api/v1/position-history", {
query: {
user_margin: args["user-margin"] || (identity ? identity.userMargin : undefined),
market_id: args["market-id"],
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "orderbook-heatmap": {
const exchange = requiredString(args.exchange, "--exchange");
const symbol = requiredString(args.symbol, "--symbol");
const payload = await apiRequest("/api/v1/orderbook-heatmap", {
query: {
exchange,
symbol,
from: maybeNumber(args.from),
to: maybeNumber(args.to),
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "orderbook-heatmap-aggregated": {
const symbolKey = requiredString(args["symbol-key"], "--symbol-key");
const payload = await apiRequest("/api/v1/orderbook-heatmap-aggregated", {
query: {
symbol_key: symbolKey,
from: maybeNumber(args.from),
to: maybeNumber(args.to),
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "trades": {
const payload = await apiRequest("/v1/trades", {
query: {
agent_id: args["agent-id"],
from: maybeNumber(args.from),
to: maybeNumber(args.to),
limit: args.limit,
offset: args.offset
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "portfolio": {
const payload = await apiRequest("/v1/portfolio", {
query: {
period: args.period || "7d"
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-portfolio": {
const agentId = String(args["agent-id"] || "").trim();
if (!agentId) {
throw new Error("--agent-id is required");
}
const payload = await apiRequest(`/v1/portfolio/agents/${encodeURIComponent(agentId)}`, {
query: {
period: args.period || "7d"
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "leaderboard": {
const payload = await apiRequest("/v1/leaderboard", {
query: {
metric: args.metric || "pnl_pct",
period: args.period || "7d",
min_trades: args["min-trades"] || 20
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agents": {
const payload = await apiRequest("/v1/agents");
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const payload = await apiRequest(`/v1/agents/${encodeURIComponent(agentID)}`);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-owner-binding": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const payload = await apiRequest(
`/v1/agents/${encodeURIComponent(agentID)}/owner-binding`
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-owner-rebind": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const challengeID = requiredString(args["challenge-id"], "--challenge-id");
const signature = requiredString(args.signature, "--signature");
const payload = await apiRequest(
`/v1/agents/${encodeURIComponent(agentID)}/owner-binding/rebind`,
{
method: "POST",
body: {
challenge_id: challengeID,
signature
},
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-session-start": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const mode = requiredString(args.mode, "--mode");
const payload = await apiRequest(
`/v1/agents/${encodeURIComponent(agentID)}/sessions`,
{
method: "POST",
body: { mode },
requireAuth: true,
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-session-stop": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const sessionID = requiredString(args["session-id"], "--session-id");
const payload = await apiRequest(
`/v1/agents/${encodeURIComponent(agentID)}/sessions/${encodeURIComponent(
sessionID
)}`,
{
method: "DELETE",
requireAuth: true,
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-risk": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const payload = await apiRequest(`/v1/agents/${encodeURIComponent(agentID)}/risk`);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-risk-patch": {
const agentID = requiredString(args["agent-id"], "--agent-id");
const maxPositionUSDC = maybeNumber(args["max-position-usdc"]);
const dailyLossLimitUSDC = maybeNumber(args["daily-loss-limit-usdc"]);
const killSwitchEnabled = maybeBoolean(
args["kill-switch-enabled"],
"--kill-switch-enabled"
);
const body = {};
if (maxPositionUSDC !== undefined) {
body.max_position_usdc = maxPositionUSDC;
}
if (dailyLossLimitUSDC !== undefined) {
body.daily_loss_limit_usdc = dailyLossLimitUSDC;
}
if (killSwitchEnabled !== undefined) {
body.kill_switch_enabled = killSwitchEnabled;
}
if (Object.keys(body).length === 0) {
throw new Error(
"At least one of --max-position-usdc, --daily-loss-limit-usdc, --kill-switch-enabled is required"
);
}
const payload = await apiRequest(
`/v1/agents/${encodeURIComponent(agentID)}/risk`,
{
method: "PATCH",
body,
requireAuth: true,
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "kill-switch": {
const all = parseBoolFlag(args.all);
const agentIDs = [
...parseCSV(args["agent-ids"]),
...parseCSV(args["agent-id"])
];
const body = {
agent_ids: all ? ["all"] : agentIDs
};
if (!all && body.agent_ids.length === 0) {
throw new Error("Pass --all or --agent-ids <csv> (or --agent-id <id>)");
}
const payload = await apiRequest("/v1/safety/kill-switch", {
method: "POST",
body,
requireAuth: true,
token
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "strategy-templates": {
const payload = await apiRequest("/v1/strategy/templates");
jsonOrPrint(payload, outputAsJSON);
return;
}
case "strategy-create": {
const name = requiredString(args.name, "--name");
const entryRules = parseJSONObject(args["entry-rules-json"], "--entry-rules-json", {});
const exitRules = parseJSONObject(args["exit-rules-json"], "--exit-rules-json", {});
const riskDefaults = parseJSONObject(
args["risk-defaults-json"],
"--risk-defaults-json",
{}
);
const payload = await apiRequest("/v1/strategies", {
method: "POST",
body: {
name,
entry_rules: entryRules,
exit_rules: exitRules,
risk_defaults: riskDefaults
},
requireAuth: true,
token
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "strategy": {
const strategyID = requiredString(args["strategy-id"], "--strategy-id");
const payload = await apiRequest(`/v1/strategies/${encodeURIComponent(strategyID)}`);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "strategy-patch": {
const strategyID = requiredString(args["strategy-id"], "--strategy-id");
const name = args.name !== undefined ? String(args.name).trim() : undefined;
const entryRules = parseJSONObject(
args["entry-rules-json"],
"--entry-rules-json",
undefined
);
const exitRules = parseJSONObject(
args["exit-rules-json"],
"--exit-rules-json",
undefined
);
const body = {};
if (name !== undefined && name !== "") {
body.name = name;
}
if (entryRules !== undefined) {
body.entry_rules = entryRules;
}
if (exitRules !== undefined) {
body.exit_rules = exitRules;
}
if (Object.keys(body).length === 0) {
throw new Error(
"At least one of --name, --entry-rules-json, --exit-rules-json is required"
);
}
const payload = await apiRequest(
`/v1/strategies/${encodeURIComponent(strategyID)}`,
{
method: "PATCH",
body,
requireAuth: true,
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "strategy-publish": {
const strategyID = requiredString(args["strategy-id"], "--strategy-id");
const payload = await apiRequest(
`/v1/strategies/${encodeURIComponent(strategyID)}/publish`,
{
method: "POST",
requireAuth: true,
token
}
);
jsonOrPrint(payload, outputAsJSON);
return;
}
case "agent-create": {
const name = requiredString(args.name, "--name");
const strategyID = requiredString(args["strategy-id"], "--strategy-id");
const riskProfile = parseJSONObject(
args["risk-profile-json"],
"--risk-profile-json",
undefined
);
const body = {
name,
strategy_id: strategyID
};
if (riskProfile !== undefined) {
body.risk_profile = riskProfile;
}
const payload = await apiRequest("/v1/agents", {
method: "POST",
body,
requireAuth: true,
token
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "auth-challenge": {
const walletPubkey = String(args["wallet-pubkey"] || "").trim();
const intent = String(args.intent || "session").trim();
if (!walletPubkey) {
throw new Error("--wallet-pubkey is required");
}
const payload = await apiRequest("/v1/auth/challenge", {
method: "POST",
body: {
wallet_pubkey: walletPubkey,
intent
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "auth-verify": {
const challengeID = String(args["challenge-id"] || "").trim();
const signature = String(args.signature || "").trim();
const walletPubkey = String(args["wallet-pubkey"] || "").trim();
if (!challengeID || !signature || !walletPubkey) {
throw new Error("--challenge-id, --signature, --wallet-pubkey are required");
}
const payload = await apiRequest("/v1/auth/verify-signature", {
method: "POST",
body: {
challenge_id: challengeID,
signature,
wallet_pubkey: walletPubkey
}
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "auth-refresh": {
const payload = await apiRequest("/v1/auth/session/refresh", {
method: "POST",
requireAuth: true,
token
});
jsonOrPrint(payload, outputAsJSON);
return;
}
case "help":
case "-h":
case "--help":
usage();
return;
default:
throw new Error(`Unknown backend command: ${command}`);
}
}
async function main() {
const { command, args } = parseMainArgs(process.argv.slice(2));
await runCommand(command, args);
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[error] ${message}`);
process.exit(1);
});
```
### scripts/ws-watch.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const WebSocket = require("ws");
const { parseArgs } = require("./common");
const { wsUrl } = require("./backend-common");
function usage() {
console.log(`Usage:
node scripts/ws-watch.js --channel system.status
node scripts/ws-watch.js --channels "agent.signals,portfolio.updates"
node scripts/ws-watch.js --channel "chart.ticks.1" --json
Options:
--channel <name> Single channel
--channels <csv> Comma separated channels (alternative)
--duration-sec <n> Auto exit after n seconds
--once Exit on first event
--json Print raw event JSON
`);
}
function parseChannels(args) {
const raw =
(typeof args.channels === "string" && args.channels) ||
(typeof args.channel === "string" && args.channel) ||
"system.status";
return raw
.split(",")
.map((item) => item.trim())
.filter(Boolean);
}
function toBool(value) {
if (value === true) return true;
if (typeof value !== "string") return false;
const normalized = value.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes";
}
function toInt(value, fallback) {
if (value === undefined || value === null || String(value).trim() === "") {
return fallback;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(`Invalid number: ${value}`);
}
return Math.floor(parsed);
}
function prettyPrint(message) {
const channel = message.channel || "(unknown)";
const ts = message.ts ? new Date(message.ts * 1000).toISOString() : new Date().toISOString();
const eventType = message.type || "event";
if (message.error) {
console.log(`[${ts}] ${eventType} ${channel} error=${message.error}`);
return;
}
const data =
message.data === undefined || message.data === null
? "null"
: JSON.stringify(message.data);
console.log(`[${ts}] ${eventType} ${channel} data=${data}`);
}
function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) {
usage();
return;
}
const channels = parseChannels(args);
const outputJSON = toBool(args.json);
const once = toBool(args.once);
const durationSec = toInt(args["duration-sec"], 0);
const endpoint = wsUrl();
console.log(`[info] ws=${endpoint}`);
console.log(`[info] channels=${channels.join(",")}`);
const socket = new WebSocket(endpoint);
let received = 0;
let timer = null;
if (durationSec > 0) {
timer = setTimeout(() => {
console.log(`[info] duration reached (${durationSec}s), closing`);
socket.close(1000, "duration reached");
}, durationSec * 1000);
}
socket.on("open", () => {
for (const channel of channels) {
socket.send(
JSON.stringify({
type: "subscribe",
channel
})
);
}
});
socket.on("message", (raw) => {
let message;
try {
message = JSON.parse(String(raw));
} catch (_err) {
message = { type: "raw", data: String(raw), ts: Math.floor(Date.now() / 1000) };
}
received += 1;
if (outputJSON) {
console.log(JSON.stringify(message, null, 2));
} else {
prettyPrint(message);
}
if (once) {
socket.close(1000, "once completed");
}
});
socket.on("error", (error) => {
console.error(`[error] websocket: ${error.message}`);
process.exitCode = 1;
});
socket.on("close", (code, reason) => {
if (timer) {
clearTimeout(timer);
}
const message = reason ? reason.toString() : "";
console.log(`[info] closed code=${code} reason=${message} events=${received}`);
});
const shutdown = () => {
socket.close(1000, "signal");
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
}
main();
```
### scripts/realtime-agent.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const fs = require("node:fs");
const path = require("node:path");
const { spawn } = require("node:child_process");
const WebSocket = require("ws");
const {
deriveUserMarginPda,
parseArgs,
programIdsFromEnv,
readSigner
} = require("./common");
const { apiRequest, wsUrl } = require("./backend-common");
function usage() {
console.log(`Usage:
node scripts/realtime-agent.js --market-id 1 --margin 1000000
node scripts/realtime-agent.js --market-id 1 --margin 1000000 --min-confidence 0.75 --agent-name "Alpha Momentum"
node scripts/realtime-agent.js --market-id 1 --margin 1000000 --strategy-file ./state/strategies/strategy.txt
Options:
--market-id <u64> Required
--margin <u64> Required order margin
--type <market|limit> Default: market
--price <u64> Optional fixed price for limit orders
--ttl <i64> Default: 300
--deposit <u64> Optional collateral deposit before order
--reduce-only Optional flag
--min-confidence <0..1> Default: 0.7
--agent-name <name> Optional signal agent filter
--cooldown-ms <ms> Default: 15000
--max-orders <n> Default: 0 (unlimited)
--channel <name> Default: agent.signals
--strategy <text> Optional strategy prompt text
--strategy-file <path> Optional strategy prompt file
--reconnect-base-delay-ms <ms> Default: 1000
--reconnect-max-delay-ms <ms> Default: 30000
--max-reconnect-attempts <n> Default: 0 (unlimited)
--dry-run Do not place real orders
`);
}
function toBool(value) {
if (value === true) return true;
if (typeof value !== "string") return false;
const normalized = value.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes";
}
function toNumber(value, fallback) {
if (value === undefined || value === null || String(value).trim() === "") {
return fallback;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(`Invalid numeric value: ${value}`);
}
return parsed;
}
function toInt(value, fallback) {
if (value === undefined || value === null || String(value).trim() === "") {
return fallback;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(`Invalid integer value: ${value}`);
}
return Math.floor(parsed);
}
function normalizeSignalSide(side) {
const normalized = String(side || "").trim().toLowerCase();
if (normalized === "long" || normalized === "buy" || normalized === "bid") {
return "buy";
}
if (normalized === "short" || normalized === "sell" || normalized === "ask") {
return "sell";
}
return "";
}
function signalItemsFromEnvelope(envelope) {
if (!envelope || envelope.type !== "event") {
return [];
}
const data = envelope.data;
if (!data) {
return [];
}
if (Array.isArray(data)) {
return data;
}
if (typeof data === "object") {
if (Array.isArray(data.items)) {
return data.items;
}
if (data.agent_name || data.side) {
return [data];
}
}
return [];
}
function normalizeInlineText(value) {
return String(value || "")
.replace(/\s+/g, " ")
.trim();
}
function compactStrategySummary(strategyPrompt) {
const normalized = normalizeInlineText(strategyPrompt);
if (!normalized) {
return "(none)";
}
if (normalized.length <= 120) {
return normalized;
}
return `${normalized.slice(0, 117)}...`;
}
function readStrategyPrompt(args) {
const filePath = String(args["strategy-file"] || "").trim();
if (filePath) {
const absolutePath = path.resolve(filePath);
if (!fs.existsSync(absolutePath)) {
throw new Error(`strategy file not found: ${absolutePath}`);
}
const fileText = fs.readFileSync(absolutePath, "utf8").trim();
return {
strategyPrompt: fileText,
strategySource: absolutePath
};
}
const inline = String(args.strategy || "").trim();
return {
strategyPrompt: inline,
strategySource: inline ? "inline" : ""
};
}
function buildDecisionReason(signal, strategyPrompt) {
const details = [];
const explicitReason = normalizeInlineText(
signal.reason || signal.rationale || signal.thesis || signal.note || signal.comment || ""
);
if (explicitReason) {
details.push(explicitReason);
}
const signalPrice = String(signal.price || "").trim();
if (signalPrice) {
details.push(`signal_price=${signalPrice}`);
}
const strategySnippet = compactStrategySummary(strategyPrompt);
if (strategySnippet !== "(none)") {
details.push(`strategy="${strategySnippet}"`);
}
if (details.length === 0) {
return "signal side/confidence matched filters";
}
return details.join("; ");
}
async function executeOrder(orderArgs) {
const scriptPath = path.join(__dirname, "order-execute.js");
const args = [scriptPath, ...orderArgs];
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
cwd: path.join(__dirname, ".."),
stdio: ["ignore", "pipe", "pipe"]
});
let stdout = "";
let stderr = "";
child.stdout.on("data", (chunk) => {
stdout += String(chunk);
});
child.stderr.on("data", (chunk) => {
stderr += String(chunk);
});
child.on("error", (error) => {
reject(error);
});
child.on("close", (code) => {
if (code === 0) {
resolve({ stdout, stderr });
return;
}
reject(new Error(`order-execute exited with code=${code} stderr=${stderr.trim()}`));
});
});
}
function buildOrderArgs(config, side) {
const out = [
"--market-id",
config.marketId,
"--side",
side,
"--type",
config.orderType,
"--margin",
config.margin,
"--ttl",
config.ttl
];
if (config.orderType === "limit") {
out.push("--price", config.price);
}
if (config.deposit) {
out.push("--deposit", config.deposit);
}
if (config.reduceOnly) {
out.push("--reduce-only");
}
return out;
}
function bigintFromText(raw) {
const value = String(raw || "").trim();
if (!/^-?\d+$/.test(value)) {
return 0n;
}
return BigInt(value);
}
function summarizePositionItem(position) {
const longQty = bigintFromText(position.long_qty);
const shortQty = bigintFromText(position.short_qty);
const longNotional = bigintFromText(position.long_entry_notional);
const shortNotional = bigintFromText(position.short_entry_notional);
const marketID = String(position.market_id || "").trim() || "?";
return `market=${marketID} long_qty=${longQty.toString()} short_qty=${shortQty.toString()} long_notional=${longNotional.toString()} short_notional=${shortNotional.toString()}`;
}
async function printPositionSnapshot(userMargin) {
try {
const [positions, history] = await Promise.all([
apiRequest("/api/v1/positions", {
query: {
user_margin: userMargin,
limit: 20,
offset: 0
}
}),
apiRequest("/api/v1/position-history", {
query: {
user_margin: userMargin,
limit: 5,
offset: 0
}
})
]);
const positionItems = Array.isArray(positions?.items) ? positions.items : [];
const historyItems = Array.isArray(history?.items) ? history.items : [];
const activePositions = positionItems.filter((item) => {
return bigintFromText(item.long_qty) > 0n || bigintFromText(item.short_qty) > 0n;
});
console.log(
`[state] positions_total=${positionItems.length} positions_active=${activePositions.length} recent_history=${historyItems.length} user_margin=${userMargin}`
);
for (const position of activePositions.slice(0, 3)) {
console.log(`[state] ${summarizePositionItem(position)}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`[warn] snapshot fetch failed: ${message}`);
}
}
function createRuntimeState() {
return {
seenSignals: new Set(),
lastExecutionAt: 0,
activeOrder: Promise.resolve(),
placedOrders: 0,
reconnectAttempts: 0,
reconnectTimer: null,
socket: null,
stopRequested: false,
stopReason: "",
maxOrders: 0,
maybeFinalize: null
};
}
function requestStop(state, reason) {
if (state.stopRequested) {
return;
}
state.stopRequested = true;
state.stopReason = reason;
if (state.reconnectTimer) {
clearTimeout(state.reconnectTimer);
state.reconnectTimer = null;
}
if (state.socket) {
try {
const safeReason = String(reason || "stop").slice(0, 120);
if (
state.socket.readyState === WebSocket.OPEN ||
state.socket.readyState === WebSocket.CONNECTING
) {
state.socket.close(1000, safeReason);
}
} catch (_ignored) {
}
}
if (typeof state.maybeFinalize === "function") {
state.maybeFinalize();
}
}
function scheduleReconnect(state, config, connect) {
if (state.stopRequested) {
return;
}
const nextAttempt = state.reconnectAttempts + 1;
if (config.maxReconnectAttempts > 0 && nextAttempt > config.maxReconnectAttempts) {
console.error("[error] maximum websocket reconnect attempts reached");
process.exitCode = 1;
requestStop(state, "max reconnect attempts reached");
return;
}
state.reconnectAttempts = nextAttempt;
const backoff = Math.min(
config.reconnectMaxDelayMs,
config.reconnectBaseDelayMs * (2 ** (nextAttempt - 1))
);
console.log(
`[warn] websocket reconnect scheduled in ${backoff}ms (attempt=${nextAttempt})`
);
state.reconnectTimer = setTimeout(() => {
state.reconnectTimer = null;
connect();
}, backoff);
}
function enqueueSignalExecution(signal, side, confidence, ts, config, state, userMargin) {
const signalAgent = String(signal.agent_name || "").trim() || "unknown-agent";
const dedupeKey = `${signalAgent}:${side}:${ts}`;
if (state.seenSignals.has(dedupeKey)) {
return;
}
state.seenSignals.add(dedupeKey);
state.activeOrder = state.activeOrder
.then(async () => {
if (state.stopRequested) {
return;
}
const nowMs = Date.now();
if (config.cooldownMs > 0 && nowMs - state.lastExecutionAt < config.cooldownMs) {
return;
}
if (config.maxOrders > 0 && state.placedOrders >= config.maxOrders) {
requestStop(state, "max orders reached");
return;
}
const orderArgs = buildOrderArgs(config, side);
const reason = buildDecisionReason(signal, config.strategyPrompt);
console.log(
`[decision] agent=${signalAgent} side=${side} confidence=${confidence.toFixed(4)} ts=${ts} reason=${JSON.stringify(reason)}`
);
if (config.dryRun) {
console.log(`[dry-run] node scripts/order-execute.js ${orderArgs.join(" ")}`);
} else {
const result = await executeOrder(orderArgs);
if (result.stdout.trim()) {
process.stdout.write(result.stdout);
}
if (result.stderr.trim()) {
process.stderr.write(result.stderr);
}
}
state.lastExecutionAt = Date.now();
state.placedOrders += 1;
console.log(
`[report] placed_orders=${state.placedOrders} side=${side} confidence=${confidence.toFixed(4)} rationale=${JSON.stringify(reason)}`
);
await printPositionSnapshot(userMargin);
if (config.maxOrders > 0 && state.placedOrders >= config.maxOrders) {
console.log(`[info] max orders reached (${config.maxOrders}), stopping`);
requestStop(state, "max orders reached");
}
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[error] order execution failed: ${message}`);
});
}
function connectWebsocket(config, state, userMargin) {
if (state.stopRequested) {
return;
}
const endpoint = wsUrl();
const socket = new WebSocket(endpoint);
state.socket = socket;
let openedAt = 0;
socket.on("open", () => {
openedAt = Date.now();
state.reconnectAttempts = 0;
console.log(`[info] websocket connected: ${endpoint}`);
socket.send(
JSON.stringify({
type: "subscribe",
channel: config.channel
})
);
});
socket.on("message", (raw) => {
let envelope;
try {
envelope = JSON.parse(String(raw));
} catch (_err) {
return;
}
const signals = signalItemsFromEnvelope(envelope);
for (const signal of signals) {
const signalAgent = String(signal.agent_name || "").trim();
const side = normalizeSignalSide(signal.side);
const confidence = toNumber(signal.confidence, 0);
const ts = Math.floor(toNumber(signal.ts, Math.floor(Date.now() / 1000)));
if (!side) {
continue;
}
if (config.agentNameFilter && signalAgent !== config.agentNameFilter) {
continue;
}
if (confidence < config.minConfidence) {
continue;
}
enqueueSignalExecution(signal, side, confidence, ts, config, state, userMargin);
}
});
socket.on("error", (error) => {
console.error(`[error] websocket: ${error.message}`);
});
socket.on("close", (code, reason) => {
const reasonText = reason ? String(reason) : "";
const openMs = openedAt > 0 ? Date.now() - openedAt : 0;
console.log(`[warn] websocket closed code=${code} reason=${reasonText} open_ms=${openMs}`);
if (state.socket === socket) {
state.socket = null;
}
if (state.stopRequested) {
if (typeof state.maybeFinalize === "function") {
state.maybeFinalize();
}
return;
}
scheduleReconnect(state, config, () => {
connectWebsocket(config, state, userMargin);
});
});
}
async function runRealtimeTrading(config, userMargin) {
const state = createRuntimeState();
state.maxOrders = config.maxOrders;
return new Promise((resolve) => {
let finished = false;
const finish = () => {
if (finished) {
return;
}
finished = true;
process.off("SIGINT", handleSignal);
process.off("SIGTERM", handleSignal);
resolve();
};
state.maybeFinalize = () => {
if (!state.stopRequested) {
return;
}
if (state.socket) {
return;
}
if (state.reconnectTimer) {
return;
}
state.activeOrder
.catch(() => {
})
.finally(() => {
finish();
});
};
const handleSignal = () => {
console.log("[info] shutdown signal received");
requestStop(state, "signal");
};
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
connectWebsocket(config, state, userMargin);
});
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) {
usage();
return;
}
const marketId = String(args["market-id"] || "").trim();
const margin = String(args.margin || "").trim();
if (!marketId) {
throw new Error("--market-id is required");
}
if (!margin) {
throw new Error("--margin is required");
}
const orderType = String(args.type || "market").trim().toLowerCase();
if (orderType !== "market" && orderType !== "limit") {
throw new Error("--type must be market or limit");
}
const price = String(args.price || "").trim();
if (orderType === "limit" && !price) {
throw new Error("--price is required for limit orders");
}
const ttl = String(args.ttl || "300").trim();
const deposit = String(args.deposit || "").trim();
const reduceOnly = toBool(args["reduce-only"]);
const minConfidence = toNumber(args["min-confidence"], 0.7);
const cooldownMs = Math.max(0, toInt(args["cooldown-ms"], 15000));
const maxOrders = Math.max(0, toInt(args["max-orders"], 0));
const channel = String(args.channel || "agent.signals").trim();
const agentNameFilter = String(args["agent-name"] || "").trim();
const dryRun = toBool(args["dry-run"]);
const reconnectBaseDelayMs = Math.max(250, toInt(args["reconnect-base-delay-ms"], 1000));
const reconnectMaxDelayMs = Math.max(
reconnectBaseDelayMs,
toInt(args["reconnect-max-delay-ms"], 30000)
);
const maxReconnectAttempts = Math.max(0, toInt(args["max-reconnect-attempts"], 0));
const { strategyPrompt, strategySource } = readStrategyPrompt(args);
const { signer } = readSigner();
const { orderEngine } = programIdsFromEnv();
const userMargin = deriveUserMarginPda(signer.publicKey, orderEngine).toBase58();
const config = {
marketId,
margin,
orderType,
price,
ttl,
deposit,
reduceOnly,
minConfidence,
cooldownMs,
maxOrders,
channel,
agentNameFilter,
dryRun,
strategyPrompt,
reconnectBaseDelayMs,
reconnectMaxDelayMs,
maxReconnectAttempts
};
console.log(`[info] ws=${wsUrl()}`);
console.log(`[info] channel=${channel}`);
console.log(`[info] wallet=${signer.publicKey.toBase58()} user_margin=${userMargin}`);
console.log(`[info] min_confidence=${minConfidence} cooldown_ms=${cooldownMs} dry_run=${dryRun}`);
if (strategySource) {
console.log(`[info] strategy_source=${strategySource}`);
}
if (strategyPrompt) {
console.log(`[info] strategy_summary="${compactStrategySummary(strategyPrompt)}"`);
}
await printPositionSnapshot(userMargin);
await runRealtimeTrading(config, userMargin);
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[error] ${message}`);
process.exit(1);
});
```
### scripts/onboard.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawn, spawnSync } = require("node:child_process");
const readline = require("node:readline/promises");
const { stdin: input, stdout: output } = require("node:process");
const { Keypair } = require("@solana/web3.js");
const { parseArgs } = require("./common");
const SKILL_DIR = path.join(__dirname, "..");
const ENV_PATH = path.join(SKILL_DIR, ".env");
const STRATEGY_DIR = path.join(SKILL_DIR, "state", "strategies");
function usage() {
console.log(`Usage:
node scripts/onboard.js
node scripts/onboard.js --wallet-path ~/.config/solana/id.json --market-id 1 --margin 1000000
node scripts/onboard.js --strategy-file ./state/strategies/my-strategy.txt --market-id 1 --margin 1000000
Options:
--wallet-path <path> Optional preferred wallet path
--strategy-file <path> Optional existing strategy prompt file
--market-id <u64> Optional (prompted if missing)
--margin <u64> Optional (prompted if missing)
--type <market|limit> Optional, default: market
--price <u64> Required when --type limit
--ttl <i64> Optional, default: 300
--deposit <u64> Optional collateral deposit before order
--reduce-only Optional flag
--min-confidence <0..1> Optional, default: 0.75
--cooldown-ms <ms> Optional, default: 15000
--max-orders <n> Optional, default: 0 (unlimited)
--channel <name> Optional, default: agent.signals
--agent-name <name> Optional signal source filter
--dry-run Optional, do not place real orders
`);
}
function expandHome(rawPath) {
const value = String(rawPath || "").trim();
if (!value) {
return "";
}
if (value === "~") {
return os.homedir();
}
if (value.startsWith("~/")) {
return path.join(os.homedir(), value.slice(2));
}
return value;
}
function fileExists(target) {
try {
return fs.existsSync(target);
} catch (_error) {
return false;
}
}
function parseWalletFile(walletPath) {
const absolutePath = path.resolve(expandHome(walletPath));
if (!fileExists(absolutePath)) {
throw new Error("wallet file not found");
}
const raw = fs.readFileSync(absolutePath, "utf8");
const decoded = JSON.parse(raw);
if (!Array.isArray(decoded)) {
throw new Error("wallet file must be a JSON array");
}
const secret = Uint8Array.from(decoded);
const keypair = Keypair.fromSecretKey(secret);
return {
walletPath: absolutePath,
walletAddress: keypair.publicKey.toBase58()
};
}
function parseSolanaConfigWalletPath() {
const result = spawnSync("solana", ["config", "get"], {
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"]
});
if (result.status !== 0) {
return "";
}
const outputText = `${result.stdout || ""}\n${result.stderr || ""}`;
const matched = outputText.match(/Keypair Path:\s*(.+)/i);
if (!matched) {
return "";
}
return String(matched[1] || "").trim();
}
function discoverWalletPaths(args) {
const candidates = [];
if (args["wallet-path"]) {
candidates.push(String(args["wallet-path"]));
}
if (process.env.KEYPAIR_PATH) {
candidates.push(process.env.KEYPAIR_PATH);
}
if (process.env.ANCHOR_WALLET) {
candidates.push(process.env.ANCHOR_WALLET);
}
candidates.push("~/.config/solana/id.json");
const solanaConfigPath = parseSolanaConfigWalletPath();
if (solanaConfigPath) {
candidates.push(solanaConfigPath);
}
const deduped = [];
const seen = new Set();
for (const rawPath of candidates) {
const absolute = path.resolve(expandHome(rawPath));
if (!absolute || seen.has(absolute)) {
continue;
}
seen.add(absolute);
deduped.push(absolute);
}
return deduped;
}
function evaluateWalletPaths(paths) {
const available = [];
const rejected = [];
for (const walletPath of paths) {
try {
const parsed = parseWalletFile(walletPath);
available.push(parsed);
} catch (error) {
rejected.push({
walletPath,
reason: error instanceof Error ? error.message : String(error)
});
}
}
return { available, rejected };
}
async function ask(rl, promptText, fallback = "") {
const answer = await rl.question(promptText);
const trimmed = String(answer || "").trim();
if (!trimmed && fallback) {
return fallback;
}
return trimmed;
}
async function askYesNo(rl, promptText, defaultYes) {
const suffix = defaultYes ? " [Y/n] " : " [y/N] ";
const answer = await ask(rl, `${promptText}${suffix}`);
if (!answer) {
return defaultYes;
}
const normalized = answer.toLowerCase();
if (["y", "yes", "1", "true"].includes(normalized)) {
return true;
}
if (["n", "no", "0", "false"].includes(normalized)) {
return false;
}
return defaultYes;
}
async function chooseWallet(rl, args) {
const discovered = discoverWalletPaths(args);
const { available, rejected } = evaluateWalletPaths(discovered);
if (available.length > 0) {
console.log("[onboarding] available wallets:");
for (let i = 0; i < available.length; i += 1) {
const entry = available[i];
console.log(` ${i + 1}. ${entry.walletAddress} (${entry.walletPath})`);
}
if (rejected.length > 0) {
console.log("[onboarding] skipped invalid wallet paths:");
for (const item of rejected) {
console.log(` - ${item.walletPath}: ${item.reason}`);
}
}
const userInput = await ask(
rl,
"Select wallet number or enter a custom wallet path (default: 1): ",
"1"
);
const index = Number(userInput);
if (Number.isInteger(index) && index >= 1 && index <= available.length) {
return available[index - 1];
}
try {
return parseWalletFile(userInput);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`invalid wallet selection: ${detail}`);
}
}
console.log("[onboarding] no readable wallets were discovered automatically.");
while (true) {
const walletPath = await ask(rl, "Enter wallet keypair path: ");
if (!walletPath) {
continue;
}
try {
return parseWalletFile(walletPath);
} catch (error) {
const detail = error instanceof Error ? error.message : String(error);
console.log(`[warn] ${detail}`);
}
}
}
function upsertEnvValue(source, key, value) {
const pattern = new RegExp(`^${key}=.*$`, "m");
const line = `${key}=${value}`;
if (pattern.test(source)) {
return source.replace(pattern, line);
}
if (!source.endsWith("\n") && source.length > 0) {
return `${source}\n${line}\n`;
}
return `${source}${line}\n`;
}
function persistWalletSelection(walletPath) {
let source = "";
if (fileExists(ENV_PATH)) {
source = fs.readFileSync(ENV_PATH, "utf8");
}
source = upsertEnvValue(source, "KEYPAIR_PATH", walletPath);
source = upsertEnvValue(source, "ANCHOR_WALLET", walletPath);
fs.writeFileSync(ENV_PATH, source);
}
async function waitForRegistration(rl, walletAddress) {
console.log("[action] register this wallet address on the platform:");
console.log(` ${walletAddress}`);
console.log("[action] when registration is done, type 'done'. Type 'quit' to stop.");
while (true) {
const answer = (await ask(rl, "registration status > ")).toLowerCase();
if (!answer) {
continue;
}
if (["done", "d", "complete", "completed"].includes(answer)) {
return;
}
if (["quit", "q", "stop", "cancel"].includes(answer)) {
throw new Error("onboarding canceled by user before registration confirmation");
}
console.log("[info] waiting for explicit confirmation. Type 'done' when ready.");
}
}
async function captureStrategyPrompt(rl, args) {
if (args["strategy-file"]) {
const absolutePath = path.resolve(expandHome(args["strategy-file"]));
if (!fileExists(absolutePath)) {
throw new Error(`strategy file not found: ${absolutePath}`);
}
const promptText = fs.readFileSync(absolutePath, "utf8").trim();
if (!promptText) {
throw new Error(`strategy file is empty: ${absolutePath}`);
}
return {
promptText,
originalPath: absolutePath
};
}
console.log("[strategy] enter the strategy prompt.");
console.log("[strategy] submit an empty line to finish.");
const lines = [];
while (true) {
const prefix = lines.length === 0 ? "strategy > " : " ";
const line = await rl.question(prefix);
const trimmed = String(line || "");
if (trimmed.trim() === "") {
if (lines.length === 0) {
continue;
}
break;
}
lines.push(trimmed);
}
return {
promptText: lines.join("\n"),
originalPath: ""
};
}
function timestampSlug() {
return new Date().toISOString().replace(/[:.]/g, "-");
}
function saveStrategyPrompt(strategyPrompt, walletAddress, walletPath) {
fs.mkdirSync(STRATEGY_DIR, { recursive: true });
const slug = timestampSlug();
const txtPath = path.join(STRATEGY_DIR, `${slug}-strategy.txt`);
const metaPath = path.join(STRATEGY_DIR, `${slug}-strategy.json`);
fs.writeFileSync(txtPath, `${strategyPrompt.trim()}\n`);
fs.writeFileSync(
metaPath,
`${JSON.stringify(
{
created_at: new Date().toISOString(),
wallet_address: walletAddress,
wallet_path: walletPath,
strategy_file: txtPath
},
null,
2
)}\n`
);
return {
txtPath,
metaPath
};
}
function isUnsignedIntegerText(value) {
return /^\d+$/.test(String(value || "").trim());
}
async function askUnsignedInteger(rl, label, initialValue, fallback = "") {
let candidate = String(initialValue || "").trim() || fallback;
while (true) {
if (candidate && isUnsignedIntegerText(candidate)) {
return candidate;
}
candidate = await ask(rl, `${label}: `, fallback);
}
}
function parseMaybeFloat(raw, fallback) {
const value = String(raw || "").trim();
if (!value) {
return fallback;
}
const number = Number(value);
if (!Number.isFinite(number)) {
throw new Error(`invalid numeric value: ${raw}`);
}
return number;
}
function parseMaybeInt(raw, fallback) {
const value = String(raw || "").trim();
if (!value) {
return fallback;
}
const parsed = Number(value);
if (!Number.isFinite(parsed)) {
throw new Error(`invalid integer value: ${raw}`);
}
return Math.floor(parsed);
}
function parseBool(raw) {
if (raw === true) {
return true;
}
if (raw === undefined || raw === null || raw === false) {
return false;
}
const normalized = String(raw).trim().toLowerCase();
return ["1", "true", "yes", "y"].includes(normalized);
}
async function collectTradeConfig(rl, args) {
const marketId = await askUnsignedInteger(rl, "market id", args["market-id"]);
const margin = await askUnsignedInteger(rl, "margin (raw USDC units)", args.margin);
let orderType = String(args.type || "market").trim().toLowerCase();
if (orderType !== "market" && orderType !== "limit") {
orderType = "";
}
while (!orderType) {
const selected = (await ask(rl, "order type (market/limit) [market]: ", "market"))
.toLowerCase()
.trim();
if (selected === "market" || selected === "limit") {
orderType = selected;
}
}
let price = String(args.price || "").trim();
if (orderType === "limit") {
price = await askUnsignedInteger(rl, "limit price (raw price units)", price);
}
const ttl = String(parseMaybeInt(args.ttl, 300));
const deposit = String(args.deposit || "").trim();
if (deposit && !isUnsignedIntegerText(deposit)) {
throw new Error("--deposit must be an unsigned integer");
}
const minConfidence = parseMaybeFloat(args["min-confidence"], 0.75);
const cooldownMs = parseMaybeInt(args["cooldown-ms"], 15000);
const maxOrders = parseMaybeInt(args["max-orders"], 0);
const channel = String(args.channel || "agent.signals").trim() || "agent.signals";
const agentName = String(args["agent-name"] || "").trim();
let dryRun = parseBool(args["dry-run"]);
if (args["dry-run"] === undefined) {
const executeRealOrders = await askYesNo(
rl,
"Execute real orders immediately?",
false
);
dryRun = !executeRealOrders;
}
return {
marketId,
margin,
orderType,
price,
ttl,
deposit,
reduceOnly: parseBool(args["reduce-only"]),
minConfidence,
cooldownMs,
maxOrders,
channel,
agentName,
dryRun
};
}
function buildAutotradeArgs(config, strategyFilePath) {
const out = [
path.join(__dirname, "realtime-agent.js"),
"--market-id",
config.marketId,
"--margin",
config.margin,
"--type",
config.orderType,
"--ttl",
config.ttl,
"--min-confidence",
String(config.minConfidence),
"--cooldown-ms",
String(config.cooldownMs),
"--max-orders",
String(config.maxOrders),
"--channel",
config.channel,
"--strategy-file",
strategyFilePath
];
if (config.orderType === "limit") {
out.push("--price", config.price);
}
if (config.deposit) {
out.push("--deposit", config.deposit);
}
if (config.reduceOnly) {
out.push("--reduce-only");
}
if (config.agentName) {
out.push("--agent-name", config.agentName);
}
if (config.dryRun) {
out.push("--dry-run");
}
return out;
}
async function runAutotrade(args, walletPath) {
return new Promise((resolve, reject) => {
const child = spawn(process.execPath, args, {
cwd: SKILL_DIR,
stdio: "inherit",
env: {
...process.env,
KEYPAIR_PATH: walletPath,
ANCHOR_WALLET: walletPath
}
});
child.on("error", (error) => {
reject(error);
});
child.on("close", (code) => {
if (code === 0) {
resolve();
return;
}
reject(new Error(`autotrade exited with code=${code}`));
});
});
}
async function main() {
const args = parseArgs(process.argv.slice(2));
if (args.help || args.h) {
usage();
return;
}
const rl = readline.createInterface({ input, output });
try {
const wallet = await chooseWallet(rl, args);
persistWalletSelection(wallet.walletPath);
process.env.KEYPAIR_PATH = wallet.walletPath;
process.env.ANCHOR_WALLET = wallet.walletPath;
console.log(`[onboarding] selected wallet: ${wallet.walletAddress}`);
console.log(`[onboarding] wallet path: ${wallet.walletPath}`);
await waitForRegistration(rl, wallet.walletAddress);
const strategy = await captureStrategyPrompt(rl, args);
const saved =
strategy.originalPath.length > 0
? {
txtPath: strategy.originalPath,
metaPath: ""
}
: saveStrategyPrompt(strategy.promptText, wallet.walletAddress, wallet.walletPath);
if (!strategy.originalPath) {
console.log(`[onboarding] strategy saved: ${saved.txtPath}`);
console.log(`[onboarding] metadata saved: ${saved.metaPath}`);
} else {
console.log(`[onboarding] strategy loaded: ${saved.txtPath}`);
}
const tradeConfig = await collectTradeConfig(rl, args);
console.log("[summary] trading configuration:");
console.log(` wallet: ${wallet.walletAddress}`);
console.log(` market_id: ${tradeConfig.marketId}`);
console.log(` margin: ${tradeConfig.margin}`);
console.log(` type: ${tradeConfig.orderType}`);
if (tradeConfig.orderType === "limit") {
console.log(` price: ${tradeConfig.price}`);
}
console.log(` min_confidence: ${tradeConfig.minConfidence}`);
console.log(` cooldown_ms: ${tradeConfig.cooldownMs}`);
console.log(` dry_run: ${tradeConfig.dryRun}`);
console.log(` strategy_file: ${saved.txtPath}`);
const confirmed = await askYesNo(rl, "Start trading with this strategy now?", false);
if (!confirmed) {
console.log("[onboarding] canceled before trading start.");
return;
}
const autotradeArgs = buildAutotradeArgs(tradeConfig, saved.txtPath);
console.log("[onboarding] starting realtime trading agent...");
await runAutotrade(autotradeArgs, wallet.walletPath);
} finally {
rl.close();
}
}
main().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[error] ${message}`);
process.exit(1);
});
```
### scripts/backend-common.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const { URL } = require("node:url");
function ensureFetch() {
if (typeof fetch !== "function") {
throw new Error("Global fetch is not available. Use Node.js 18+.");
}
}
function apiBaseUrl() {
const raw =
process.env.EASYCLAW_API_BASE_URL ||
process.env.API_BASE_URL ||
"http://127.0.0.1:8080";
return String(raw).trim().replace(/\/$/, "");
}
function wsUrl() {
const explicit =
process.env.EASYCLAW_WS_URL ||
process.env.BACKEND_WS_URL ||
process.env.WS_URL;
if (explicit && String(explicit).trim().length > 0) {
return String(explicit).trim();
}
const base = apiBaseUrl();
if (base.startsWith("https://")) {
return `${base.replace("https://", "wss://")}/ws`;
}
if (base.startsWith("http://")) {
return `${base.replace("http://", "ws://")}/ws`;
}
return `${base}/ws`;
}
function authToken(overrideToken) {
if (overrideToken && String(overrideToken).trim().length > 0) {
return String(overrideToken).trim();
}
const token =
process.env.EASYCLAW_API_TOKEN ||
process.env.API_AUTH_TOKEN ||
process.env.API_TOKEN ||
"";
return String(token).trim();
}
function appendQueryParams(url, query) {
if (!query) {
return;
}
for (const [key, value] of Object.entries(query)) {
if (value === undefined || value === null || String(value).trim() === "") {
continue;
}
url.searchParams.set(key, String(value));
}
}
async function apiRequest(path, options = {}) {
ensureFetch();
const {
method = "GET",
query,
body,
requireAuth = false,
token,
timeoutMs = 15000
} = options;
const endpoint = new URL(path, `${apiBaseUrl()}/`);
appendQueryParams(endpoint, query);
const headers = {
Accept: "application/json"
};
const resolvedToken = authToken(token);
if (resolvedToken) {
headers.Authorization = `Bearer ${resolvedToken}`;
} else if (requireAuth) {
throw new Error(
"Missing API auth token. Set EASYCLAW_API_TOKEN or pass --token."
);
}
let payload;
if (body !== undefined) {
headers["Content-Type"] = "application/json";
payload = JSON.stringify(body);
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
let response;
try {
response = await fetch(endpoint.toString(), {
method,
headers,
body: payload,
signal: controller.signal
});
} finally {
clearTimeout(timer);
}
const text = await response.text();
let parsed;
if (text.length > 0) {
try {
parsed = JSON.parse(text);
} catch (_err) {
parsed = text;
}
}
if (!response.ok) {
let message = `HTTP ${response.status}`;
if (parsed && typeof parsed === "object" && parsed.error) {
message = `${message}: ${parsed.error}`;
} else if (typeof parsed === "string" && parsed.length > 0) {
message = `${message}: ${parsed}`;
}
throw new Error(message);
}
return parsed;
}
module.exports = {
apiBaseUrl,
apiRequest,
authToken,
wsUrl
};
```
### scripts/common.js
```javascript
#!/usr/bin/env node
require("dotenv").config();
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const {
Connection,
Keypair,
PublicKey,
SystemProgram,
Transaction,
TransactionInstruction
} = require("@solana/web3.js");
const {
ASSOCIATED_TOKEN_PROGRAM_ID,
TOKEN_PROGRAM_ID,
createAssociatedTokenAccountIdempotentInstruction,
getAssociatedTokenAddressSync
} = require("@solana/spl-token");
const DEFAULT_PROGRAM_IDS = {
ORDER_ENGINE: "GpMobZUKPtEE1eiZQAADo2ecD54JXhNHPNts5kPGwLtb",
MARKET_REGISTRY: "BsA8fuyw8XqBMiUfpLbdiBwbKg8MZMHB1jdZzjs7c46q"
};
const DISCRIMINATORS = {
CREATE_MARGIN_ACCOUNT: Buffer.from([98, 114, 213, 184, 129, 89, 90, 185]),
CREATE_USER_MARKET_POSITION: Buffer.from([
184, 183, 182, 125, 240, 234, 69, 166
]),
DEPOSIT_COLLATERAL: Buffer.from([156, 131, 142, 116, 146, 247, 162, 120]),
PLACE_ORDER: Buffer.from([51, 194, 155, 175, 109, 130, 96, 106]),
ENGINE_CONFIG_ACCOUNT: Buffer.from([10, 197, 172, 236, 51, 169, 22, 207]),
USER_MARGIN_ACCOUNT: Buffer.from([198, 202, 205, 196, 42, 177, 76, 75]),
ORDER_ACCOUNT: Buffer.from([134, 173, 223, 185, 77, 86, 28, 51])
};
function usageNumberToBigInt(name, value) {
const raw = String(value).trim();
if (!/^\d+$/.test(raw)) {
throw new Error(`Invalid numeric value for ${name}: ${value}`);
}
return BigInt(raw);
}
function usageSignedToBigInt(name, value) {
const raw = String(value).trim();
if (!/^-?\d+$/.test(raw)) {
throw new Error(`Invalid signed numeric value for ${name}: ${value}`);
}
return BigInt(raw);
}
function u64Le(value) {
let n = typeof value === "bigint" ? value : usageNumberToBigInt("u64", value);
if (n < 0n || n > 18446744073709551615n) {
throw new Error(`u64 out of range: ${value}`);
}
const out = Buffer.alloc(8);
out.writeBigUInt64LE(n, 0);
return out;
}
function i64Le(value) {
let n = typeof value === "bigint" ? value : usageSignedToBigInt("i64", value);
if (n < -9223372036854775808n || n > 9223372036854775807n) {
throw new Error(`i64 out of range: ${value}`);
}
const out = Buffer.alloc(8);
out.writeBigInt64LE(n, 0);
return out;
}
function readU64Le(data, offset) {
return data.readBigUInt64LE(offset);
}
function readI64Le(data, offset) {
return data.readBigInt64LE(offset);
}
function pubkeyFromEnv(name, fallback) {
const raw = process.env[name];
const value = raw && raw.trim().length > 0 ? raw.trim() : fallback;
if (!value) {
throw new Error(`Missing required env: ${name}`);
}
return new PublicKey(value);
}
function readSigner() {
const keypairPath =
process.env.KEYPAIR_PATH ||
process.env.ANCHOR_WALLET ||
path.join(os.homedir(), ".config/solana/id.json");
const absolute = path.resolve(keypairPath);
if (!fs.existsSync(absolute)) {
throw new Error(`Signer keypair not found: ${absolute}`);
}
const content = fs.readFileSync(absolute, "utf8");
const secret = Uint8Array.from(JSON.parse(content));
return { signer: Keypair.fromSecretKey(secret), keypairPath: absolute };
}
function getConnection() {
const rpc =
process.env.SOLANA_RPC_URL ||
process.env.ANCHOR_PROVIDER_URL ||
"http://127.0.0.1:8899";
return { connection: new Connection(rpc, { commitment: "confirmed" }), rpc };
}
function deriveEngineConfigPda(orderEngineProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("engine-config")],
orderEngineProgramId
)[0];
}
function deriveUserMarginPda(user, orderEngineProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("user-margin"), user.toBuffer()],
orderEngineProgramId
)[0];
}
function deriveUserMarketPositionPda(userMargin, marketId, orderEngineProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("user-market-pos"), userMargin.toBuffer(), u64Le(marketId)],
orderEngineProgramId
)[0];
}
function deriveMarketPda(marketId, marketRegistryProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("market"), u64Le(marketId)],
marketRegistryProgramId
)[0];
}
function deriveGlobalConfigPda(marketRegistryProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("global-config")],
marketRegistryProgramId
)[0];
}
function deriveOrderPda(userMargin, nonce, orderEngineProgramId) {
return PublicKey.findProgramAddressSync(
[Buffer.from("order"), userMargin.toBuffer(), u64Le(nonce)],
orderEngineProgramId
)[0];
}
function decodeEngineConfig(data) {
if (data.length < 8 + 32 * 3) {
throw new Error("Invalid EngineConfig account length");
}
if (!data.subarray(0, 8).equals(DISCRIMINATORS.ENGINE_CONFIG_ACCOUNT)) {
throw new Error("EngineConfig discriminator mismatch");
}
let offset = 8;
const admin = new PublicKey(data.subarray(offset, offset + 32));
offset += 32;
const usdcMint = new PublicKey(data.subarray(offset, offset + 32));
offset += 32;
const collateralVault = new PublicKey(data.subarray(offset, offset + 32));
return { admin, usdcMint, collateralVault };
}
function decodeUserMargin(data) {
if (data.length < 8 + 32 + 8 + 8 + 8 + 1) {
throw new Error("Invalid UserMargin account length");
}
if (!data.subarray(0, 8).equals(DISCRIMINATORS.USER_MARGIN_ACCOUNT)) {
throw new Error("UserMargin discriminator mismatch");
}
let offset = 8;
const owner = new PublicKey(data.subarray(offset, offset + 32));
offset += 32;
const collateralBalance = readU64Le(data, offset);
offset += 8;
const nextOrderNonce = readU64Le(data, offset);
offset += 8;
const totalNotional = readU64Le(data, offset);
offset += 8;
const bump = data.readUInt8(offset);
return { owner, collateralBalance, nextOrderNonce, totalNotional, bump };
}
function decodeOrder(data, pubkey) {
if (data.length < 133) {
throw new Error("Invalid Order account length");
}
if (!data.subarray(0, 8).equals(DISCRIMINATORS.ORDER_ACCOUNT)) {
throw new Error("Order discriminator mismatch");
}
let offset = 8;
const id = readU64Le(data, offset);
offset += 8;
const userMargin = new PublicKey(data.subarray(offset, offset + 32));
offset += 32;
const user = new PublicKey(data.subarray(offset, offset + 32));
offset += 32;
const marketId = readU64Le(data, offset);
offset += 8;
const side = data.readUInt8(offset);
offset += 1;
const orderType = data.readUInt8(offset);
offset += 1;
const reduceOnly = data.readUInt8(offset) === 1;
offset += 1;
const margin = readU64Le(data, offset);
offset += 8;
const price = readU64Le(data, offset);
offset += 8;
const createdAt = readI64Le(data, offset);
offset += 8;
const expiresAt = readI64Le(data, offset);
offset += 8;
const clientOrderId = readU64Le(data, offset);
offset += 8;
const status = data.readUInt8(offset);
offset += 1;
const bump = data.readUInt8(offset);
return {
pubkey,
id,
userMargin,
user,
marketId,
side,
orderType,
reduceOnly,
margin,
price,
createdAt,
expiresAt,
clientOrderId,
status,
bump
};
}
function sideName(side) {
return side === 0 ? "buy" : "sell";
}
function orderTypeName(orderType) {
return orderType === 0 ? "market" : "limit";
}
function orderStatusName(status) {
if (status === 0) return "open";
if (status === 1) return "executed";
if (status === 2) return "cancelled";
if (status === 3) return "expired";
return `unknown(${status})`;
}
function boolByte(value) {
return Buffer.from([value ? 1 : 0]);
}
function orderData({
marketId,
side,
orderType,
reduceOnly,
margin,
price,
ttlSecs,
clientOrderId
}) {
return Buffer.concat([
DISCRIMINATORS.PLACE_ORDER,
u64Le(marketId),
Buffer.from([side]),
Buffer.from([orderType]),
boolByte(reduceOnly),
u64Le(margin),
u64Le(price),
i64Le(ttlSecs),
u64Le(clientOrderId)
]);
}
function instruction(programId, keys, data) {
return new TransactionInstruction({ programId, keys, data });
}
async function sendInstructions(connection, signer, instructions) {
if (instructions.length === 0) {
return null;
}
const latestBlockhash = await connection.getLatestBlockhash("confirmed");
const tx = new Transaction({
feePayer: signer.publicKey,
recentBlockhash: latestBlockhash.blockhash
});
tx.add(...instructions);
tx.sign(signer);
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false
});
await connection.confirmTransaction(
{ signature, ...latestBlockhash },
"confirmed"
);
return signature;
}
function parseArgs(argv) {
const args = {};
for (let i = 0; i < argv.length; i += 1) {
const token = argv[i];
if (!token.startsWith("--")) {
continue;
}
const key = token.slice(2);
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
args[key] = true;
continue;
}
args[key] = next;
i += 1;
}
return args;
}
async function ensureAtaExists(connection, signer, owner, mint) {
const ata = getAssociatedTokenAddressSync(
mint,
owner,
false,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const info = await connection.getAccountInfo(ata);
if (info) {
return { ata, createInstruction: null };
}
return {
ata,
createInstruction: createAssociatedTokenAccountIdempotentInstruction(
signer.publicKey,
ata,
owner,
mint,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
)
};
}
function programIdsFromEnv() {
return {
orderEngine: pubkeyFromEnv(
"ORDER_ENGINE_PROGRAM_ID",
DEFAULT_PROGRAM_IDS.ORDER_ENGINE
),
marketRegistry: pubkeyFromEnv(
"MARKET_REGISTRY_PROGRAM_ID",
DEFAULT_PROGRAM_IDS.MARKET_REGISTRY
)
};
}
module.exports = {
ASSOCIATED_TOKEN_PROGRAM_ID,
DISCRIMINATORS,
DEFAULT_PROGRAM_IDS,
PublicKey,
SystemProgram,
TOKEN_PROGRAM_ID,
decodeEngineConfig,
decodeOrder,
decodeUserMargin,
deriveEngineConfigPda,
deriveGlobalConfigPda,
deriveMarketPda,
deriveOrderPda,
deriveUserMarginPda,
deriveUserMarketPositionPda,
ensureAtaExists,
getConnection,
instruction,
orderData,
orderStatusName,
orderTypeName,
parseArgs,
programIdsFromEnv,
readSigner,
sendInstructions,
sideName,
u64Le,
usageNumberToBigInt
};
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "ice-coldbell",
"slug": "easyclaw",
"displayName": "easyclaw",
"latest": {
"version": "0.0.2",
"publishedAt": 1772079524440,
"commit": "https://github.com/openclaw/skills/commit/ea3e912f19a68d847f28306b53a243a73d5206d1"
},
"history": []
}
```
### scripts/dex-agent.sh
```bash
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
usage() {
cat <<'EOF'
Usage:
dex-agent.sh doctor
dex-agent.sh install
dex-agent.sh balance [--json]
dex-agent.sh order [order options...]
dex-agent.sh backend <command> [options...]
dex-agent.sh watch [--channel <name> | --channels <csv>] [--json]
dex-agent.sh autotrade [autotrade options...]
dex-agent.sh onboard [onboarding options...]
dex-agent.sh help
Examples:
dex-agent.sh balance
dex-agent.sh order --market-id 1 --side buy --type market --margin 1000000
dex-agent.sh backend positions --mine --limit 20
dex-agent.sh watch --channel agent.signals
dex-agent.sh autotrade --market-id 1 --margin 1000000 --min-confidence 0.75
dex-agent.sh onboard --market-id 1 --margin 1000000
EOF
}
require_cmd() {
if ! command -v "$1" >/dev/null 2>&1; then
echo "[error] required command not found: $1" >&2
exit 1
fi
}
ensure_skill_dir() {
if [ ! -d "${SKILL_DIR}" ]; then
echo "[error] skill directory not found: ${SKILL_DIR}" >&2
exit 1
fi
}
install_deps() {
ensure_skill_dir
require_cmd npm
if [ ! -d "${SKILL_DIR}/node_modules" ]; then
echo "[info] installing npm dependencies in ${SKILL_DIR}"
(cd "${SKILL_DIR}" && npm install)
fi
}
run_local_script() {
local script_rel="$1"
shift || true
ensure_skill_dir
install_deps
require_cmd node
(
cd "${SKILL_DIR}"
node "${script_rel}" "$@"
)
}
doctor() {
echo "skill_dir=${SKILL_DIR}"
for cmd in node npm solana; do
if command -v "${cmd}" >/dev/null 2>&1; then
echo "[ok] ${cmd}: $(command -v "${cmd}")"
else
echo "[warn] ${cmd}: missing"
fi
done
local api_base="${EASYCLAW_API_BASE_URL:-${API_BASE_URL:-http://127.0.0.1:8080}}"
local ws_url="${EASYCLAW_WS_URL:-${BACKEND_WS_URL:-}}"
if [ -z "${ws_url}" ]; then
ws_url="${api_base/http:\/\//ws://}"
ws_url="${ws_url/https:\/\//wss://}"
ws_url="${ws_url%/}/ws"
fi
echo "[info] api_base=${api_base}"
echo "[info] ws_url=${ws_url}"
}
balance() {
run_local_script "scripts/balance.js" "$@"
}
order_execute() {
run_local_script "scripts/order-execute.js" "$@"
}
backend_api() {
run_local_script "scripts/backend.js" "$@"
}
watch_stream() {
run_local_script "scripts/ws-watch.js" "$@"
}
autotrade() {
run_local_script "scripts/realtime-agent.js" "$@"
}
onboard() {
run_local_script "scripts/onboard.js" "$@"
}
main() {
local cmd="${1:-help}"
shift || true
case "${cmd}" in
doctor)
doctor
;;
install)
install_deps
;;
balance)
balance "$@"
;;
order)
order_execute "$@"
;;
backend)
backend_api "$@"
;;
watch)
watch_stream "$@"
;;
autotrade)
autotrade "$@"
;;
onboard)
onboard "$@"
;;
help|-h|--help)
usage
;;
*)
echo "[error] unknown command: ${cmd}" >&2
usage
exit 1
;;
esac
}
main "$@"
```