references/cli.md
# query-address-info — CLI Reference
Complete reference for every command in `scripts/cli.mjs`.
**Invocation pattern:** `node <skill-dir>/scripts/cli.mjs <command> '<json_params>'`
**Exit codes:** `0` success · `1` usage/upstream error · `3` network failure
---
## `positions` — Wallet active-position list
```bash
node <skill-dir>/scripts/cli.mjs positions '{"address":"0x...","chainId":"56","offset":0}'
```
### Parameters
| Param | Type | Required | Description |
|---|---|---|---|
| `address` | string | **yes** | Wallet address (EVM `0x...` or Solana base58) |
| `chainId` | string | **yes** | `"56"` (BSC) · `"8453"` (Base) · `"CT_501"` (Solana) |
| `offset` | number | **yes** | Pagination offset (start from `0`) |
### Return fields (under `.data.list[]`)
| Field | Type | Description |
|---|---|---|
| `chainId` | string | Chain identifier |
| `address` | string | Queried wallet address (lowercased) |
| `contractAddress` | string | Token contract address |
| `binanceTokenId` | string | Binance-internal stable token ID |
| `name`, `symbol` | string | Token display names |
| `icon` | string | Logo path — prefix with `https://bin.bnbstatic.com` |
| `decimals` | number | Token decimals |
| `price` | string | Current USD price (decimal string) |
| `percentChange24h` | string | 24h price change (%, signed decimal string) |
| `baseCoinPrice` | string | USD price of the chain native coin |
| `remainQty` | string | Holding quantity (human-readable, already divided by decimals) |
| `circulatingSupply` | string | Circulating supply |
| `riskLevel`, `riskLevelInt` | string \| null, number | Risk badge (`"LOW"` = 1, etc.); may be `null` |
| `lowLiquidity` | number | `1` = low-liquidity warning |
### Response envelope (under `.data`)
| Field | Type | Description |
|---|---|---|
| `offset` | number | Echo of request offset |
| `list` | array \| `null` | Position list; `null` when wallet has no tracked holdings |
| `addressStatus` | any | Reserved (currently always `null`) |
---
## Errors
Exit codes: `0` ok · `1` upstream/usage (stderr: reason; stdout: body with business `code`) · `3` network.
Business `code`: `000000` ok · `100004` rate-limited · `100002` bad param · `000400` unsupported chain / bad address.
scripts/cli.mjs
#!/usr/bin/env node
// query-address-info CLI — self-contained, zero-dep, Node >= 22
// Usage: node cli.mjs <command> '<json_params>'
//
// Commands:
// positions GET wallet active-position list (token balances + 24h price change)
//
// ---- inline HTTP helper (self-contained, zero dependency) ----
const TIMEOUT_MS = 10_000;
const UA = { 'Accept-Encoding': 'identity', 'User-Agent': 'binance-web3/2.0 (Skill)' };
const qs = (p) => Object.entries(p)
.filter(([, v]) => v != null)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
async function call({ url, method = 'GET', body, headers = {} }) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), TIMEOUT_MS);
const opts = { method, headers: { ...UA, ...headers }, signal: ctrl.signal };
if (method === 'POST') { opts.headers['content-type'] = 'application/json'; opts.body = JSON.stringify(body || {}); }
let res;
try { res = await fetch(url, opts); }
catch { clearTimeout(timer); throw Object.assign(new Error('Network request failed'), { exitCode: 3 }); }
clearTimeout(timer);
const data = await res.json();
if (res.status >= 400) throw Object.assign(new Error(`HTTP ${res.status}`), { exitCode: 1, body: data });
return data;
}
// ---- supported chains (client-side fail-fast) ----
const SUPPORTED_CHAINS = new Set(['1', '56', '8453', 'CT_501']);
function validateChainId(chainId) {
const id = String(chainId ?? '');
if (!SUPPORTED_CHAINS.has(id)) {
const supported = [...SUPPORTED_CHAINS].map((c) => `"${c}"`).join(', ');
throw Object.assign(
new Error(`positions: unsupported chainId "${chainId}". Supported: ${supported}`),
{ exitCode: 1 },
);
}
}
// Client-side fail-fast for malformed addresses.
const EVM_CHAINS = new Set(['1', '56', '8453']);
const SOLANA_CHAIN = 'CT_501';
const EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/;
const SOLANA_ADDRESS_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
const EVM_ZERO = '0x' + '0'.repeat(40);
function validationError(msg) {
return Object.assign(new Error(msg), { exitCode: 1 });
}
function validateAddress(address, chainId) {
if (typeof address !== 'string' || address.length === 0) {
throw validationError('address is required');
}
// Reject whitespace, control chars, URL-encoded sequences (CRLF injection guard).
if (/\s/.test(address) || /[\x00-\x1f\x7f]/.test(address) || /%/.test(address)) {
throw validationError('address contains invalid characters (whitespace / control / %-encoded)');
}
// Reject non-ASCII (rejects unicode look-alikes and multibyte chars).
if (/[^\x20-\x7e]/.test(address)) {
throw validationError('address contains non-ASCII characters');
}
const chain = String(chainId ?? '');
if (EVM_CHAINS.has(chain)) {
if (!EVM_ADDRESS_RE.test(address)) {
throw validationError(`invalid EVM address for chainId=${chain}: expected 0x + 40 hex chars`);
}
if (address.toLowerCase() === EVM_ZERO) {
throw validationError('zero address (0x000...000) is not a valid wallet');
}
return;
}
if (chain === SOLANA_CHAIN) {
if (!SOLANA_ADDRESS_RE.test(address)) {
throw validationError('invalid Solana address: expected base58, 32-44 chars');
}
return;
}
// Unknown chain — let upstream decide.
}
// ---- commands: (params) => { url, method?, body?, headers? } ----
const COMMANDS = {
positions: (p) => {
validateChainId(p.chainId);
validateAddress(p.address, p.chainId);
return {
url: `https://web3.binance.com/bapi/defi/v3/public/wallet-direct/buw/wallet/address/pnl/active-position-list/ai?${qs(p)}`,
headers: { clienttype: 'web', clientversion: '1.2.0' },
};
},
};
// ---- exports (for unit testing; direct execution still works — see dispatch below) ----
export { COMMANDS, call, qs, UA, TIMEOUT_MS, validateAddress, validateChainId, SUPPORTED_CHAINS };
// ---- CLI dispatch (only runs when executed directly, not when imported) ----
if (import.meta.url === `file://${process.argv[1]}`) {
const [cmd, paramsStr] = process.argv.slice(2);
if (!cmd || cmd === '--help' || cmd === '-h') {
console.log("Usage: node cli.mjs <command> '<json_params>'\n\nCommands:");
for (const name of Object.keys(COMMANDS)) console.log(` ${name}`);
process.exit(0);
}
const builder = COMMANDS[cmd];
if (!builder) { console.error(`Unknown command: ${cmd}\nRun with --help to see available commands.`); process.exit(1); }
let params = {};
if (paramsStr) {
try { params = JSON.parse(paramsStr); }
catch { console.error('Invalid JSON params'); process.exit(1); }
}
try {
const result = await call(builder(params));
console.log(JSON.stringify(result, null, 2));
} catch (err) {
console.error(err.message);
if (err.body) console.log(JSON.stringify(err.body, null, 2));
process.exit(err.exitCode || 1);
}
}
SKILL.md
---
name: query-address-info
description: |
Snapshot of a single wallet's token holdings on a specific chain — list of every token currently held with
name, symbol, current price, 24h price change, and holding quantity.
Use when the user provides an explicit wallet address (or says "my wallet") and wants the current portfolio:
"what does 0x... hold", "wallet balance breakdown", "list positions for this address",
"what tokens are in this wallet", "show me the holdings of <address>".
metadata:
author: binance-web3-team
version: "2.0"
---
# Query Address Info Skill
## Overview
This skill queries any on-chain wallet address for token holdings, supporting:
List of all tokens held by a wallet address
Current price of each token
24-hour price change percentage
Holding quantity
## When to Use This Skill
| User intent | Command |
|-------------|---------|
| List a wallet's token holdings with price and 24h change | `positions` |
## Supported Chains
| Chain | chainId |
|-------|---------|
| BSC | `56` |
| Solana | `CT_501` |
| Base | `8453` |
| Ethereum | `1` |
## How to Call APIs
```bash
node <skill-dir>/scripts/cli.mjs positions '{"address":"0x...","chainId":"56","offset":0}'
```
## Commands
| Command | Purpose | Required args | Example |
|---------|---------|---------------|---------|
| `positions` | List wallet token holdings (price + 24h change + quantity) | `address`, `chainId`, `offset` | `node <skill-dir>/scripts/cli.mjs positions '{"address":"0x...","chainId":"56","offset":0}'` |
## Rules
- **`offset` is required on every call — including the first page.** Pass `0` to fetch the first page; increment for subsequent pages. Omitting it causes an upstream validation error.
- **Pagination**: repeat with increasing `offset` until `data.list` is empty or shorter than the page size.
- **Icon URL prefix**: `icon` is a relative path (e.g., `/images/web3-data/public/token/logos/xxxx.png`). Prepend `https://bin.bnbstatic.com` to render.
- **Numbers as strings**: `price`, `percentChange24h`, `remainQty` are strings — convert to numbers before arithmetic.
## Full CLI Reference
See [`references/cli.md`](references/cli.md) for per-subcommand invocations, parameter tables, return-field tables, and real response samples.