Skip to content
Open

Try #53

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
1 change: 0 additions & 1 deletion skills/chains/SKILL.md → .agents/skills/chains/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ These are the chain IDs currently accepted by the wallet commands in this repo:
| Solana | `solana` |
| Zora | `zora` |
| Blast | `blast` |

`zerion chains` may return a broader catalog. For `positions`, `history`, and `analyze`, use the IDs above unless the CLI validator is expanded.

## Using with analysis commands
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Use `zerion chains` to inspect the broader chain catalog, but stick to the IDs a
- **`missing_api_key`**: Set `ZERION_API_KEY` or add `--x402` flag
- **`unsupported_chain`**: Run `zerion chains` to check valid chain IDs
- **Empty positions/transactions**: Wallet may be inactive or very new
- **`api_error` with status 429**: Rate limited -- wait or switch to x402
- **`api_error` with status 401/403/429**: API key limit reached. The CLI will automatically fallback to x402 if `WALLET_PRIVATE_KEY` is present. Otherwise, wait or append the `--x402` flag.
- **ENS name fails**: Retry with the resolved 0x address if upstream name resolution is unavailable

For worked examples, see [EXAMPLES.md](EXAMPLES.md).
File renamed without changes.
File renamed without changes.
4 changes: 2 additions & 2 deletions skills/zerion/SKILL.md → .agents/skills/zerion/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,8 +287,8 @@ Wallets are encrypted with AES-256-GCM via the Open Wallet Standard (OWS) vault
| `wallet_not_found` | Wallet name doesn't exist in OWS vault | Run `zerion wallet list` to check |
| `unsupported_chain` | Invalid `--chain` value | Run `zerion chains` for valid IDs |
| `unsupported_positions_filter` | Invalid `--positions` value | Use `all`, `simple`, or `defi` |
| `api_error` status 401 | Invalid API key | Check key at dashboard.zerion.io |
| `api_error` status 429 | Rate limited | Wait, reduce frequency, or use x402 |
| `api_error` status 401/403 | Invalid/Overused key | Check key at dashboard.zerion.io or use x402 (auto-fallback if WALLET_PRIVATE_KEY is set) |
| `api_error` status 429 | Rate limited | Wait, reduce frequency, or use x402 (auto-fallback if WALLET_PRIVATE_KEY is set) |
| `api_error` status 400 | Invalid address or ENS failed | Retry with 0x hex address |
| `unexpected_error` | `WALLET_PRIVATE_KEY` missing in x402 | Export the private key or disable x402 |
| `unexpected_error` | Node.js < 20 | Upgrade Node.js |
Expand Down
1 change: 1 addition & 0 deletions .claude/skills/chains
1 change: 1 addition & 0 deletions .claude/skills/wallet-analysis
1 change: 1 addition & 0 deletions .claude/skills/wallet-trading
1 change: 1 addition & 0 deletions .claude/skills/zerion
1 change: 1 addition & 0 deletions .qwen/skills/chains
1 change: 1 addition & 0 deletions .qwen/skills/wallet-analysis
1 change: 1 addition & 0 deletions .qwen/skills/wallet-trading
1 change: 1 addition & 0 deletions .qwen/skills/zerion
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,14 @@ Start here:
- [OpenClaw example](./examples/openclaw/README.md)
- [CLI usage](./cli/README.md)

### Chat Bots (Discord / Telegram)

Use this if you are building an interactive AI bot using popular chat platform APIs.

Start here:

- [Discord & Telegram implementations](./docs/discord-telegram-bots.md)

## 4. Run the first wallet analysis

### MCP quickstart
Expand Down
3 changes: 2 additions & 1 deletion cli/commands/analytics/overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import { fetchAPI } from "../../lib/api/client.js";
import { summarizeAnalyze } from "../../lib/util/analyze.js";
import { print, printError } from "../../lib/util/output.js";
import { formatOverview } from "../../lib/util/format.js";
import { isX402Enabled } from "../../lib/api/x402.js";
import { resolveAddressOrWallet } from "../../lib/wallet/resolve.js";
import { validateChain } from "../../lib/util/validate.js";
Expand Down Expand Up @@ -50,7 +51,7 @@ export default async function walletAnalyze(args, flags) {
if (failures.length) summary.failures = failures;
if (useX402) summary.auth = "x402";

print(summary);
print(summary, formatOverview);
} catch (err) {
printError(err.code || "analyze_error", err.message);
process.exit(1);
Expand Down
66 changes: 66 additions & 0 deletions cli/commands/apikey.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* zerion apikey — quick shortcut for viewing / setting the Zerion API key.
*
* Usage:
* zerion apikey → show current key (redacted)
* zerion apikey set <key> → save key to config
* zerion apikey unset → remove saved key from config
*/

import { getApiKey, getConfigValue, setConfigValue, unsetConfigValue } from "../lib/config.js";
import { print, printError } from "../lib/util/output.js";

function redact(val) {
if (!val) return null;
return val.length > 8 ? val.slice(0, 8) + "..." : "***";
}

export default async function apikeyCmd(args, flags) {
const [action, ...valueParts] = args;

// No action → show current key + source
if (!action) {
const envKey = process.env.ZERION_API_KEY || null;
const configKey = getConfigValue("apiKey") || null;
const active = envKey || configKey;
print({
apiKey: redact(active),
source: envKey ? "ZERION_API_KEY env var" : configKey ? "config file" : null,
hint: active
? null
: "Set via: zerion apikey set <key> OR export ZERION_API_KEY=<key>",
});
return;
}

if (action === "set") {
const key = valueParts.join(" ").trim() || flags.key;
if (!key) {
printError("missing_value", "Usage: zerion apikey set <api-key>", {
hint: "Get your key at https://developers.zerion.io",
});
process.exit(1);
}
setConfigValue("apiKey", key);
print({ apiKey: redact(key), updated: true });
return;
}

if (action === "unset" || action === "remove" || action === "delete") {
unsetConfigValue("apiKey");
print({ apiKey: null, removed: true });
return;
}

// If the first arg looks like a key value, treat it as `apikey set <key>`
if (action.startsWith("zk_") || action.length > 20) {
setConfigValue("apiKey", action);
print({ apiKey: redact(action), updated: true });
return;
}

printError("invalid_action", "Usage: zerion apikey [set <key> | unset]", {
hint: "Run 'zerion apikey' to see current key",
});
process.exit(1);
}
8 changes: 7 additions & 1 deletion cli/lib/api/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export function basicAuthHeader(key) {
return `Basic ${Buffer.from(`${key}:`).toString("base64")}`;
}

export async function fetchAPI(pathname, params = {}, useX402 = false) {
export async function fetchAPI(pathname, params = {}, useX402 = false, isRetry = false) {
const apiKey = useX402 ? null : getApiKey();
if (!useX402 && !apiKey) {
const err = new Error(
Expand Down Expand Up @@ -48,6 +48,12 @@ export async function fetchAPI(pathname, params = {}, useX402 = false) {
}

if (!response.ok) {
// Hybrid fallback: If rate-limited or quota exceeded on standard API and we have a wallet key, fallback to x402
if ([401, 403, 429].includes(response.status) && !useX402 && !isRetry && process.env.WALLET_PRIVATE_KEY) {
process.stderr.write(`\\x1b[33m⚠ API error (${response.status}). Falling back to x402 pay-per-call for ${pathname}...\\x1b[0m\\n`);
return fetchAPI(pathname, params, true, true);
}

const err = new Error(
`Zerion API error: ${response.status} ${response.statusText}`
);
Expand Down
95 changes: 95 additions & 0 deletions cli/lib/util/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,98 @@ export function formatPnl(data) {
if (p.totalFees != null) lines.push(` Fees Paid: ${usd(p.totalFees)}`);
return lines.join("\n");
}

export function formatOverview(data) {
const walletLabel = data.label || data.wallet?.query || "Unknown";
const addr = data.wallet?.query || "";
const shortAddr = addr.length > 12 ? `${addr.slice(0, 6)}...${addr.slice(-4)}` : addr;
const lines = [];

// Header
lines.push(`${BOLD}━━━ Wallet Analysis ━━━${RESET}`);
lines.push(` ${BOLD}${walletLabel}${RESET} ${DIM}${shortAddr}${RESET}`);
lines.push("");

// Portfolio
if (data.portfolio) {
const total = data.portfolio.total;
lines.push(` ${BOLD}💰 Portfolio${RESET} ${BOLD}${usd(total)}${RESET}`);
const ch = data.portfolio.change_1d;
if (ch) {
const absChange = usd(ch.absolute_1d);
const pctChange = pct(ch.percent_1d);
lines.push(` ${DIM}24h:${RESET} ${pctChange} (${absChange})`);
}

// Top chains by value
if (data.portfolio.chains) {
const chainEntries = Object.entries(data.portfolio.chains)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);
if (chainEntries.length > 0) {
lines.push("");
lines.push(` ${DIM}Top chains:${RESET}`);
for (const [chain, val] of chainEntries) {
lines.push(` ${pad(chain, 22)} ${padStart(usd(val), 14)}`);
}
}
}
lines.push("");
}

// Top Positions
if (data.positions && data.positions.count > 0) {
lines.push(` ${BOLD}📊 Top Positions${RESET} (${data.positions.count} total)`);
lines.push(` ${DIM}${pad("Token", 20)} ${pad("Chain", 14)} ${padStart("Value", 14)} ${padStart("Amount", 16)}${RESET}`);
lines.push(` ${DIM}${"─".repeat(66)}${RESET}`);
for (const p of data.positions.top) {
const sym = p.symbol ? `${p.name} (${p.symbol})` : p.name;
const qty = p.quantity != null ? p.quantity.toFixed(4) : "-";
lines.push(` ${pad(sym, 20)} ${pad(p.chain || "?", 14)} ${padStart(usd(p.value), 14)} ${padStart(qty, 16)}`);
}
lines.push("");
} else {
lines.push(` ${DIM}📊 No positions data${RESET}`);
lines.push("");
}

// Recent Transactions
if (data.transactions && data.transactions.sampled > 0) {
lines.push(` ${BOLD}📝 Recent Transactions${RESET} (${data.transactions.sampled} sampled)`);
for (const tx of data.transactions.recent) {
const status = tx.status === "confirmed" ? `${GREEN}✓${RESET}` : tx.status === "pending" ? `${YELLOW}⏳${RESET}` : `${DIM}${tx.status || "?"}${RESET}`;
const type = tx.operation_type || "unknown";
const time = tx.mined_at ? new Date(tx.mined_at * 1000).toISOString().replace("T", " ").slice(0, 16) : "?";
lines.push(` ${status} ${DIM}${time}${RESET} ${type}`);
for (const t of tx.transfers || []) {
const dir = t.direction === "in" ? `${GREEN}+${RESET}` : `${RED}-${RESET}`;
const name = t.fungible_info?.symbol || t.fungible_info?.name || "?";
lines.push(` ${dir} ${t.quantity || "?"} ${name} ${DIM}${usd(t.value)}${RESET}`);
}
}
lines.push("");
} else {
lines.push(` ${DIM}📝 No recent transactions${RESET}`);
lines.push("");
}

// PnL
if (data.pnl && data.pnl.available && data.pnl.summary) {
const s = data.pnl.summary;
lines.push(` ${BOLD}📈 PnL${RESET}`);
if (s.total_gain != null) lines.push(` Total Gain: ${usd(s.total_gain)}`);
if (s.realized_gain != null) lines.push(` Realized: ${usd(s.realized_gain)}`);
if (s.unrealized_gain != null) lines.push(` Unrealized: ${usd(s.unrealized_gain)}`);
} else {
lines.push(` ${DIM}📈 PnL not available${RESET}`);
}

// Failures
if (data.failures && data.failures.length > 0) {
lines.push("");
lines.push(` ${YELLOW}⚠ Some data unavailable: ${data.failures.join(", ")}${RESET}`);
}

return lines.join("\n");
}

3 changes: 3 additions & 0 deletions cli/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ function printUsage() {
},
other: {
"chains": "List supported chains",
"apikey": "Show current API key (redacted) and its source",
"apikey set <key>": "Save API key to config",
"apikey unset": "Remove saved API key from config",
"config set <key> <value>": "Set config (apiKey, defaultWallet, defaultChain, slippage)",
"config unset <key>": "Remove a config value (resets to default)",
"config list": "Show current configuration",
Expand Down
2 changes: 2 additions & 0 deletions cli/zerion.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,9 @@ register("agent", "delete-policy", agentDeletePolicy);
// --- Config ---

import configCmd from "./commands/config.js";
import apikeyCmd from "./commands/apikey.js";
registerSingle("config", configCmd);
registerSingle("apikey", apikeyCmd);

// --- Dispatch ---

Expand Down
Loading