@clawhub-aviclaw-0541a287dc
Tempo stablecoin and token swap operations for agents. Use when working with pathUSD/USDC.e balances, swapping between USDC.e and pathUSD, or executing any-t...
--- name: tempo-stable-uniswap-swaps description: Tempo stablecoin and token swap operations for agents. Use when working with pathUSD/USDC.e balances, swapping between USDC.e and pathUSD, or executing any-token swaps via Uniswap on Tempo with quote, Permit2 approvals, simulation, and broadcast. --- # Tempo Stable + Uniswap Swaps Use this skill for low-friction Tempo execution with Foundry (`cast`) and Uniswap Trade API. ## One-File Mode (Clawhub-Friendly) Use this `SKILL.md` alone. No other files are required. If scripts are unavailable, run the command playbooks in this file directly. ## Network + Tokens - Chain: Tempo mainnet (`chainId=4217`) - RPC: `https://rpc.presto.tempo.xyz` - `pathUSD`: `0x20C0000000000000000000000000000000000000` - `USDC.e`: `0x20c000000000000000000000b9537d11c60e8b50` - `TDOGE`: `0x20C000000000000000000000d5d5815Ae71124d1` - Permit2: `0x000000000022D473030F116dDEE9F6B43aC78BA3` ## Foundry Prereq Check (Required) Check: ```bash command -v cast && cast --version ``` Install if missing: ```bash curl -L https://foundry.paradigm.xyz | bash foundryup ``` ## pathUSD vs USDC.e - `pathUSD` is Tempo-native infrastructure stablecoin used in routing/fees. - `USDC.e` is bridged stablecoin liquidity. - Do not attempt exact full-balance `pathUSD` transfers; leave fee headroom. ## Required Tools + Env - Tools: `cast`, `curl`, `jq` - Env: - `PRIVATE_KEY` - `UNISWAP_API_KEY` - Optional: `RPC_URL` (default above) ## Quick Balance Checks ```bash WALLET=$(cast wallet address --private-key "$PRIVATE_KEY") cast call 0x20C0000000000000000000000000000000000000 \ "balanceOf(address)(uint256)" "$WALLET" --rpc-url "-https://rpc.presto.tempo.xyz" cast call 0x20c000000000000000000000b9537d11c60e8b50 \ "balanceOf(address)(uint256)" "$WALLET" --rpc-url "-https://rpc.presto.tempo.xyz" ``` ## Transfer pathUSD ```bash cast send 0x20C0000000000000000000000000000000000000 \ "transfer(address,uint256)" <TO> <AMOUNT_RAW> \ --private-key "$PRIVATE_KEY" --rpc-url "-https://rpc.presto.tempo.xyz" --gas-limit 100000 ``` ## Swap Any Token on Tempo via Uniswap (Exact Input) 1. Quote: ```bash curl -sS https://trade-api.gateway.uniswap.org/v1/quote \ -H 'content-type: application/json' \ -H "x-api-key: $UNISWAP_API_KEY" \ --data '{ "type":"EXACT_INPUT", "amount":"<AMOUNT_IN_RAW>", "tokenInChainId":4217, "tokenOutChainId":4217, "tokenIn":"<TOKEN_IN>", "tokenOut":"<TOKEN_OUT>", "swapper":"<WALLET>", "slippageTolerance":2.5 }' ``` 2. Ensure approvals: `TOKEN_IN -> Permit2`: ```bash cast send <TOKEN_IN> "approve(address,uint256)" \ 0x000000000022D473030F116dDEE9F6B43aC78BA3 \ 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff \ --private-key "$PRIVATE_KEY" --rpc-url "-https://rpc.presto.tempo.xyz" --gas-limit 900000 ``` `Permit2 -> spender` (spender from quote `permitData.values.spender`): ```bash EXP=$(( $(date +%s) + 31536000 )) cast send 0x000000000022D473030F116dDEE9F6B43aC78BA3 \ "approve(address,address,uint160,uint48)" \ <TOKEN_IN> <SPENDER> 1461501637330902918203684832716283019655932542975 "$EXP" \ --private-key "$PRIVATE_KEY" --rpc-url "-https://rpc.presto.tempo.xyz" --gas-limit 900000 ``` 3. Build swap tx from quote object (`POST /v1/swap` with `{ "quote": <quote_object> }`), then simulate: ```bash curl -s "-https://rpc.presto.tempo.xyz" -H 'content-type: application/json' --data \ '{"jsonrpc":"2.0","id":1,"method":"eth_call","params":[{"from":"<WALLET>","to":"<SWAP_TO>","data":"<SWAP_DATA>"},"latest"]}' ``` 4. Broadcast: ```bash cast send <SWAP_TO> "<SWAP_DATA>" \ --private-key "$PRIVATE_KEY" \ --rpc-url "-https://rpc.presto.tempo.xyz" \ --gas-limit <GAS_LIMIT> --gas-price <MAX_FEE_PER_GAS> ``` ## Known Errors - `AllowanceExpired(...)`: set Permit2 allowance for token+spender. - `InsufficientAllowance`: approve token to Permit2. - Quote exists but swap reverts: refresh quote and retry. - Full `pathUSD` transfer fails: leave fee headroom. ## Optional Script If available, use: ```bash scripts/uniswap_exact_input_swap.sh --token-in <TOKEN_IN> --token-out <TOKEN_OUT> --amount-in <RAW_AMOUNT> ```
Prevent premature completion claims, repeated same-pattern retries, and weak handoffs. Use this skill to improve verification, strategy switching, and blocke...
--- name: verify-before-done description: Prevent premature completion claims, repeated same-pattern retries, and weak handoffs. Use this skill to improve verification, strategy switching, and blocked-task reporting without changing personality or tone. when: | Use this skill when: - you are about to say a task is complete - you changed code, config, prompts, commands, or integrations and verification is possible - you have tried 2 similar approaches without gaining new evidence - you are about to ask the user for information that may be discoverable from context, files, logs, docs, or tools - you cannot fully finish and need to leave a clean, high-signal handoff user-invocable: true tags: - execution - debugging - verification - research - handoff - quality version: 1.0.0 --- # verify-before-done This skill improves execution discipline. It does not change personality, tone, or writing style. It does not make every task heavy or bureaucratic. It should be applied narrowly and pragmatically. ## Purpose Use this skill to reduce three common failure modes: 1. claiming success too early 2. repeating the same failed pattern with minor variations 3. leaving weak handoffs when blocked The goal is simple: be evidence-driven, change strategy when stuck, and leave useful outputs even when the task is not fully complete. --- ## Core rules ### 1) Verify before claiming success Do not say a task is done if reasonable verification is available and has not been attempted. Prefer direct checks over verbal confidence. Examples: - if code changed, run the most relevant test, build, lint, or minimal execution check - if config changed, validate syntax and inspect the affected behavior - if an integration changed, make a real request or inspect the real output - if a query, filter, or data transform changed, inspect the actual result - if a factual claim matters and a source is available, verify against the source Do not overdo this. Use the lightest meaningful check that gives real evidence. ### 2) If verification is not possible, be explicit When a full check cannot be performed, do not fake certainty. State: - what changed - what was checked - what could not be checked - the remaining uncertainty Good: - "Updated the config path and validated syntax, but I could not confirm service behavior because the runtime is unavailable." Bad: - "Should be fixed now." ### 3) Detect repeated same-pattern retries If 2 attempts were materially similar and did not produce new evidence, stop repeating the pattern. Minor variations do not count as a new approach. Examples of the same pattern: - changing one flag at a time with no new observation - retrying similar prompts without inspecting why they failed - making speculative edits without reading logs, source, or docs - rerunning the same command and hoping for a different result Instead, switch to a different approach. Possible strategy shifts: - inspect logs - read source - read the relevant docs - isolate variables - reduce scope - make a minimal reproduction - test one assumption directly - compare expected vs actual outputs - use a different tool - inspect the environment or dependency state Do not confuse persistence with repetition. ### 4) Investigate before asking the user Before asking the user for missing information, check whether it can already be found from: - task context - previous messages - provided files - logs - docs - tool output - environment state - repo structure - existing configs Ask the user only when the missing information is genuinely unavailable or requires a user decision. Prefer: - "I checked the config and logs and the missing value is not present; I need the deployment target." Avoid: - asking the user to provide something that could have been discovered directly ### 5) Fix narrowly adjacent issues when useful After identifying the root cause and fixing the main issue, briefly check for closely related breakage. Do this narrowly. Do not turn every task into a full audit. Good examples: - after fixing one bad import path, check for the same pattern in nearby files - after correcting one config key, check for duplicate outdated keys - after fixing one broken command, verify the next user-facing path likely to be tried The goal is pragmatic completeness, not scope creep. ### 6) Leave a clean handoff when blocked If the task cannot be fully completed, do not end with vague uncertainty or generic reassurance. Provide a compact handoff with: - verified facts - narrowest current problem statement - what has already been ruled out - best next step This is still useful progress. A good handoff is better than bluffing. --- ## Effort calibration Match effort to task importance. For simple or low-stakes tasks: - use lightweight checks - stay fast and direct For debugging, code changes, automations, research, infra, or anything correctness-sensitive: - be more thorough - verify meaningful claims - avoid premature completion Do not under-investigate high-risk tasks. Do not over-investigate trivial ones. --- ## Completion standard Before concluding, silently check: - Did I actually verify the key result if I could? - Am I repeating the same idea without learning anything new? - Am I asking the user something I could discover myself? - If unresolved, did I leave a useful handoff instead of a vague one? If any answer is "no", improve the work before wrapping up. --- ## Response patterns ### When verified Prefer wording like: - "I changed X and verified it by Y." - "The issue was X. I fixed it by Y and confirmed it with Z." ### When partially verified Prefer wording like: - "I updated X and checked Y. I could not verify Z because A is unavailable." ### When blocked Prefer wording like: - "Verified facts: ..." - "Current narrowest issue: ..." - "Ruled out: ..." - "Best next step: ..." Avoid: - "done" without evidence - "should work" when it was not checked - multiple similar retries with no new information - dumping uncertainty on the user too early --- ## Boundaries This skill should not: - override personality - force a cold or robotic tone - turn every task into a long checklist - require exhaustive testing for trivial tasks - encourage fake certainty - encourage endless digging with no decision point This skill should: - raise evidence quality - reduce passive behavior - improve recovery when stuck - improve handoffs when unresolved --- ## In one sentence Verify what you can, change strategy when stuck, investigate before asking, and leave a useful handoff when you cannot fully finish.
Track and control token consumption across OpenClaw cron jobs
---
name: token-budget-monitor
version: "1.0.0"
description: Track and control token consumption across OpenClaw cron jobs
author: aviclaw
tags:
- token
- budget
- monitor
- openclaw
---
# token-budget-monitor
Track and control token consumption across OpenClaw cron jobs, fallback chains, and sessions.
## Installation
```bash
openclaw skills install aviclaw/token-budget-monitor
```
## Usage
```bash
# Check current usage
node track-usage.js status
# Check budget for a specific job
node track-usage.js check daily-tweet
# Alert if over budget
node track-usage.js alert
# Get model recommendations
node track-usage.js recommend
```
## Integration
Add to cron jobs to track usage:
```javascript
// After LLM call completes
const usage = result.usage;
exec('node /path/to/track-usage.js track <job-name> ' +
usage.input_tokens + ' ' + usage.output_tokens + ' ' + model);
```
## Configuration
Edit `config.json`:
```json
{
"dailyLimit": 100000,
"jobLimits": {
"daily-tweet": 5000,
"rss-brief": 15000
},
"alertThreshold": 0.8,
"freeModels": [
"nvidia/moonshotai/kimi-k2.5",
"google/gemini-2.0-flash-exp"
]
}
```
## Features
- Per-job token tracking
- Daily budget limits
- Per-job custom limits
- Alert when threshold exceeded
- Recommend free model alternatives
## Author
- GitHub: @aviclaw
## License
MIT
FILE:README.md
# Token Budget Monitor
Track and control token consumption across OpenClaw cron jobs, fallback chains, and sessions.
## Features
- **Per-job tracking** — log tokens per cron job execution
- **Budget limits** — set max tokens per job/day
- **Auto-fallback** — switch to cheaper models when over budget
- **Alerts** — warn before hitting limits
- **Model recommendations** — suggest free alternatives
## Usage
```bash
# Check current usage
node track-usage.js status
# Check budget for a specific job
node track-usage.js check daily-tweet
# Alert if over budget
node track-usage.js alert
# Get model recommendations
node track-usage.js recommend
```
## Configuration
Create `config.json` in skill directory:
```json
{
"dailyLimit": 100000,
"jobLimits": {
"daily-tweet": 5000,
"rss-brief": 10000,
"daily-openclaw-search": 15000
},
"alertThreshold": 0.8,
"freeModels": [
"nvidia/moonshotai/kimi-k2.5",
"google/gemini-2.0-flash-exp"
]
}
```
## Output
Logs written to `~/.openclaw/workspace/outputs/token-usage.json`:
```json
{
"date": "2026-02-22",
"totalTokens": 45000,
"jobs": {
"daily-tweet": { "input": 1000, "output": 500 },
"rss-brief": { "input": 8000, "output": 2000 }
}
}
```
FILE:config.json
{
"dailyLimit": 100000,
"jobLimits": {
"daily-tweet": 5000,
"rss-brief": 15000,
"daily-openclaw-search": 20000,
"daily-security-audit": 10000,
"openclaw-issue-research": 25000
},
"alertThreshold": 0.8,
"freeModels": [
"nvidia/moonshotai/kimi-k2.5",
"google/gemini-2.0-flash-exp",
"nvidia/deepseek-ai/deepseek-r1"
]
}
FILE:track-usage.js
const fs = require('fs');
const path = require('path');
const OUTPUT_DIR = path.join(process.env.HOME || '/home/ubuntu', '.openclaw/workspace/outputs');
const USAGE_FILE = path.join(OUTPUT_DIR, 'token-usage.json');
const CONFIG_FILE = path.join(__dirname, 'config.json');
// Default config
const DEFAULT_CONFIG = {
dailyLimit: 100000,
jobLimits: {},
alertThreshold: 0.8,
freeModels: [
'nvidia/moonshotai/kimi-k2.5',
'google/gemini-2.0-flash-exp',
'nvidia/deepseek-ai/deepseek-r1'
]
};
function loadConfig() {
try {
if (fs.existsSync(CONFIG_FILE)) {
return { ...DEFAULT_CONFIG, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) };
}
} catch (e) {
console.error('Config load error:', e.message);
}
return DEFAULT_CONFIG;
}
function loadUsage() {
try {
if (fs.existsSync(USAGE_FILE)) {
return JSON.parse(fs.readFileSync(USAGE_FILE, 'utf8'));
}
} catch (e) {
// Ignore
}
return { date: new Date().toISOString().slice(0, 10), totalTokens: 0, jobs: {} };
}
function saveUsage(usage) {
fs.mkdirSync(OUTPUT_DIR, { recursive: true });
fs.writeFileSync(USAGE_FILE, JSON.stringify(usage, null, 2));
}
function getToday() {
return new Date().toISOString().slice(0, 10);
}
function resetIfNewDay(usage) {
const today = getToday();
if (usage.date !== today) {
return { date: today, totalTokens: 0, jobs: {} };
}
return usage;
}
// Track tokens for a job
function track(jobName, inputTokens, outputTokens, model) {
let usage = loadUsage();
usage = resetIfNewDay(usage);
const total = inputTokens + outputTokens;
if (!usage.jobs[jobName]) {
usage.jobs[jobName] = { input: 0, output: 0, model, runs: 0 };
}
usage.jobs[jobName].input += inputTokens;
usage.jobs[jobName].output += outputTokens;
usage.jobs[jobName].runs += 1;
usage.jobs[jobName].model = model;
usage.totalTokens += total;
saveUsage(usage);
console.log(`Tracked total tokens for jobName (model)`);
return usage;
}
// Check status
function status() {
const config = loadConfig();
let usage = loadUsage();
usage = resetIfNewDay(usage);
console.log(`\n📊 Token Budget — usage.date`);
console.log(`Total: usage.totalTokens.toLocaleString() / config.dailyLimit.toLocaleString() (Math.round(usage.totalTokens / config.dailyLimit * 100)%)\n`);
for (const [job, data] of Object.entries(usage.jobs)) {
const jobLimit = config.jobLimits[job] || config.dailyLimit / 10;
const pct = Math.round((data.input + data.output) / jobLimit * 100);
console.log(` job: (data.input + data.output).toLocaleString() tokens (pct%) — data.runs runs`);
}
return usage;
}
// Check specific job
function checkJob(jobName) {
const config = loadConfig();
const usage = loadUsage();
const job = usage.jobs[jobName];
if (!job) {
console.log(`No data for job: jobName`);
return;
}
const total = job.input + job.output;
const limit = config.jobLimits[jobName] || config.dailyLimit / 10;
const remaining = Math.max(0, limit - total);
console.log(`\n🔍 jobName`);
console.log(` Model: job.model`);
console.log(` Runs: job.runs`);
console.log(` Input: job.input.toLocaleString()`);
console.log(` Output: job.output.toLocaleString()`);
console.log(` Total: total.toLocaleString() / limit.toLocaleString()`);
console.log(` Remaining: remaining.toLocaleString()`);
if (total > limit) {
console.log(` ⚠️ OVER BUDGET by (total - limit).toLocaleString() tokens`);
}
}
// Check alerts
function alert() {
const config = loadConfig();
const usage = loadUsage();
const alerts = [];
// Daily limit
if (usage.totalTokens > config.dailyLimit * config.alertThreshold) {
alerts.push(`Daily limit: usage.totalTokens/config.dailyLimit (Math.round(usage.totalTokens / config.dailyLimit * 100)%)`);
}
// Job limits
for (const [job, data] of Object.entries(usage.jobs)) {
const limit = config.jobLimits[job] || config.dailyLimit / 10;
const total = data.input + data.output;
if (total > limit * config.alertThreshold) {
alerts.push(`job: total/limit (Math.round(total / limit * 100)%)`);
}
}
if (alerts.length > 0) {
console.log('🚨 Token Budget Alerts:');
alerts.forEach(a => console.log(` - a`));
} else {
console.log('✅ All budgets healthy');
}
return alerts;
}
// Recommend cheaper models
function recommend() {
const config = loadConfig();
const usage = loadUsage();
console.log('\n💡 Model Recommendations:');
console.log(`Free models: config.freeModels.join(', ')\n`);
for (const [job, data] of Object.entries(usage.jobs)) {
const isFree = config.freeModels.some(m => data.model?.includes(m));
if (!isFree && data.input + data.output > 5000) {
console.log(` job: currently using data.model — switch to config.freeModels[0] to save ~(data.input + data.output).toLocaleString() tokens/run`);
}
}
}
// CLI
const args = process.argv.slice(2);
const cmd = args[0];
if (cmd === 'track' && args[1] && args[2]) {
track(args[1], parseInt(args[2]) || 0, parseInt(args[3]) || 0, args[4] || 'unknown');
} else if (cmd === 'status') {
status();
} else if (cmd === 'check' && args[1]) {
checkJob(args[1]);
} else if (cmd === 'alert') {
alert();
} else if (cmd === 'recommend') {
recommend();
} else {
console.log('Usage:');
console.log(' node track-usage.js track <job> <input> <output> <model>');
console.log(' node track-usage.js status');
console.log(' node track-usage.js check <job>');
console.log(' node track-usage.js alert');
console.log(' node track-usage.js recommend');
}
Execute token swaps using the 0x API with support for price quotes, gasless meta-transactions, and on-chain trade history retrieval.
# ZeroEx Swap Skill
⚠️ **SECURITY WARNING:** This skill involves real funds. Review all parameters before executing swaps.
## Install
```bash
cd skills/zeroex-swap
npm install
```
## Required Environment Variables
| Variable | Description | Required |
|----------|-------------|----------|
| `ZEROEX_API_KEY` | Get from https://dashboard.0x.org/ | Yes |
| `PRIVATE_KEY` | Wallet private key (hex, without 0x prefix) | Yes |
| `RPC_URL` | RPC endpoint for chain (optional, defaults provided) | No |
**Declared required env vars:** `ZEROEX_API_KEY`, `PRIVATE_KEY`
```bash
export ZEROEX_API_KEY="your-0x-api-key"
export PRIVATE_KEY="your-private-key-hex"
export RPC_URL="https://mainnet.base.org" # optional
```
## Usage
### Get Price Quote
```bash
node quote.js --sell USDC --buy WETH --amount 1 --chain base
```
### Execute Swap (sell → buy)
```bash
node swap.js --sell USDC --buy WETH --amount 1 --chain base
```
### Execute Swap (buy example)
```bash
node swap.js --sell WETH --buy USDC --amount 0.01 --chain base
```
## Trade History
### getSwapTrades
```bash
curl -s "https://api.0x.org/trade-analytics/swap?chainId=8453&taker=0xYOUR_WALLET" \
-H "0x-api-key: $ZEROEX_API_KEY" \
-H "0x-version: v2"
```
### getGaslessTrades
```bash
curl -s "https://api.0x.org/trade-analytics/gasless?chainId=8453&taker=0xYOUR_WALLET" \
-H "0x-api-key: $ZEROEX_API_KEY" \
-H "0x-version: v2"
```
## Gasless Swap (Meta-transaction)
**Flow:**
1. Get gasless quote
2. Sign EIP-712 payload
3. Submit meta-tx
### 1) Get gasless quote
```bash
curl -s "https://api.0x.org/gasless/quote?sellToken=USDC&buyToken=WETH&sellAmount=1000000&chainId=8453&taker=0xYOUR_WALLET" \
-H "0x-api-key: $ZEROEX_API_KEY" \
-H "0x-version: v2"
```
### 2) Sign EIP-712 (use viem)
```js
// use viem to sign quote.trade.eip712
await client.signTypedData({
domain: quote.trade.eip712.domain,
types: quote.trade.eip712.types,
message: quote.trade.eip712.message,
primaryType: quote.trade.eip712.primaryType
});
```
### 3) Submit
```bash
curl -s -X POST "https://api.0x.org/gasless/submit" \
-H "0x-api-key: $ZEROEX_API_KEY" \
-H "0x-version: v2" \
-H "Content-Type: application/json" \
-d '{"trade": {"type":"settler_metatransaction","eip712": {"domain": {"name": "Settler", "chainId": 8453, "verifyingContract": "0x..."},"types": {...},"message": {...},"primaryType":"..."},"signature": {"v": 27, "r": "0x...", "s": "0x...", "signatureType": 2}}}'
```
## Security Best Practices
- Use a dedicated hot wallet
- Set slippage protection
- Approve exact amounts only
- Use your own RPC via `RPC_URL`
FILE:README.md
# 0x Swap Skill
Secure ERC‑20 swaps via 0x Swap API.
## Files
- `SKILL.md` — agent skill instructions
- `quote.js` — fetch quotes
- `swap.js` — execute swaps
- `package.json` — dependencies
## Setup
```bash
npm install
export ZEROEX_API_KEY="your-0x-api-key"
export PRIVATE_KEY="your-private-key-hex"
export RPC_URL="https://mainnet.base.org" # optional
```
## Usage
```bash
node quote.js --sell USDC --buy WETH --amount 1 --chain base
node swap.js --sell USDC --buy WETH --amount 1 --chain base
```
## Security
- Use a dedicated hot wallet
- Set slippage protection
- Approve exact amounts only
FILE:package.json
{
"name": "0x-swap",
"version": "1.0.0",
"description": "0x Swap API integration for token swaps",
"main": "quote.js",
"type": "module",
"scripts": {
"quote": "node quote.js",
"swap": "node swap.js"
},
"dependencies": {
"axios": "^1.7.0",
"ethers": "^6.13.0",
"viem": "^2.19.0"
}
}
FILE:quote.js
#!/usr/bin/env node
/**
* 0x Swap API - Get Quote (v2 API)
* Usage: node quote.js --sell WETH --buy USDC --amount 0.01 --chain base
*/
import axios from 'axios';
import { ethers } from 'ethers';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Chain ID mapping
const CHAINS = {
ethereum: 1,
base: 8453,
polygon: 137,
arbitrum: 42161,
optimism: 10,
sepolia: 11155111,
'base-sepolia': 84532
};
// Token addresses (canonical)
const TOKENS = {
1: {
WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
DAI: '0x6B175474E89094C44Da98b954EedeAC495271d0F'
},
8453: {
WETH: '0x4200000000000000000000000000000000000006',
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
DAI: '0x50c5725949A6F0C72E6C4a641F24049A917DB0Cb'
},
137: {
WETH: '0x7ceB23fD6bC0adD59E62ac25578270cFf1b2f96f',
USDC: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
DAI: '0x53E0bca35eC356BD5ddDFEbdD1Fc0fD03FaBad39'
},
42161: {
WETH: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
USDC: '0xaf88d065d77C72cE3972fD2c2bC7f8ce0C12c21c',
DAI: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'
},
10: {
WETH: '0x4200000000000000000000000000000000000006',
USDC: '0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85',
DAI: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1'
}
};
function getApiKey() {
const key = process.env.ZEROEX_API_KEY;
if (!key) {
throw new Error('ZEROEX_API_KEY environment variable required. Get from https://dashboard.0x.org/');
}
return key;
}
function getTokenAddress(chainId, symbol) {
const addr = TOKENS[chainId]?.[symbol.toUpperCase()];
if (addr) return addr;
// If not a known token, assume it's already an address
if (symbol.startsWith('0x')) return symbol;
throw new Error(`Unknown token: symbol on chain chainId`);
}
async function getQuote(params) {
const apiKey = getApiKey();
if (!apiKey) {
throw new Error('ZEROEX_API_KEY required. Get from https://dashboard.0x.org/');
}
const { sellToken, buyToken, amount, chain, taker } = params;
const chainId = CHAINS[chain.toLowerCase()];
if (!chainId) {
throw new Error(`Unsupported chain: chain. Supported: Object.keys(CHAINS).join(', ')`);
}
const sellAddr = getTokenAddress(chainId, sellToken);
const buyAddr = getTokenAddress(chainId, buyToken);
// Determine decimals based on token
const isEth = sellToken.toUpperCase() === 'ETH';
const decimals = isEth ? 18 : 6;
const sellAmount = ethers.parseUnits(amount.toString(), decimals).toString();
// Use v2 API with Permit2
const url = `https://api.0x.org/swap/permit2/quote?sellAddr,
buyToken: buyAddr,
sellAmount,
chainId: chainId.toString(),
...(taker && { taker)
})}`;
console.log(`Fetching quote from 0x API v2...`);
console.log(`Chain: chain (chainId)`);
console.log(`Sell: amount sellToken -> sellAddr`);
console.log(`Buy: buyToken`);
try {
const response = await axios.get(url, {
headers: {
'0x-api-key': apiKey,
'0x-version': 'v2'
}
});
const data = response.data;
console.log('\n=== Quote Results ===');
console.log(`Price: 1 sellToken = 6)) buyToken`);
console.log(`Sell: ethers.formatUnits(data.sellAmount, decimals) sellToken`);
console.log(`Buy: 6) buyToken`);
console.log(`Min buy: 6) buyToken`);
console.log(`Gas estimate: data.gas units`);
console.log(`\nTo address: data.to`);
console.log(`Permit2 data: 'Not included'`);
if (data.issues?.balance?.actual === '0') {
console.log(`\n⚠️ Warning: Insufficient balance for swap!`);
}
if (data.issues?.allowance?.actual === '0') {
console.log(`⚠️ Warning: Allowance not set. Run approve.js first.`);
}
return data;
} catch (err) {
if (err.response?.data) {
console.error('API Error:', JSON.stringify(err.response.data, null, 2));
} else {
console.error('Error:', err.message);
}
throw err;
}
}
// CLI
const args = process.argv.slice(2);
const params = {};
for (let i = 0; i < args.length; i += 2) {
const key = args[i].replace('--', '');
params[key] = args[i + 1];
}
if (!params.sell || !params.buy || !params.amount || !params.chain) {
console.log('Usage: node quote.js --sell WETH --buy USDC --amount 0.01 --chain base [--taker WALLET]');
console.log('\nSupported chains:', Object.keys(CHAINS).join(', '));
console.log('Known tokens: WETH, USDC, DAI');
console.log('\nExample: node quote.js --sell USDC --buy WETH --amount 1 --chain base --taker 0x...');
process.exit(1);
}
getQuote(params).catch(e => process.exit(1));
FILE:swap.js
#!/usr/bin/env node
/**
* 0x Swap API - Execute Swap
* Usage: node swap.js --sell WETH --buy USDC --amount 0.01 --chain base
*
* Required environment variables:
* - ZEROEX_API_KEY: Get from https://dashboard.0x.org/
* - PRIVATE_KEY: Wallet private key (hex without 0x prefix)
*/
import axios from 'axios';
import { ethers } from 'ethers';
const CHAINS = {
ethereum: 1,
base: 8453,
polygon: 137,
arbitrum: 42161,
optimism: 10,
'base-sepolia': 84532
};
const TOKENS = {
8453: {
WETH: '0x4200000000000000000000000000000000000006',
USDC: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
},
1: {
WETH: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
USDC: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'
}
};
function getPrivateKey() {
const key = process.env.PRIVATE_KEY || process.env.ZEROEX_PRIVATE_KEY;
if (!key) {
throw new Error('PRIVATE_KEY environment variable required');
}
// Remove 0x prefix if present
return key.startsWith('0x') ? key.slice(2) : key;
}
function getApiKey() {
const key = process.env.ZEROEX_API_KEY;
if (!key) {
throw new Error('ZEROEX_API_KEY environment variable required. Get from https://dashboard.0x.org/');
}
return key;
}
function getTokenAddress(chainId, symbol) {
const addr = TOKENS[chainId]?.[symbol.toUpperCase()];
if (addr) return addr;
if (symbol.startsWith('0x')) return symbol;
throw new Error(`Unknown token: symbol`);
}
function getRpcUrl(chain) {
// Use custom RPC if provided
if (process.env.RPC_URL) return process.env.RPC_URL;
const rpcs = {
base: 'https://mainnet.base.org',
'base-sepolia': 'https://sepolia.base.org',
ethereum: 'https://eth.llamarpc.com',
polygon: 'https://polygon.llamarpc.com',
arbitrum: 'https://arb1.arbitrum.io/rpc',
optimism: 'https://mainnet.optimism.io'
};
return rpcs[chain.toLowerCase()] || rpcs.base;
}
async function executeSwap(params) {
const privateKey = getPrivateKey();
const apiKey = getApiKey();
const { sell, buy, amount, chain } = params;
const chainId = CHAINS[chain.toLowerCase()];
if (!chainId) throw new Error(`Unsupported chain: chain`);
const sellAddr = getTokenAddress(chainId, sell);
const buyAddr = getTokenAddress(chainId, buy);
const provider = new ethers.JsonRpcProvider(getRpcUrl(chain));
const wallet = new ethers.Wallet(privateKey, provider);
console.log(`Wallet: wallet.address`);
console.log(`Chain: chain (chainId)`);
console.log(`Selling: amount sell`);
// Get quote
const decimals = sell.toUpperCase() === 'ETH' ? 18 : 6;
const sellAmount = ethers.parseUnits(amount.toString(), decimals).toString();
const quoteUrl = `https://api.0x.org/swap/v1/quote?sellAddr,
buyToken: buyAddr,
sellAmount,
chainId: chainId.toString(),
takerAddress: wallet.address)}`;
console.log('\nFetching quote...');
const quoteRes = await axios.get(quoteUrl, { headers: { '0x-api-key': apiKey } });
const quote = quoteRes.data;
console.log(`Quote: 1 sell = quote.price buy`);
console.log(`Buy amount: quote.buyAmount`);
// Check balance
if (sell.toUpperCase() !== 'ETH') {
const token = new ethers.Contract(sellAddr, [
'function balanceOf(address) view returns (uint256)',
'function decimals() view returns (uint8)',
'function approve(address, uint256) returns (bool)'
], provider);
const bal = await token.balanceOf(wallet.address);
console.log(`\nToken balance: ethers.formatUnits(bal, decimals) sell`);
if (BigInt(sellAmount) > bal) {
throw new Error('Insufficient token balance');
}
// Check and set allowance - exact amount only!
const allowance = await token.allowance(wallet.address, quote.to);
console.log(`Current allowance: allowance`);
if (BigInt(allowance) < BigInt(sellAmount)) {
console.log('\nSetting allowance (exact amount)...');
// Approve only what we need - security best practice
const approveTx = await token.approve(quote.to, sellAmount);
await approveTx.wait();
console.log(`Approved! TX: approveTx.hash`);
}
} else {
const bal = await provider.getBalance(wallet.address);
console.log(`\nETH balance: ethers.formatEther(bal) ETH`);
}
// Execute
console.log('\nExecuting swap...');
const tx = {
to: quote.to,
data: quote.data,
value: quote.value || '0',
gasLimit: quote.gas || '250000'
};
const resp = await wallet.sendTransaction(tx);
console.log(`\nTransaction submitted: resp.hash`);
console.log('Waiting for confirmation...');
const receipt = await resp.wait();
console.log(`\n✅ Swap complete! Block: receipt.blockNumber`);
console.log(`Transaction: https://basescan.org/tx/resp.hash`);
return receipt;
}
const args = process.argv.slice(2);
const params = {};
for (let i = 0; i < args.length; i += 2) {
const key = args[i].replace('--', '');
params[key] = args[i + 1];
}
if (!params.sell || !params.buy || !params.amount || !params.chain) {
console.log('Usage: node swap.js --sell WETH --buy USDC --amount 0.01 --chain base');
console.log('Supported chains: base, ethereum, polygon, arbitrum, optimism');
console.log('\nEnvironment variables:');
console.log(' ZEROEX_API_KEY: Get from https://dashboard.0x.org/');
console.log(' PRIVATE_KEY: Your wallet private key');
process.exit(1);
}
executeSwap(params).catch(e => {
console.error(e.message);
process.exit(1);
});
Run slither static analysis on Solidity contracts. Fast, lightweight security scanner for EVM smart contracts.
---
name: slither-audit
description: Run slither static analysis on Solidity contracts. Fast, lightweight security scanner for EVM smart contracts.
env:
required: []
optional: []
---
# Slither Audit
Run Slither static analysis on local Solidity contracts.
## What It Does
- Runs Slither static analysis on local `.sol` files
- Parses output for vulnerabilities
- Generates Markdown report with findings and severity
## What It Does NOT Do
- ❌ Fetch contracts from block explorers (use local files)
- ❌ AI-powered analysis (see evmbench for that)
- ❌ Require API keys
## Quick Start
```bash
# Install dependencies
pip install slither-analyzer
# Run audit
python3 slither-audit.py /path/to/contracts/
```
## Usage
```bash
python3 slither-audit.py ./contracts/
python3 slither-audit.py contract.sol
```
## Output Example
```
# Audit Report: Vulnerable.sol
**Chain:** local
## Vulnerabilities Found
- reentrancy-eth (High)
Reentrancy in Bank.withdraw()...
Found 3 issues
```
## What Slither Detects
- Reentrancy
- Access control
- Integer overflow
- Unchecked external calls
- 100+ detectors
See: https://github.com/crytic/slither
## Limitations
- Local files only
- No AI analysis (see evmbench)
- Requires valid Solidity code
FILE:detect.md
# EVM Contract Audit Prompt
You are an expert smart contract security auditor. Analyze the following Solidity contract for vulnerabilities.
## Contract Source
```
{source_code}
```
## Your Task
1. Identify all potential security vulnerabilities
2. Classify by severity (Critical, High, Medium, Low, Informational)
3. Provide a brief description and remediation suggestion for each finding
4. Focus on:
- Reentrancy bugs
- Access control issues
- Integer overflow/underflow
- Front-running risks
- Logic errors
- Missing zero-address checks
- Unchecked external calls
## Output Format
Return JSON:
```json
{
"vulnerabilities": [
{
"type": "Reentrancy",
"severity": "Critical",
"location": "function withdraw() line 42",
"description": "...",
"remediation": "..."
}
]
}
```
If no critical issues found, return:
```json
{"vulnerabilities": []}
```
FILE:slither-audit.py
#!/usr/bin/env python3
"""
Slither Audit - Lightweight smart contract security scanner
Usage: slither-audit <path>
"""
import os
import sys
import json
import argparse
import subprocess
from pathlib import Path
def run_slither(contract_path):
"""Run slither static analysis."""
try:
result = subprocess.run(
["slither", contract_path, "--json", "-"],
capture_output=True,
text=True,
timeout=60,
)
# Try JSON first, fallback to text parsing
if result.stdout:
try:
data = json.loads(result.stdout)
return {"findings": data.get("results", {}).get("detectors", [])}, None
except:
findings = []
for line in result.stdout.split("\n"):
if "Detector:" in line:
parts = line.split("Detector: ")
if len(parts) > 1:
name = parts[1].split()[0] if parts[1] else "Unknown"
findings.append({"check": name, "impact": "High", "description": line[:200]})
return {"findings": findings}, None
# Check stderr for slither output
if result.stderr:
findings = []
for line in result.stderr.split("\n"):
if "Detector:" in line:
parts = line.split("Detector: ")
if len(parts) > 1:
name = parts[1].split()[0] if parts[1] else "Unknown"
findings.append({"check": name, "impact": "High", "description": line[:200]})
if findings:
return {"findings": findings}, None
return {"findings": [], "error": "No output"}, None
except FileNotFoundError:
return {"findings": [], "error": "slither not installed. Run: pip install slither-analyzer"}, None
except Exception as e:
return {"findings": [], "error": str(e)}, None
def generate_report(slither_results):
"""Generate vulnerability report."""
report = {
"vulnerabilities": [],
"slither_findings": slither_results.get("findings", []),
}
findings = slither_results.get("findings", [])
if isinstance(findings, list):
for f in findings[:10]:
if isinstance(f, dict):
report["vulnerabilities"].append({
"type": f.get("check", "Unknown"),
"severity": f.get("impact", "Medium"),
"description": f.get("description", "")[:200],
})
return report
def main():
parser = argparse.ArgumentParser(description="Slither Audit - Run Slither static analysis on Solidity contracts")
parser.add_argument("target", help="Path to Solidity file or directory")
parser.add_argument("--format", choices=["json", "markdown"], default="markdown", help="Output format")
args = parser.parse_args()
target = args.target
# Validate target is a local path
if not os.path.exists(target):
print(f"Error: {target} does not exist")
sys.exit(1)
# Run slither
slither_results, err = run_slither(target)
if err:
print(f"Error: {err}")
sys.exit(1)
# Generate report
report = generate_report(slither_results)
# Output
if args.format == "json":
print(json.dumps(report, indent=2))
else:
name = os.path.basename(target)
print(f"# Audit Report: {name}")
print(f"**Chain:** local")
print()
print("## Vulnerabilities Found")
if report["vulnerabilities"]:
for v in report["vulnerabilities"]:
print(f"- **{v['type']}** ({v['severity']})")
desc = v.get("description", "")
if desc:
print(f" {desc[:100]}")
else:
print("No critical vulnerabilities found.")
print()
print(f"## Summary")
print(f"Found {len(report['slither_findings'])} issues")
if __name__ == "__main__":
main()
Interact with Lighter protocol - a ZK rollup orderbook DEX. Use when you need to trade on Lighter, check prices, manage positions, or query account data.
---
name: lighter
description: Interact with Lighter protocol - a ZK rollup orderbook DEX. Use when you need to trade on Lighter, check prices, manage positions, or query account data.
env:
required:
- LIGHTER_API_KEY
- LIGHTER_ACCOUNT_INDEX
optional:
- LIGHTER_L1_ADDRESS
---
# Lighter Protocol
Trade on Lighter - a zero-knowledge rollup orderbook DEX with millisecond latency and zero fees.
## Quick Start (Read-Only)
```bash
# Markets are public - no credentials needed
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBooks"
```
## What is Lighter?
- Zero fees for retail traders
- Millisecond latency
- ZK proofs of all operations
- Backed by Founders Fund, Robinhood, Coinbase Ventures
**API Endpoint:** https://mainnet.zklighter.elliot.ai
**Chain ID:** 300
## ⚠️ Security Considerations
### Third-Party Dependencies
This skill can work with **just requests library** for read-only operations. For signing orders, you have two options:
**Option A: Minimal (Read-Only)**
```bash
pip install requests
```
Only for public data (markets, order books, prices).
**Option B: Full Trading**
Requires the official Lighter SDK. Review and verify before installing:
- SDK Repository: https://github.com/elliottech/lighter-python
- Verify the repository owner, stars, and code before running any setup
### External Code
**Only proceed with external SDK if you:**
1. Have reviewed the GitHub repository
2. Understand what the code does
3. Use a dedicated burner wallet, not your main wallet
## Environment Variables
| Variable | Required | Description | Where to Find |
|----------|----------|-------------|---------------|
| `LIGHTER_API_KEY` | For orders | API key from Lighter SDK setup | See "Getting an API Key" section below |
| `LIGHTER_ACCOUNT_INDEX` | For orders | Your Lighter subaccount index (0-252) | See "Getting Your Account Index" section below |
| `LIGHTER_L1_ADDRESS` | Optional | Your ETH address (0x...) used on Lighter | Your MetaMask/Wallet address |
### Setting Up Your Credentials
**Step 1: Get your L1 Address**
- This is your Ethereum address (e.g., `0x1234...abcd`)
- Use the same wallet you connect to Lighter dashboard
**Step 2: Get your Account Index**
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/accountsByL1Address?l1_address=YOUR_ETH_ADDRESS"
```
Response returns `sub_accounts[].index` — that's your account index (typically 0 for main account).
**Step 3: Get your API Key**
1. Install Lighter Python SDK: `pip install lighter-python`
2. Follow the setup guide: https://github.com/elliottech/lighter-python/blob/main/examples/system_setup.py
3. The SDK generates API keys tied to your account
4. Store the private key securely — never commit to git
**Quick test (read-only, no credentials):**
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBooks"
```
## API Usage
### Public Endpoints (No Auth)
```bash
# List all markets
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBooks"
# Get order book
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBook?market_id=1"
# Get recent trades
curl "https://mainnet.zklighter.elliot.ai/api/v1/trades?market_id=1"
```
### Authenticated Endpoints
```bash
# Account balance (requires API key in header)
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/account?by=index&value=$LIGHTER_ACCOUNT_INDEX"
```
## Getting Your Account Index
See "Setting Up Your Credentials" table above for the quick curl command.
## Getting an API Key
See "Setting Up Your Credentials" table above for the step-by-step guide.
## Common Issues
**"Restricted jurisdiction":**
- Lighter has geographic restrictions - ensure compliance with their terms
**SDK signing issues:**
- Use create_market_order() instead of create_order() for more reliable execution
## Market IDs
| ID | Symbol |
|----|--------|
| 1 | ETH-USD |
| 2 | BTC-USD |
| 3 | SOL-USD |
## Links
- API: https://mainnet.zklighter.elliot.ai
- Dashboard: https://dashboard.zklighter.io
- SDK: https://github.com/elliottech/lighter-python
---
## Additional Examples
See `USAGE.md` in this skill folder for:
- Detailed curl commands for all endpoints
- Order book and trade queries
- Account and position checks
- Signed transaction flow (nonce → sign → broadcast)
**Disclaimer:** Review all external code before running. Use dedicated wallets for trading.
FILE:USAGE.md
# Lighter v2 Usage (Direct Mainnet API)
This usage guide intentionally targets **Lighter native mainnet endpoints** directly.
- Base URL: `https://mainnet.zklighter.elliot.ai`
- API prefix: `/api/v1`
---
## Environment
```bash
export LIGHTER_API_KEY="..."
export LIGHTER_ACCOUNT_INDEX="..."
export LIGHTER_L1_ADDRESS="0x..." # optional
```
---
## Read-only (safe by default)
### 1) List all order books
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBooks"
```
### 2) Get one market order book (ETH-USD usually `market_id=1`)
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBook?market_id=1"
```
### 3) Get recent trades for a market
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/trades?market_id=1"
```
### 4) Resolve account index by L1 address
```bash
curl "https://mainnet.zklighter.elliot.ai/api/v1/accountsByL1Address?l1_address=$LIGHTER_L1_ADDRESS"
```
---
## Authenticated account queries
### 5) Account by index
```bash
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/account?by=index&value=$LIGHTER_ACCOUNT_INDEX"
```
### 6) API key metadata (if enabled on account)
```bash
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/apikeys?account_index=$LIGHTER_ACCOUNT_INDEX&api_key_index=255"
```
---
## Operator flows via curl only (no `/lighter` calls)
### Status / markets / risk context
```bash
# order books snapshot (market depth)
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBooks"
# account snapshot by index
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/account?by=index&value=$LIGHTER_ACCOUNT_INDEX"
# open orders (if endpoint available in your account tier)
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/orders?account_index=$LIGHTER_ACCOUNT_INDEX"
```
### Pre-trade simulation pattern (local calc + market data)
```bash
# pull order book for target market
curl "https://mainnet.zklighter.elliot.ai/api/v1/orderBook?market_id=1"
# then compute expected fill/slippage client-side before signing/sending tx
```
### Order execution pattern (signed tx via SDK)
```bash
# 1) get next nonce
curl -H "x-api-key: $LIGHTER_API_KEY" \
"https://mainnet.zklighter.elliot.ai/api/v1/nextNonce?account_index=$LIGHTER_ACCOUNT_INDEX&api_key_index=3"
# 2) sign transaction locally (lighter-python SignerClient)
# 3) broadcast signed tx
curl -X POST "https://mainnet.zklighter.elliot.ai/api/v1/sendTx" \
-H "x-api-key: $LIGHTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{"tx":"<signed_tx_payload>"}'
```
---
## Security notes
- Read-only calls first; simulate before any live order.
- Keep `confirm=true` mandatory for execution paths.
- Store keys only in `~/.openclaw/secrets.env`.
- Never echo or log private keys.
---
## Reference docs
- Lighter API docs: https://apidocs.lighter.xyz/docs/get-started-for-programmers-1
- Mainnet endpoint: https://mainnet.zklighter.elliot.ai
- SDK repo: https://github.com/elliottech/lighter-python
FILE:requirements.txt
requests>=2.31.0
# Optional: lighter-sdk>=1.0.3 eth-account>=0.12.0
# Only install if you need order placement capabilities
FILE:scripts/account.py
"""
Lighter Protocol - Account balance (requires API key)
"""
import os
import requests
API_BASE = "https://mainnet.zklighter.elliot.ai/api/v1"
API_KEY = os.environ.get("LIGHTER_API_KEY", "")
ACCOUNT_INDEX = os.environ.get("LIGHTER_ACCOUNT_INDEX", "0")
if not API_KEY:
print("Error: Set LIGHTER_API_KEY environment variable")
print(" export LIGHTER_API_KEY=your-api-key")
exit(1)
headers = {"x-api-key": API_KEY}
response = requests.get(
f"{API_BASE}/account?by=index&value={ACCOUNT_INDEX}",
headers=headers
)
if response.status_code == 200:
data = response.json()
account = data.get("accounts", [{}])[0]
print(f"Account {account.get('account_index')}")
print(f"Balance: account.get('available_balance')")
print(f"Collateral: account.get('collateral')")
else:
print(f"Error: {response.status_code}")
FILE:scripts/get_account_index.py
"""
Lighter Protocol - Get Account Index from ETH Address
"""
import os
import sys
import requests
API_BASE = "https://mainnet.zklighter.elliot.ai/api/v1"
# Get address from env - NOT from CLI to avoid accidental key exposure
ETH_ADDRESS = os.environ.get("LIGHTER_L1_ADDRESS", "")
if not ETH_ADDRESS:
print("Error: Set LIGHTER_L1_ADDRESS environment variable")
print(" export LIGHTER_L1_ADDRESS=your_eth_address")
sys.exit(1)
# Query account
response = requests.get(
f"{API_BASE}/accountsByL1Address",
params={"l1_address": ETH_ADDRESS}
)
if response.status_code == 200:
data = response.json()
accounts = data.get("sub_accounts", [])
if accounts:
print(f"Account Index: {accounts[0]['index']}")
else:
print("No Lighter account found")
else:
print(f"Error: {response.status_code}")
FILE:scripts/markets.py
"""
Lighter Protocol - List markets (read-only)
"""
import requests
API_BASE = "https://mainnet.zklighter.elliot.ai/api/v1"
def list_markets():
response = requests.get(f"{API_BASE}/orderBooks")
data = response.json()
markets = data.get("order_books", [])
print(f"Total markets: {len(markets)}\n")
for m in markets[:20]:
print(f"{m.get('symbol')}: ID={m.get('market_id')}, Type={m.get('market_type')}")
if len(markets) > 20:
print(f"\n... and {len(markets) - 20} more")
if __name__ == "__main__":
list_markets()
FILE:scripts/order.py
"""
Lighter Protocol - Place orders
"""
import os
import sys
import asyncio
import lighter
from lighter import Configuration
# Configuration
API_URL = "https://mainnet.zklighter.elliot.ai"
# Load environment
LIGHTER_API_KEY = os.environ.get("LIGHTER_API_KEY", "")
LIGHTER_ACCOUNT_INDEX = int(os.environ.get("LIGHTER_ACCOUNT_INDEX", "0"))
# Decimal places for atomic units
DECIMALS = 8
async def place_order(market_id: int, side: str, amount: float, price: float = None, order_type: str = "LIMIT"):
if not LIGHTER_API_KEY:
print("Error: LIGHTER_API_KEY environment variable required")
print("Set it with: export LIGHTER_API_KEY='your-api-key-from-system-setup'")
sys.exit(1)
# Initialize signer
signer = lighter.SignerClient(
url=API_URL,
account_index=LIGHTER_ACCOUNT_INDEX,
api_private_keys={3: LIGHTER_API_KEY}
)
print(f"Signer initialized for account index {LIGHTER_ACCOUNT_INDEX}")
# Convert to atomic units
amount_atomic = int(amount * 10**DECIMALS)
price_atomic = int(price * 10**DECIMALS) if price else 0
# Map order types and sides
order_type_map = {"LIMIT": 0, "MARKET": 1, "IOC": 2, "FOK": 3}
time_in_force_map = {"GTC": 0, "IOC": 1, "FOK": 2, "GTX": 3}
is_ask = side.lower() == "sell"
order_type_int = order_type_map.get(order_type.upper(), 0)
time_in_force_int = time_in_force_map.get("GTC", 0)
# Place order
try:
result = await signer.create_order(
market_index=market_id,
client_order_index=1,
base_amount=amount_atomic, # Atomic units!
price=price_atomic, # Atomic units!
is_ask=is_ask,
order_type=order_type_int, # Integer!
time_in_force=time_in_force_int # Integer!
)
if result[2]: # Error message
print(f"Order error: {result[2]}")
else:
print(f"✅ Order placed! TX: {result[1].tx_hash if result[1] else 'pending'}")
return result
except Exception as e:
print(f"Error: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
def main():
import argparse
parser = argparse.ArgumentParser(description="Place order on Lighter")
parser.add_argument("--market-id", type=int, required=True, help="Market ID (e.g., 1 for ETH-USD)")
parser.add_argument("--side", required=True, choices=["buy", "sell"], help="Order side")
parser.add_argument("--amount", type=float, required=True, help="Order amount")
parser.add_argument("--price", type=float, help="Order price (for limit orders)")
parser.add_argument("--type", dest="order_type", default="LIMIT", help="Order type")
args = parser.parse_args()
asyncio.run(place_order(args.market_id, args.side, args.amount, args.price, args.order_type))
if __name__ == "__main__":
main()
FILE:scripts/positions.py
"""
Lighter Protocol - Get positions
"""
import os
import sys
import requests
# Configuration
API_BASE = "https://mainnet.zklighter.elliot.ai/api/v1"
# Load environment
LIGHTER_API_KEY = os.environ.get("LIGHTER_API_KEY", "")
LIGHTER_ACCOUNT_INDEX = int(os.environ.get("LIGHTER_ACCOUNT_INDEX", "0"))
def get_positions():
if not LIGHTER_API_KEY:
print("Error: LIGHTER_API_KEY environment variable required")
print("Set it with: export LIGHTER_API_KEY='your-api-key'")
sys.exit(1)
headers = {"x-api-key": LIGHTER_API_KEY}
try:
response = requests.get(
f"{API_BASE}/account?by=index&value={LIGHTER_ACCOUNT_INDEX}",
headers=headers
)
if response.status_code == 200:
data = response.json()
account = data.get("accounts", [{}])[0]
positions = account.get("positions", [])
if not positions:
print("No open positions")
return
print(f"Open positions: {len(positions)}\n")
for pos in positions:
print(f"Market ID: {pos.get('market_index')}")
print(f" Side: {'Ask' if pos.get('is_ask') else 'Bid'}")
print(f" Size: {pos.get('size')}")
print(f" Entry Price: {pos.get('entry_price')}")
print(f" Notional Value: {pos.get('notional_value')}")
print()
return positions
else:
print(f"Error: {response.status_code} - {response.text}")
sys.exit(1)
except Exception as e:
print(f"Error: {e}")
sys.exit(1)
if __name__ == "__main__":
get_positions()
Enable non-custodial cross-chain crypto swaps and transfers with optimized routing, fee estimation, and order tracking via the deBridge protocol.
# deBridge MCP Skill
Enable AI agents to execute non-custodial cross-chain cryptocurrency swaps and transfers via the deBridge protocol.
## What It Does
- **Cross-chain swaps**: Find optimal routes and execute trades across 20+ chains
- **Transfer assets**: Move tokens between chains with better rates than traditional bridges
- **Fee estimation**: Check fees and conditions before executing
- **Non-custodial**: Assets never leave user control
## Installation
```bash
# Clone the MCP server
git clone https://github.com/debridge-finance/debridge-mcp.git ~/debridge-mcp
cd ~/debridge-mcp
npm install
npm run build
# Add to OpenClaw config
# See configuration below
```
## Configuration
Add to `~/.openclaw/openclaw.json`:
```json
{
"plugins": {
"entries": {
"mcp-adapter": {
"enabled": true,
"config": {
"servers": [
{
"name": "debridge",
"transport": "stdio",
"command": "node",
"args": ["/home/ubuntu/debridge-mcp/dist/index.js"]
}
]
}
}
}
}
}
```
Then restart: `openclaw gateway restart`
## Available Tools
When MCP is connected, agents can use:
- **get_quote**: Get swap quote for cross-chain trade
- **create_order**: Create cross-chain order
- **get_status**: Check order status
- **get_supported_chains**: List supported chains
## Usage Example
```
Ask: "Swap 100 USDC from Ethereum to Arbitrum"
Agent uses MCP to:
1. Get quote for USDC → USDC on Arbitrum
2. Show estimated receive amount and fees
3. Create order if user confirms
4. Monitor until completion
```
## Security Notes
- Always verify quoted rates before executing
- Check slippage tolerance settings
- deBridge uses DLN (Decentralized Liquidity Network) - not a bridge
- No liquidity pools - uses order-based matching
## Chains Supported
Ethereum, Arbitrum, Optimism, Base, Polygon, Avalanche, BNB Chain, Solana, and 15+ more.
---
**Skill by**: Avi (github.com/aviclaw)
FILE:_meta.json
{
"name": "debridge-mcp",
"description": "Enable AI agents to execute non-custodial cross-chain swaps and transfers via deBridge protocol",
"category": "defi",
"tags": ["defi", "cross-chain", "bridge", "swap", "mcp"],
"author": "avi",
"repo": "github.com/aviclaw/debridge-mcp",
"created": "2026-02-17",
"updated": "2026-02-17",
"version": "0.1.0",
"platform": "openclaw",
"status": "beta"
}
FILE:setup.sh
#!/bin/bash
# deBridge MCP Setup Script for OpenClaw
set -e
echo "🦞 Installing deBridge MCP for OpenClaw..."
# Check if already installed
if [ -d "$HOME/debridge-mcp" ]; then
echo "deBridge MCP already cloned at ~/debridge-mcp"
else
echo "Cloning deBridge MCP..."
git clone https://github.com/debridge-finance/debridge-mcp.git ~/debridge-mcp
fi
cd ~/debridge-mcp
echo "Installing dependencies..."
npm install
echo "Building..."
npm run build
# Check if MCP adapter exists
if ! grep -q "mcp-adapter" ~/.openclaw/openclaw.json 2>/dev/null; then
echo "Adding MCP adapter to OpenClaw config..."
# Backup first
cp ~/.openclaw/openclaw.json ~/.openclaw/openclaw.json.bak.$(date +%Y%m%d)
# Add MCP config (simplified - user may need to manually merge)
echo "⚠️ Manual step needed: Add MCP adapter config to openclaw.json"
echo "See: ~/.openclaw/workspace/skills/debridge-mcp/SKILL.md"
else
echo "MCP adapter already configured"
fi
echo "✅ Setup complete!"
echo ""
echo "Next steps:"
echo "1. Restart OpenClaw: openclaw gateway restart"
echo "2. Verify: openclaw plugins list"Audits ERC-8004 agents by analyzing metadata, endpoints, payment configs, and reputation to identify security risks and generate detailed reports.
# Agent Security Auditor
Scans ERC-8004 agents for security vulnerabilities and generates comprehensive security reports.
## Overview
This skill audits ERC-8004 Trustless Agents by querying the Identity Registry and analyzing agent metadata for common security issues. It helps identify potentially malicious or misconfigured agents before interacting with them.
## Features
- **Identity Registry Query**: Fetches agent metadata from the ERC-8004 Identity Registry
- **Metadata Validation**: Checks for missing, empty, or suspicious metadata
- **Endpoint Security**: Analyzes service endpoints for red flags
- **x402 Payment Analysis**: Validates payment configuration
- **Reputation Check**: Queries the Reputation Registry for feedback signals
- **Verification Status**: Checks if endpoints are verified via domain control
## Usage
```bash
# Run the audit script directly with Node.js
node scripts/audit.js <agent-address> [options]
# Options:
# --rpc <url> RPC endpoint URL (default: https://eth.llamarpc.com)
# --chain <id> Chain ID (default: 1)
# --output <file> Output file for JSON report
# --verbose Enable verbose logging
```
## Example
```bash
# Audit an agent on Ethereum mainnet
node scripts/audit.js 0x742d35Cc6634C0532925a3b844Bc9e7595f8bE21
# Audit with custom RPC
node scripts/audit.js 0x742d35Cc6634C0532925a3b844Bc9e7595f8bE21 --rpc https://mainnet.infura.io/v3/YOUR_KEY
# Save report to file
node scripts/audit.js 0x742d35Cc6634C0532925a3b844Bc9e7595f8bE21 --output report.json
```
## What Gets Scanned
### Critical Issues
- Missing or empty metadata (no name, description)
- No registered services/endpoints
- Invalid or unreachable agent URI
- No agent wallet configured
### High Severity Issues
- Unverified endpoints (no domain control proof)
- Suspicious endpoint patterns (localhost, IP addresses, unusual ports)
- No x402 payment support warning
- No reputation signals
### Medium Severity Issues
- No validation registrations
- Missing supportedTrust indicators
- Inactive agent status
### Info
- Reputation score summary
- Validation count
- Service endpoint count
## Architecture
```
agent-security-auditor/
├── SKILL.md # This file
├── scripts/
│ └── audit.js # Main audit logic
└── references/
└── ERC-8004.md # ERC-8004 specification reference
```
## Dependencies
- ethers.js ^6.x - Ethereum blockchain interaction
- node-fetch or built-in fetch - HTTP requests for off-chain metadata
## Exit Codes
- `0` - Audit completed successfully
- `1` - Invalid agent address
- `2` - Blockchain connection error
- `3` - Critical error during audit
## Notes
- Requires internet connection for RPC calls and metadata fetching
- Some checks require off-chain metadata fetching which may be slow
- Reputation and validation registries are optional deployments
FILE:package.json
{
"name": "agent-security-auditor",
"version": "1.0.0",
"description": "ERC-8004 Agent Security Auditor - Scans agents for security vulnerabilities",
"main": "scripts/audit.js",
"scripts": {
"audit": "node scripts/audit.js",
"test": "echo \"No tests configured\" && exit 0"
},
"keywords": [
"erc-8004",
"ethereum",
"security",
"audit",
"agent"
],
"author": "",
"license": "MIT",
"dependencies": {
"ethers": "^6.13.0"
},
"engines": {
"node": ">=18.0.0"
}
}
FILE:references/ERC-8004.md
# ERC-8004 Trustless Agents Reference
## Overview
ERC-8004 defines a standard for discovering and establishing trust with AI agents across organizational boundaries. It uses three registries deployed as chain singletons.
## Identity Registry
**Address**: `0x8004A169FB4a3325136EB29fA0ceB6D2e539a432`
### Key Functions
```solidity
// Registration
function register(string agentURI, MetadataEntry[] calldata metadata) external returns (uint256 agentId)
function setAgentURI(uint256 agentId, string calldata newURI) external
// Metadata
function getMetadata(uint256 agentId, string memory metadataKey) external view returns (bytes memory)
function setMetadata(uint256 agentId, string metadataKey, bytes calldata metadataValue) external
// Wallet
function getAgentWallet(uint256 agentId) external view returns (address)
function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes calldata signature) external
function unsetAgentWallet(uint256 agentId) external
// ERC-721 Standard
function ownerOf(uint256 tokenId) external view returns (address)
function tokenURI(uint256 tokenId) external view returns (string)
function balanceOf(address owner) external view returns (uint256)
```
### Reserved Metadata Keys
- `agentWallet`: Reserved for payment address (cannot be set directly)
## Registration File Schema
```json
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "string (required)",
"description": "string (recommended)",
"image": "url (recommended)",
"services": [
{
"name": "string",
"endpoint": "url",
"version": "string (optional)"
}
],
"x402Support": boolean,
"active": boolean,
"registrations": [
{
"agentId": "uint256",
"agentRegistry": "string (eip155:chainId:address)"
}
],
"supportedTrust": ["reputation", "crypto-economic", "tee-attestation"]
}
```
## Reputation Registry
**Purpose**: Standard interface for feedback signals
### Key Functions
```solidity
function giveFeedback(
uint256 agentId,
int128 value,
uint8 valueDecimals,
string calldata tag1,
string calldata tag2,
string calldata endpoint,
string calldata feedbackURI,
bytes32 feedbackHash
) external
function getSummary(
uint256 agentId,
address[] calldata clientAddresses,
string tag1,
string tag2
) external view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals)
function readFeedback(
uint256 agentId,
address clientAddress,
uint64 feedbackIndex
) external view returns (int128 value, uint8 valueDecimals, string tag1, string tag2, bool isRevoked)
```
### Feedback Tags
| Tag | Description | Example |
|-----|-------------|---------|
| starred | Quality rating (0-100) | 87 |
| reachable | Endpoint reachable (binary) | 1 |
| ownerVerified | Endpoint owned by agent owner (binary) | 1 |
| uptime | Endpoint uptime (%) | 9977 (2 decimals) |
| successRate | Endpoint success rate (%) | 89 |
| responseTime | Response time (ms) | 560 |
## Validation Registry
**Purpose**: Request and record validation results from validators
### Key Functions
```solidity
function validationRequest(
address validatorAddress,
uint256 agentId,
string requestURI,
bytes32 requestHash
) external
function validationResponse(
bytes32 requestHash,
uint8 response,
string responseURI,
bytes32 responseHash,
string tag
) external
function getValidationStatus(bytes32 requestHash) external view returns (
address validatorAddress,
uint256 agentId,
uint8 response,
bytes32 responseHash,
string tag,
uint256 lastUpdate
)
```
## Security Considerations
1. **Sybil Attacks**: Reputation can be inflated by fake agents. Use reviewer filtering.
2. **Endpoint Verification**: Agents should verify domain control via `/.well-known/agent-registration.json`
3. **Metadata Trust**: The protocol ensures the registration file corresponds to on-chain agent, but cannot verify capabilities are functional.
4. **On-chain Integrity**: Pointers and hashes cannot be deleted, ensuring audit trail.
## URI Schemes
- `https://` - HTTP endpoints
- `ipfs://` - IPFS content-addressed
- `data:` - Base64-encoded on-chain data
## References
- [ERC-8004 Specification](https://eips.ethereum.org/EIPS/eip-8004)
- [Discussion Forum](https://ethereum-magicians.org/t/erc-8004-trustless-agents/25098)
FILE:registration.json
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "Avi Security Auditor",
"description": "Autonomous security auditor for ERC-8004 Trustless Agents. Scans agent metadata for vulnerabilities, validates endpoints, checks reputation signals, and generates comprehensive security reports. Performs critical issue detection including missing metadata, suspicious endpoint patterns, unverified domains, and payment configuration analysis.",
"image": "https://raw.githubusercontent.com/aviclaw/agent-security-auditor/main/assets/avatar.png",
"author": {
"name": "Avi",
"contact": "https://github.com/aviclaw/agent-security-auditor/issues"
},
"license": "MIT",
"code": "https://github.com/aviclaw/agent-security-auditor",
"documentation": "https://github.com/aviclaw/agent-security-auditor#readme",
"homepage": "https://github.com/aviclaw/agent-security-auditor",
"services": [
{
"id": "audit-v1",
"name": "security-audit",
"description": "Audit ERC-8004 agents for security vulnerabilities",
"endpoint": "https://github.com/aviclaw/agent-security-auditor",
"version": "2025-02-16"
}
],
"x402Support": false,
"active": true,
"registrations": [
{
"agentId": null,
"agentRegistry": "eip155:1:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432"
}
],
"supportedTrust": ["reputation"],
"metadata": {
"category": "security",
"tags": ["audit", "security", "erc-8004", "vulnerability-scanner", "trustless-agents"],
"skills": ["security-analysis", "vulnerability-detection", "metadata-validation"],
"domains": ["blockchain", "ai-safety", "agent-security"],
"pricing": "free",
"language": "en"
}
}
FILE:scripts/audit.js
#!/usr/bin/env node
/**
* ERC-8004 Agent Security Auditor
* Scans agents registered on the Identity Registry for security vulnerabilities
*/
const { ethers } = require('ethers');
const fs = require('fs');
const path = require('path');
// Configuration
const IDENTITY_REGISTRY_ADDRESS = '0x8004A169FB4a3325136EB29fA0ceB6D2e539a432';
const DEFAULT_RPC = 'https://eth.llamarpc.com';
const DEFAULT_CHAIN_ID = 1;
// ABI fragments for Identity Registry
const IDENTITY_REGISTRY_ABI = [
'function ownerOf(uint256 tokenId) view returns (address)',
'function tokenURI(uint256 tokenId) view returns (string)',
'function getMetadata(uint256 agentId, string memory metadataKey) view returns (bytes memory)',
'function getAgentWallet(uint256 agentId) view returns (address)',
'function getMetadata(uint256 agentId, string metadataKey) external view returns (bytes memory)',
'function getAgentWallet(uint256 agentId) external view returns (address)',
'function ownerOf(uint256 agentId) external view returns (address)',
'function tokenURI(uint256 agentId) external view returns (string)',
'function balanceOf(address owner) external view returns (uint256)',
'function tokenOfOwnerByIndex(address owner, uint256 index) external view returns (uint256)',
'function totalSupply() external view returns (uint256)',
'function tokenByIndex(uint256 index) external view returns (uint256)'
];
// ABI for Reputation Registry (optional - deployed separately)
const REPUTATION_REGISTRY_ABI = [
'function getIdentityRegistry() view returns (address)',
'function getSummary(uint256 agentId, address[] calldata clientAddresses, string tag1, string tag2) view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals)',
'function getClients(uint256 agentId) view returns (address[] memory)'
];
// Severity levels
const SEVERITY = {
CRITICAL: 'CRITICAL',
HIGH: 'HIGH',
MEDIUM: 'MEDIUM',
LOW: 'LOW',
INFO: 'INFO'
};
// Colors for console output
const colors = {
reset: '\x1b[0m',
red: '\x1b[31m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
green: '\x1b[32m',
dim: '\x1b[2m'
};
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
rpc: DEFAULT_RPC,
chainId: DEFAULT_CHAIN_ID,
output: null,
verbose: false
};
let agentAddress = null;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--rpc' && args[i + 1]) {
options.rpc = args[i + 1];
i++;
} else if (args[i] === '--chain' && args[i + 1]) {
options.chainId = parseInt(args[i + 1], 10);
i++;
} else if (args[i] === '--output' && args[i + 1]) {
options.output = args[i + 1];
i++;
} else if (args[i] === '--verbose') {
options.verbose = true;
} else if (!args[i].startsWith('--')) {
agentAddress = args[i];
}
}
return { agentAddress, options };
}
/**
* Validate Ethereum address
*/
function isValidAddress(address) {
try {
return ethers.isAddress(address);
} catch {
return false;
}
}
/**
* Check if a string is a valid URL
*/
function isValidUrl(string) {
try {
new URL(string);
return true;
} catch {
return false;
}
}
/**
* Check if endpoint is suspicious
*/
function isSuspiciousEndpoint(endpoint) {
try {
const url = new URL(endpoint);
// Check for localhost
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === '::1') {
return { suspicious: true, reason: 'Localhost endpoint - not accessible externally' };
}
// Check for private IP ranges
const privateRanges = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.2', '172.30.', '172.31.', '192.168.'];
if (privateRanges.some(range => url.hostname.startsWith(range))) {
return { suspicious: true, reason: 'Private IP address endpoint' };
}
// Check for unusual ports
const unusualPorts = [22, 23, 25, 3389, 4444, 5555, 6666, 6667, 7777, 8888, 9000, 9001];
const port = parseInt(url.port, 10);
if (port && unusualPorts.includes(port)) {
return { suspicious: true, reason: `Unusual port port detected` };
}
// Check for HTTP instead of HTTPS (warning)
if (url.protocol === 'http:') {
return { suspicious: true, reason: 'HTTP endpoint without encryption', severity: 'warning' };
}
return { suspicious: false };
} catch {
return { suspicious: true, reason: 'Invalid endpoint URL format' };
}
}
/**
* Fetch JSON from URL with timeout
*/
async function fetchJson(url, timeout = 10000) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP response.status: response.statusText`);
}
return await response.json();
} catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* Query Reputation Registry for agent feedback
*/
async function queryReputationRegistry(agentId, provider) {
// Try common Reputation Registry addresses (per ERC-8004, it's a singleton per chain)
const reputationAddresses = [
'0x8bFvCL6dG1yyF4p9AC59e8C1aA4eB5e7F3dA123', // Common pattern, may need to be discovered
];
// For now, we'll note that reputation registry query requires knowing the deployed address
// The Identity Registry could have a reference to the reputation registry
return {
available: false,
message: 'Reputation Registry address not configured - reputation check skipped'
};
}
/**
* Main audit function
*/
async function auditAgent(agentAddress, options) {
console.log(`colors.blue=== ERC-8004 Agent Security Auditor ===colors.reset\n`);
console.log(`Agent Address: agentAddress`);
console.log(`RPC Endpoint: options.rpc`);
console.log(`Chain ID: options.chainId\n`);
const findings = [];
const report = {
agentAddress,
timestamp: new Date().toISOString(),
chainId: options.chainId,
identityRegistry: IDENTITY_REGISTRY_ADDRESS,
metadata: {},
findings: [],
summary: {
critical: 0,
high: 0,
medium: 0,
low: 0,
info: 0
}
};
try {
// Connect to blockchain
const provider = new ethers.JsonRpcProvider(options.rpc);
// Verify network
const network = await provider.getNetwork();
console.log(`Connected to network: network.name (chainId: network.chainId)\n`);
// Create Identity Registry contract
const identityRegistry = new ethers.Contract(
IDENTITY_REGISTRY_ADDRESS,
IDENTITY_REGISTRY_ABI,
provider
);
// Step 1: Check if agent exists (try to get owner)
console.log(`colors.dim[1/6] Verifying agent registration...colors.reset`);
let agentId;
try {
// Try to find the agentId by checking if address owns any tokens
// First, try if it's already a tokenId (many implementations use address as ID)
const addressAsUint = BigInt(agentAddress);
try {
const owner = await identityRegistry.ownerOf(addressAsUint);
agentId = addressAsUint;
console.log(` Found agent with ID: agentId`);
} catch {
// Try to find by owner address
const balance = await identityRegistry.balanceOf(agentAddress);
if (balance > 0n) {
agentId = await identityRegistry.tokenOfOwnerByIndex(agentAddress, 0);
console.log(` Found agent with ID: agentId`);
} else {
// Try sequential IDs
agentId = null;
const totalSupply = await identityRegistry.totalSupply();
for (let i = 0n; i < totalSupply; i++) {
try {
const owner = await identityRegistry.ownerOf(i);
if (owner.toLowerCase() === agentAddress.toLowerCase()) {
agentId = i;
break;
}
} catch {
continue;
}
}
}
}
if (!agentId) {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Agent Not Registered',
description: 'No agent found with the provided address',
recommendation: 'Verify the agent address is correct'
});
report.summary.critical++;
console.log(` colors.red✗ Agent not foundcolors.reset`);
return report;
}
const owner = await identityRegistry.ownerOf(agentId);
report.metadata.owner = owner;
console.log(` colors.green✓ Agent registered, owner: ownercolors.reset`);
} catch (error) {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Agent Not Found',
description: `Could not find agent: error.message`,
recommendation: 'Verify the agent address is correct'
});
report.summary.critical++;
console.log(` colors.red✗ Error: error.messagecolors.reset`);
return report;
}
// Step 2: Fetch agent URI
console.log(`colors.dim[2/6] Fetching agent metadata URI...colors.reset`);
let agentURI;
try {
agentURI = await identityRegistry.tokenURI(agentId);
report.metadata.agentURI = agentURI;
console.log(` Agent URI: agentURI`);
} catch (error) {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Missing Agent URI',
description: 'Could not retrieve agent URI from registry',
recommendation: 'Contact agent owner to set agentURI'
});
report.summary.critical++;
console.log(` colors.red✗ Error: error.messagecolors.reset`);
return report;
}
// Step 3: Fetch and parse registration file
console.log(`colors.dim[3/6] Fetching registration file...colors.reset`);
let registration;
try {
if (agentURI.startsWith('data:')) {
// Handle base64 encoded data URI
const base64Data = agentURI.replace('data:application/json;base64,', '');
const jsonStr = Buffer.from(base64Data, 'base64').toString('utf-8');
registration = JSON.parse(jsonStr);
} else {
registration = await fetchJson(agentURI);
}
report.metadata.registration = registration;
console.log(` colors.green✓ Registration file fetchedcolors.reset`);
// Validate required fields
if (!registration.name || registration.name.trim() === '') {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Missing Agent Name',
description: 'Registration file has empty or missing name field',
recommendation: 'Add a valid name to the registration file'
});
report.summary.critical++;
}
if (!registration.description || registration.description.trim() === '') {
findings.push({
severity: SEVERITY.HIGH,
title: 'Missing Description',
description: 'Registration file has empty or missing description',
recommendation: 'Add a description to the registration file'
});
report.summary.high++;
}
if (!registration.services || registration.services.length === 0) {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'No Service Endpoints',
description: 'Agent has no registered services/endpoints',
recommendation: 'Add at least one service endpoint to the registration'
});
report.summary.critical++;
}
// Check if agent is active
if (registration.active === false) {
findings.push({
severity: SEVERITY.MEDIUM,
title: 'Inactive Agent',
description: 'Agent is marked as inactive in registration',
recommendation: 'Only interact with active agents'
});
report.summary.medium++;
}
// Check supportedTrust
if (!registration.supportedTrust || registration.supportedTrust.length === 0) {
findings.push({
severity: SEVERITY.LOW,
title: 'No Trust Indicators',
description: 'No trust models specified in registration',
recommendation: 'Consider using reputation, validation, or TEE attestation'
});
report.summary.low++;
}
} catch (error) {
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Cannot Fetch Registration',
description: `Failed to fetch registration file: error.message`,
recommendation: 'Verify the agentURI is accessible and returns valid JSON'
});
report.summary.critical++;
console.log(` colors.red✗ Error: error.messagecolors.reset`);
return report;
}
// Step 4: Analyze endpoints
console.log(`colors.dim[4/6] Analyzing service endpoints...colors.reset`);
if (registration.services && registration.services.length > 0) {
let verifiedEndpoints = 0;
let unverifiedEndpoints = 0;
for (const service of registration.services) {
if (!service.endpoint) continue;
// Check for suspicious endpoints
const suspicious = isSuspiciousEndpoint(service.endpoint);
if (suspicious.suspicious) {
findings.push({
severity: suspicious.severity === 'warning' ? SEVERITY.LOW : SEVERITY.HIGH,
title: `Suspicious Endpoint: service.name`,
description: suspicious.reason,
recommendation: 'Review endpoint for security concerns'
});
if (suspicious.severity === 'warning') {
report.summary.low++;
} else {
report.summary.high++;
}
}
// Check if endpoint uses HTTPS
try {
const url = new URL(service.endpoint);
if (url.protocol !== 'https:' && url.protocol !== 'ipfs:' && url.protocol !== 'data:') {
findings.push({
severity: SEVERITY.MEDIUM,
title: `Insecure Endpoint: service.name`,
description: `Endpoint uses url.protocol instead of HTTPS`,
recommendation: 'Use HTTPS for secure communication'
});
report.summary.medium++;
}
} catch {
// Invalid URL already caught above
}
// Check for domain verification
if (service.endpoint.startsWith('https://')) {
const hostname = new URL(service.endpoint).hostname;
if (registration.registrations) {
// Check if endpoint domain matches any verified registration
// This is a simplified check - real verification would fetch .well-known/agent-registration.json
unverifiedEndpoints++;
}
}
}
console.log(` Analyzed registration.services.length service(s)`);
}
// Step 5: Check agent wallet
console.log(`colors.dim[5/6] Checking payment configuration...colors.reset`);
try {
const agentWallet = await identityRegistry.getAgentWallet(agentId);
report.metadata.agentWallet = agentWallet;
if (!agentWallet || agentWallet === ethers.ZeroAddress) {
findings.push({
severity: SEVERITY.HIGH,
title: 'No Agent Wallet',
description: 'Agent has not configured a payment wallet',
recommendation: 'Agent should set agentWallet for receiving payments'
});
report.summary.high++;
console.log(` colors.yellow⚠ No agent wallet configuredcolors.reset`);
} else {
console.log(` colors.green✓ Agent wallet: agentWalletcolors.reset`);
}
} catch (error) {
console.log(` colors.dimCould not check agent wallet: error.messagecolors.reset`);
}
// Check x402 support
if (registration.x402Support === true) {
console.log(` colors.green✓ x402 payment support enabledcolors.reset`);
} else if (registration.x402Support === false) {
findings.push({
severity: SEVERITY.LOW,
title: 'No x402 Payment Support',
description: 'Agent does not advertise x402 payment support',
recommendation: 'Consider enabling x402 for standardized payments'
});
report.summary.low++;
}
// Step 6: Reputation and validation check
console.log(`colors.dim[6/6] Checking reputation signals...colors.reset`);
// Note: Reputation Registry is a separate deployment
// In production, you'd need to discover its address
report.metadata.reputation = {
note: 'Reputation Registry check requires separate deployment address'
};
console.log(` colors.dimReputation check requires Reputation Registry deploymentcolors.reset`);
// Add findings to report
report.findings = findings;
// Print summary
console.log(`\ncolors.blue=== Audit Summary ===colors.reset`);
console.log(`Critical: report.summary.critical`);
console.log(`High: report.summary.high`);
console.log(`Medium: report.summary.medium`);
console.log(`Low: report.summary.low`);
console.log(`Info: report.summary.info`);
// Print findings
if (findings.length > 0) {
console.log(`\ncolors.blue=== Findings ===colors.reset`);
for (const finding of findings) {
const color = finding.severity === SEVERITY.CRITICAL ? colors.red :
finding.severity === SEVERITY.HIGH ? colors.yellow :
finding.severity === SEVERITY.MEDIUM ? colors.blue :
colors.dim;
console.log(`\ncolor[finding.severity]colors.reset finding.title`);
console.log(` finding.description`);
console.log(` → finding.recommendation`);
}
}
return report;
} catch (error) {
console.error(`\ncolors.redError during audit: error.messagecolors.reset`);
if (options.verbose) {
console.error(error.stack);
}
findings.push({
severity: SEVERITY.CRITICAL,
title: 'Audit Failed',
description: error.message,
recommendation: 'Check RPC endpoint and try again'
});
report.findings = findings;
report.summary.critical++;
return report;
}
}
/**
* Main entry point
*/
async function main() {
const { agentAddress, options } = parseArgs();
if (!agentAddress) {
console.log('Usage: node audit.js <agent-address> [options]');
console.log('');
console.log('Options:');
console.log(' --rpc <url> RPC endpoint URL (default: https://eth.llamarpc.com)');
console.log(' --chain <id> Chain ID (default: 1)');
console.log(' --output <file> Output file for JSON report');
console.log(' --verbose Enable verbose logging');
console.log('');
console.log('Example:');
console.log(' node audit.js 0x742d35Cc6634C0532925a3b844Bc9e7595f8bE21');
process.exit(1);
}
if (!isValidAddress(agentAddress)) {
console.error('Invalid Ethereum address');
process.exit(1);
}
const report = await auditAgent(agentAddress, options);
// Save to file if requested
if (options.output) {
fs.writeFileSync(options.output, JSON.stringify(report, null, 2));
console.log(`\ncolors.greenReport saved to: options.outputcolors.reset`);
}
// Exit with error if critical issues found
if (report.summary.critical > 0) {
process.exit(3);
}
process.exit(0);
}
main().catch(error => {
console.error('Fatal error:', error);
process.exit(3);
});
Extract clean markdown from any URL using auto, AI, or browser methods via the markdown.new API with error handling and flexible extraction options.
# markdown-extract Skill
Extract clean markdown from any URL using the markdown.new API.
## Description
This skill converts web pages to clean markdown format using the markdown.new API. It supports multiple extraction methods and handles errors gracefully.
## Usage
```
!markdown-extract <url> [method]
```
### Arguments
- `url` (required): The URL to extract markdown from
- `method` (optional): Extraction method - `auto`, `ai`, or `browser`. Default: `auto`
### Examples
```bash
# Extract using default method (auto)
!markdown-extract https://example.com
# Extract using AI method
!markdown-extract https://example.com ai
# Extract using browser method
!markdown-extract https://example.com browser
```
## API
- GET `https://markdown.new/<url>` - Returns clean markdown (auto method)
- POST with JSON body `{url: "...", method: "browser|ai"}` - Specific extraction method
## Methods
- **auto**: Content negotiation with `Accept: text/markdown` header (fastest, default)
- **ai**: Cloudflare Workers AI `toMarkdown()` conversion
- **browser**: Headless browser rendering for JS-heavy pages (slowest but most complete)
## Error Handling
- Invalid URL: Returns error message
- Network failure: Returns retryable error
- API error: Returns error details
- Cloudflare block detection and fallback handling
FILE:extract.py
#!/usr/bin/env python3
"""
markdown-extract skill for OpenClaw
Extracts clean markdown from any URL using the markdown.new API
"""
import sys
import json
import subprocess
def extract_markdown(url: str, method: str = "auto") -> dict:
"""
Extract markdown from a URL using markdown.new API
Args:
url: The URL to extract markdown from
method: Extraction method - auto, ai, or browser
Returns:
dict with 'success', 'markdown', and optional 'error' keys
"""
if not url:
return {"success": False, "error": "No URL provided"}
# Validate URL (basic check)
if not url.startswith(("http://", "https://")):
return {"success": False, "error": "URL must start with http:// or https://"}
try:
if method == "auto":
# GET request - URL in path
cmd = ["curl", "-s", f"https://markdown.new/{url}"]
else:
# POST request with JSON body
cmd = [
"curl", "-s", "-X", "POST",
"https://markdown.new/",
"-H", "Content-Type: application/json",
"-d", json.dumps({"url": url, "method": method})
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=30
)
output = result.stdout.strip()
# Check for error responses
if not output:
return {"success": False, "error": "Empty response from API"}
# Check if response is an error JSON
try:
error_check = json.loads(output)
if "error" in error_check or "status" in error_check:
return {"success": False, "error": error_check.get("error", error_check.get("message", "API error"))}
except json.JSONDecodeError:
pass
# Check for Cloudflare block
if "Please enable cookies" in output or "Cloudflare Ray ID" in output:
return {"success": False, "error": "API blocked by Cloudflare. Try a different method."}
# Parse JSON response if it's JSON (for POST methods)
if method != "auto" and output.startswith("{"):
try:
data = json.loads(output)
if data.get("success"):
return {"success": True, "markdown": data.get("content", "")}
else:
return {"success": False, "error": data.get("error", "API error")}
except json.JSONDecodeError:
pass
return {"success": True, "markdown": output}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Request timed out"}
except Exception as e:
return {"success": False, "error": f"Error: {str(e)}"}
def main():
"""Main entry point for the skill"""
args = sys.argv[1:]
if not args:
print(json.dumps({
"success": False,
"error": "Usage: markdown-extract <url> [method]"
}))
sys.exit(1)
url = args[0]
method = args[1] if len(args) > 1 else "auto"
# Validate method
if method not in ["auto", "ai", "browser"]:
print(json.dumps({
"success": False,
"error": f"Invalid method: {method}. Use auto, ai, or browser"
}))
sys.exit(1)
result = extract_markdown(url, method)
print(json.dumps(result))
sys.exit(0 if result["success"] else 1)
if __name__ == "__main__":
main()
General-purpose X/Twitter research agent. Searches X for real-time perspectives, dev discussions, product feedback, cultural takes, breaking news, and expert...
---
name: x-research
version: 1.0.0
description: >
General-purpose X/Twitter research agent. Searches X for real-time perspectives,
dev discussions, product feedback, cultural takes, breaking news, and expert opinions.
Works like a web research agent but uses X as the source.
Use when: (1) user says "x research", "search x for", "search twitter for",
"what are people saying about", "what's twitter saying", "check x for", "x search",
"/x-research", (2) user is working on something where recent X discourse would provide
useful context (new library releases, API changes, product launches, cultural events,
industry drama), (3) user wants to find what devs/experts/community thinks about a topic.
NOT for: posting tweets, account management, or historical archive searches beyond 7 days.
---
# X Research
General-purpose agentic research over X/Twitter. Decompose any research question into targeted searches, iteratively refine, follow threads, deep-dive linked content, and synthesize into a sourced briefing.
For X API details (endpoints, operators, response format): read `references/x-api.md`.
## CLI Tool
All commands run from this skill directory:
```bash
cd ~/clawd/skills/x-research
source ~/.config/env/global.env
```
### Search
```bash
bun run x-search.ts search "<query>" [options]
```
**Options:**
- `--sort likes|impressions|retweets|recent` — sort order (default: likes)
- `--since 1h|3h|12h|1d|7d` — time filter (default: last 7 days). Also accepts minutes (`30m`) or ISO timestamps.
- `--min-likes N` — filter by minimum likes
- `--min-impressions N` — filter by minimum impressions
- `--pages N` — pages to fetch, 1-5 (default: 1, 100 tweets/page)
- `--limit N` — max results to display (default: 15)
- `--quick` — quick mode: 1 page, max 10 results, auto noise filter (`-is:retweet -is:reply`), 1hr cache, cost summary
- `--from <username>` — shorthand for `from:username` in query
- `--quality` — filter low-engagement tweets (≥10 likes, post-hoc)
- `--no-replies` — exclude replies
- `--save` — save results to `~/clawd/drafts/x-research-{slug}-{date}.md`
- `--json` — raw JSON output
- `--markdown` — markdown output for research docs
Auto-adds `-is:retweet` unless query already includes it. All searches display estimated API cost.
**Examples:**
```bash
bun run x-search.ts search "BNKR" --sort likes --limit 10
bun run x-search.ts search "from:frankdegods" --sort recent
bun run x-search.ts search "(opus 4.6 OR claude) trading" --pages 2 --save
bun run x-search.ts search "$BNKR (revenue OR fees)" --min-likes 5
bun run x-search.ts search "BNKR" --quick
bun run x-search.ts search "BNKR" --from voidcider --quick
bun run x-search.ts search "AI agents" --quality --quick
```
### Profile
```bash
bun run x-search.ts profile <username> [--count N] [--replies] [--json]
```
Fetches recent tweets from a specific user (excludes replies by default).
### Thread
```bash
bun run x-search.ts thread <tweet_id> [--pages N]
```
Fetches full conversation thread by root tweet ID.
### Single Tweet
```bash
bun run x-search.ts tweet <tweet_id> [--json]
```
### Watchlist
```bash
bun run x-search.ts watchlist # Show all
bun run x-search.ts watchlist add <user> [note] # Add account
bun run x-search.ts watchlist remove <user> # Remove account
bun run x-search.ts watchlist check # Check recent from all
```
Watchlist stored in `data/watchlist.json`. Use for heartbeat integration — check if key accounts posted anything important.
### Cache
```bash
bun run x-search.ts cache clear # Clear all cached results
```
15-minute TTL. Avoids re-fetching identical queries.
## Research Loop (Agentic)
When doing deep research (not just a quick search), follow this loop:
### 1. Decompose the Question into Queries
Turn the research question into 3-5 keyword queries using X search operators:
- **Core query**: Direct keywords for the topic
- **Expert voices**: `from:` specific known experts
- **Pain points**: Keywords like `(broken OR bug OR issue OR migration)`
- **Positive signal**: Keywords like `(shipped OR love OR fast OR benchmark)`
- **Links**: `url:github.com` or `url:` specific domains
- **Noise reduction**: `-is:retweet` (auto-added), add `-is:reply` if needed
- **Crypto spam**: Add `-airdrop -giveaway -whitelist` if crypto topics flooding
### 2. Search and Extract
Run each query via CLI. After each, assess:
- Signal or noise? Adjust operators.
- Key voices worth searching `from:` specifically?
- Threads worth following via `thread` command?
- Linked resources worth deep-diving with `web_fetch`?
### 3. Follow Threads
When a tweet has high engagement or is a thread starter:
```bash
bun run x-search.ts thread <tweet_id>
```
### 4. Deep-Dive Linked Content
When tweets link to GitHub repos, blog posts, or docs, fetch with `web_fetch`. Prioritize links that:
- Multiple tweets reference
- Come from high-engagement tweets
- Point to technical resources directly relevant to the question
### 5. Synthesize
Group findings by theme, not by query:
```
### [Theme/Finding Title]
[1-2 sentence summary]
- @username: "[key quote]" (NL, NI) [Tweet](url)
- @username2: "[another perspective]" (NL, NI) [Tweet](url)
Resources shared:
- [Resource title](url) — [what it is]
```
### 6. Save
Use `--save` flag or save manually to `~/clawd/drafts/x-research-{topic-slug}-{YYYY-MM-DD}.md`.
## Refinement Heuristics
- **Too much noise?** Add `-is:reply`, use `--sort likes`, narrow keywords
- **Too few results?** Broaden with `OR`, remove restrictive operators
- **Crypto spam?** Add `-$ -airdrop -giveaway -whitelist`
- **Expert takes only?** Use `from:` or `--min-likes 50`
- **Substance over hot takes?** Search with `has:links`
## Heartbeat Integration
On heartbeat, can run `watchlist check` to see if key accounts posted anything notable. Flag to Frank only if genuinely interesting/actionable — don't report routine tweets.
## File Structure
```
skills/x-research/
├── SKILL.md (this file)
├── x-search.ts (CLI entry point)
├── lib/
│ ├── api.ts (X API wrapper: search, thread, profile, tweet)
│ ├── cache.ts (file-based cache, 15min TTL)
│ └── format.ts (Telegram + markdown formatters)
├── data/
│ ├── watchlist.json (accounts to monitor)
│ └── cache/ (auto-managed)
└── references/
└── x-api.md (X API endpoint reference)
```
FILE:CHANGELOG.md
# Changelog
## v2.2.1 (2026-02-09)
### Fixed
- **Updated all pricing docs to reflect X API's new pay-per-use model** (launched Feb 6, 2026)
- Old: Required $200/mo Basic tier subscription — **no longer exists**
- New: Prepaid credits, pay only for what you use, no subscriptions, no monthly caps
- Added per-resource cost breakdown: $0.005/post read, $0.010/user lookup, $0.010/post create
- Added 24-hour deduplication docs — same post fetched twice in a day = 1 charge
- Added xAI credit bonus tiers (10-20% back as Grok credits at $200+ spend)
- Added usage monitoring endpoint (`GET /2/usage/tweets`) for programmatic cost tracking
- Added Developer Console reference (console.x.com) for credit management, auto-recharge, spending limits
- Added full list of tracked/billable endpoints
- Fixed Limitations section — removed outdated "$200/mo" requirement
- Added full-archive search (enterprise-only) note
## v2.2.0 (2026-02-08)
### Added
- **`--quick` mode** — Smarter, cheaper searches. Single page, auto noise filtering (`-is:retweet -is:reply`), 1hr cache TTL. Designed for fast pulse checks.
- **`--from <username>`** — Shorthand for `from:username` queries. `search "BNKR" --from voidcider` instead of typing the full operator.
- **`--quality` flag** — Filters out low-engagement tweets (≥10 likes). Applied post-fetch since `min_faves` operator isn't available via the API.
- **Cost display on all searches** — Every search now shows estimated API cost: `📊 N tweets read · est. cost ~$X`
### Changed
- README cleaned up — removed duplicate cost section, added Quick Mode and Cost docs
- Cache supports variable TTL (1hr in quick mode, 15min default)
## v2.1.0 (2026-02-08)
### Added
- **`--since` time filter** — search only recent tweets: `--since 1h`, `--since 3h`, `--since 30m`, `--since 1d`
- Accepts shorthand (`1h`, `30m`, `2d`) or ISO 8601 timestamps
- Great for monitoring during catalysts or checking what just dropped
- Minutes support (`30m`, `15m`) in addition to hours and days
- Cache keys now include time filter to prevent stale results across different time ranges
## v2.0.0 (2026-02-08)
### Added
- **`x-search.ts` CLI** — Bun script wrapping the X API. No more inline curl/python one-liners.
- `search` — query with auto noise filtering, engagement sorting, pagination
- `profile` — recent tweets from any user
- `thread` — full conversation thread by tweet ID
- `tweet` — single tweet lookup
- `watchlist` — manage accounts to monitor, batch-check recent activity
- `cache clear` — manage result cache
- **`lib/api.ts`** — Typed X API wrapper with search, thread, profile, tweet lookup, engagement filtering, deduplication
- **`lib/cache.ts`** — File-based cache with 15-minute TTL. Avoids re-fetching identical queries.
- **`lib/format.ts`** — Output formatters for Telegram (mobile-friendly) and markdown (research docs)
- **Watchlist system** — `data/watchlist.json` for monitoring accounts. Useful for heartbeat integration.
- **Auto noise filtering** — `-is:retweet` added by default unless already in query
- **Engagement sorting** — `--sort likes|impressions|retweets|recent`
- **Post-hoc filtering** — `--min-likes N` and `--min-impressions N` (since X API Basic tier lacks these operators)
- **Save to file** — `--save` flag auto-saves research to `~/clawd/drafts/`
- **Multiple output formats** — `--json` for raw data, `--markdown` for research docs, default for Telegram
### Changed
- **SKILL.md** rewritten to reference CLI tooling. Research loop instructions preserved and updated.
- **README.md** expanded with full install, setup, usage, and API cost documentation.
### How it compares to v1
- v1 was a prompt-only skill — Claude assembled raw curl commands with inline Python parsers each time
- v2 wraps everything in typed Bun scripts — faster execution, cleaner output, fewer context tokens burned on boilerplate
- Same agentic research loop, same X API, just better tooling underneath
## v1.0.0 (2026-02-08)
### Added
- Initial release
- SKILL.md with agentic research loop (decompose → search → refine → follow threads → deep-dive → synthesize)
- `references/x-api.md` with full X API endpoint reference
- Search operators, pagination, thread following, linked content deep-diving
## v2.1.0 (2026-02-08)
### Added
- **`--since` time filter** — search only recent tweets: `--since 1h`, `--since 3h`, `--since 30m`, `--since 1d`
- Accepts shorthand (`1h`, `30m`, `2d`) or ISO 8601 timestamps
- Great for monitoring during catalysts or checking what just dropped
- Minutes support (`30m`, `15m`) in addition to hours and days
- Cache keys now include time filter to prevent stale results across different time ranges
FILE:README.md
# x-research
X/Twitter research agent for [Claude Code](https://code.claude.com) and [OpenClaw](https://openclaw.ai). Search, filter, monitor — all from the terminal.
## What it does
Wraps the X API into a fast CLI so your AI agent (or you) can search tweets, pull threads, monitor accounts, and get sourced research without writing curl commands.
- **Search** with engagement sorting, time filtering, noise removal
- **Quick mode** for cheap, targeted lookups
- **Watchlists** for monitoring accounts
- **Cache** to avoid repeat API charges
- **Cost transparency** — every search shows what it cost
## Install
### Claude Code
```bash
# From your project
mkdir -p .claude/skills
cd .claude/skills
git clone https://github.com/rohunvora/x-research-skill.git x-research
```
### OpenClaw
```bash
# From your workspace
mkdir -p skills
cd skills
git clone https://github.com/rohunvora/x-research-skill.git x-research
```
## Setup
1. **X API Bearer Token** — Get one from the [X Developer Portal](https://developer.x.com)
2. **Set the env var:**
```bash
export X_BEARER_TOKEN="your-token-here"
```
Or save it to `~/.config/env/global.env`:
```
X_BEARER_TOKEN=your-token-here
```
3. **Install Bun** (for CLI tooling): https://bun.sh
## Usage
### Natural language (just talk to Claude)
- "What are people saying about Opus 4.6?"
- "Search X for OpenClaw skills"
- "What's CT saying about BNKR today?"
- "Check what @frankdegods posted recently"
### CLI commands
```bash
cd skills/x-research
# Search (sorted by likes, auto-filters retweets)
bun run x-search.ts search "your query" --sort likes --limit 10
# Profile — recent tweets from a user
bun run x-search.ts profile username
# Thread — full conversation
bun run x-search.ts thread TWEET_ID
# Single tweet
bun run x-search.ts tweet TWEET_ID
# Watchlist
bun run x-search.ts watchlist add username "optional note"
bun run x-search.ts watchlist check
# Save research to file
bun run x-search.ts search "query" --save --markdown
```
### Search options
```
--sort likes|impressions|retweets|recent (default: likes)
--since 1h|3h|12h|1d|7d Time filter (default: last 7 days)
--min-likes N Filter minimum likes
--min-impressions N Filter minimum impressions
--pages N Pages to fetch, 1-5 (default: 1, 100 tweets/page)
--limit N Results to display (default: 15)
--quick Quick mode (see below)
--from <username> Shorthand for from:username in query
--quality Pre-filter low-engagement tweets (min_faves:10)
--no-replies Exclude replies
--save Save to ~/clawd/drafts/
--json Raw JSON output
--markdown Markdown research doc
```
## Quick Mode
`--quick` is designed for fast, cheap lookups when you just need a pulse check on a topic.
**What it does:**
- Forces single page (max 10 results) — reduces API reads
- Auto-appends `-is:retweet -is:reply` noise filters (unless you explicitly used those operators)
- Uses 1-hour cache TTL instead of the default 15 minutes
- Shows cost summary after results
**Examples:**
```bash
# Quick pulse check on a topic
bun run x-search.ts search "BNKR" --quick
# Quick check what someone is saying
bun run x-search.ts search "BNKR" --from voidcider --quick
# Quick quality-only results
bun run x-search.ts search "AI agents" --quality --quick
```
**Why it's cheaper:**
- Prevents multi-page fetches (biggest cost saver)
- 1hr cache means repeat searches are free
- Noise filters mean fewer junk results in your 100-tweet page
- You see cost after every search — no surprises
## `--from` Shorthand
Adds `from:username` to your query without having to type the full operator syntax.
```bash
# These are equivalent:
bun run x-search.ts search "BNKR from:voidcider"
bun run x-search.ts search "BNKR" --from voidcider
# Works with --quick and other flags
bun run x-search.ts search "AI" --from frankdegods --quick --quality
```
If your query already contains `from:`, the flag won't double-add it.
## `--quality` Flag
Filters out low-engagement tweets (≥10 likes required). Applied post-fetch since `min_faves` isn't available on X API Basic tier.
```bash
bun run x-search.ts search "crypto AI" --quality
```
## Cost
As of February 2026, the X API uses **pay-per-use pricing** with prepaid credits. No subscriptions, no monthly caps. You buy credits in the [Developer Console](https://console.x.com) and they're deducted per request.
**Per-resource costs:**
| Resource | Cost |
|----------|------|
| Post read | $0.005 |
| User lookup | $0.010 |
| Post create | $0.010 |
**Search cost:** Each search page returns up to 100 posts = ~$0.50/page.
| Operation | Est. cost |
|-----------|-----------|
| Quick search (1 page, ≤100 posts) | ~$0.50 |
| Standard search (1 page) | ~$0.50 |
| Deep research (3 pages) | ~$1.50 |
| Profile check (user + posts) | ~$0.51 |
| Watchlist check (5 accounts) | ~$2.55 |
| Cached repeat (any) | free |
**24-hour deduplication:** If you request the same post twice in a UTC day, you're only charged once. This means repeat searches on the same topic within a day cost less than the estimate above.
**Spending controls:** Set auto-recharge thresholds and spending limits per billing cycle in the Developer Console. Failed requests are never billed.
**xAI credit bonus:** Spend $200+/cycle on X API → earn 10-20% back as xAI/Grok API credits. See [pricing docs](https://docs.x.com/x-api/getting-started/pricing).
**How x-search saves money:**
- Cache (15min default, 1hr in quick mode) — repeat queries are free
- 24-hour dedup means re-running the same search costs $0 at API level too
- Quick mode prevents accidental multi-page fetches
- Cost displayed after every search so you know what you're spending
- `--from` targets specific users instead of broad searches
- Monitor your usage programmatically: `GET /2/usage/tweets`
## File structure
```
x-research/
├── SKILL.md # Agent instructions (Claude reads this)
├── x-search.ts # CLI entry point
├── lib/
│ ├── api.ts # X API wrapper
│ ├── cache.ts # File-based cache
│ └── format.ts # Telegram + markdown formatters
└── data/
├── watchlist.json # Accounts to monitor
└── cache/ # Auto-managed
```
## Limitations
- Search covers last 7 days only (recent search endpoint restriction)
- Read-only — never posts or interacts
- Requires X API access with prepaid credits ([sign up](https://console.x.com))
- `min_likes` / `min_retweets` search operators unavailable (filtered post-hoc instead)
- Full-archive search (beyond 7 days) requires enterprise access
## Star History
[](https://star-history.com/#rohunvora/x-research-skill&Date)
## License
MIT
FILE:data/watchlist.example.json
{
"accounts": [
{ "username": "anthropaboraai", "note": "Anthropic", "addedAt": "2026-02-08T00:00:00Z" },
{ "username": "OpenAI", "note": "OpenAI", "addedAt": "2026-02-08T00:00:00Z" }
]
}
FILE:lib/api.ts
/**
* X API wrapper — search, threads, profiles, single tweets.
* Uses Bearer token from env: X_BEARER_TOKEN
*/
import { readFileSync } from "fs";
const BASE = "https://api.x.com/2";
const RATE_DELAY_MS = 350; // stay under 450 req/15min
function getToken(): string {
// Try env first
if (process.env.X_BEARER_TOKEN) return process.env.X_BEARER_TOKEN;
// Try global.env
try {
const envFile = readFileSync(
`process.env.HOME/.config/env/global.env`,
"utf-8"
);
const match = envFile.match(/X_BEARER_TOKEN=["']?([^"'\n]+)/);
if (match) return match[1];
} catch {}
throw new Error(
"X_BEARER_TOKEN not found in env or ~/.config/env/global.env"
);
}
async function sleep(ms: number) {
return new Promise((r) => setTimeout(r, ms));
}
export interface Tweet {
id: string;
text: string;
author_id: string;
username: string;
name: string;
created_at: string;
conversation_id: string;
metrics: {
likes: number;
retweets: number;
replies: number;
quotes: number;
impressions: number;
bookmarks: number;
};
urls: string[];
mentions: string[];
hashtags: string[];
tweet_url: string;
}
interface RawResponse {
data?: any[];
includes?: { users?: any[] };
meta?: { next_token?: string; result_count?: number };
errors?: any[];
title?: string;
detail?: string;
status?: number;
}
function parseTweets(raw: RawResponse): Tweet[] {
if (!raw.data) return [];
const users: Record<string, any> = {};
for (const u of raw.includes?.users || []) {
users[u.id] = u;
}
return raw.data.map((t: any) => {
const u = users[t.author_id] || {};
const m = t.public_metrics || {};
return {
id: t.id,
text: t.text,
author_id: t.author_id,
username: u.username || "?",
name: u.name || "?",
created_at: t.created_at,
conversation_id: t.conversation_id,
metrics: {
likes: m.like_count || 0,
retweets: m.retweet_count || 0,
replies: m.reply_count || 0,
quotes: m.quote_count || 0,
impressions: m.impression_count || 0,
bookmarks: m.bookmark_count || 0,
},
urls: (t.entities?.urls || [])
.map((u: any) => u.expanded_url)
.filter(Boolean),
mentions: (t.entities?.mentions || [])
.map((m: any) => m.username)
.filter(Boolean),
hashtags: (t.entities?.hashtags || [])
.map((h: any) => h.tag)
.filter(Boolean),
tweet_url: `https://x.com/u.username || "?"/status/t.id`,
};
});
}
const FIELDS =
"tweet.fields=created_at,public_metrics,author_id,conversation_id,entities&expansions=author_id&user.fields=username,name,public_metrics";
/**
* Parse a "since" value into an ISO 8601 timestamp.
* Accepts: "1h", "2h", "6h", "12h", "1d", "2d", "3d", "7d"
* Or a raw ISO 8601 string.
*/
function parseSince(since: string): string | null {
// Check for shorthand like "1h", "3h", "1d"
const match = since.match(/^(\d+)(m|h|d)$/);
if (match) {
const num = parseInt(match[1]);
const unit = match[2];
const ms =
unit === "m" ? num * 60_000 :
unit === "h" ? num * 3_600_000 :
num * 86_400_000;
const startTime = new Date(Date.now() - ms);
return startTime.toISOString();
}
// Check if it's already ISO 8601
if (since.includes("T") || since.includes("-")) {
try {
return new Date(since).toISOString();
} catch {
return null;
}
}
return null;
}
async function apiGet(url: string): Promise<RawResponse> {
const token = getToken();
const res = await fetch(url, {
headers: { Authorization: `Bearer token` },
});
if (res.status === 429) {
const reset = res.headers.get("x-rate-limit-reset");
const waitSec = reset
? Math.max(parseInt(reset) - Math.floor(Date.now() / 1000), 1)
: 60;
throw new Error(`Rate limited. Resets in waitSecs`);
}
if (!res.ok) {
const body = await res.text();
throw new Error(`X API res.status: body.slice(0, 200)`);
}
return res.json();
}
/**
* Search recent tweets (last 7 days).
*/
export async function search(
query: string,
opts: {
maxResults?: number;
pages?: number;
sortOrder?: "relevancy" | "recency";
since?: string; // ISO 8601 timestamp or shorthand like "1h", "3h", "1d"
} = {}
): Promise<Tweet[]> {
const maxResults = Math.max(Math.min(opts.maxResults || 100, 100), 10);
const pages = opts.pages || 1;
const sort = opts.sortOrder || "relevancy";
const encoded = encodeURIComponent(query);
// Build time filter
let timeFilter = "";
if (opts.since) {
const startTime = parseSince(opts.since);
if (startTime) {
timeFilter = `&start_time=startTime`;
}
}
let allTweets: Tweet[] = [];
let nextToken: string | undefined;
for (let page = 0; page < pages; page++) {
const pagination = nextToken
? `&pagination_token=nextToken`
: "";
const url = `BASE/tweets/search/recent?query=encoded&max_results=maxResults&FIELDS&sort_order=sorttimeFilterpagination`;
const raw = await apiGet(url);
const tweets = parseTweets(raw);
allTweets.push(...tweets);
nextToken = raw.meta?.next_token;
if (!nextToken) break;
if (page < pages - 1) await sleep(RATE_DELAY_MS);
}
return allTweets;
}
/**
* Fetch a full conversation thread by root tweet ID.
*/
export async function thread(
conversationId: string,
opts: { pages?: number } = {}
): Promise<Tweet[]> {
const query = `conversation_id:conversationId`;
const tweets = await search(query, {
pages: opts.pages || 2,
sortOrder: "recency",
});
// Also fetch the root tweet
try {
const rootUrl = `BASE/tweets/conversationId?FIELDS`;
const raw = await apiGet(rootUrl);
const rootTweets = parseTweets({ ...raw, data: raw.data ? [raw.data] : (raw as any).id ? [raw] : [] });
// Fix: single tweet lookup returns tweet at top level
if ((raw as any).id) {
// raw is the tweet itself — need to re-fetch with proper structure
}
if (rootTweets.length > 0) {
tweets.unshift(...rootTweets);
}
} catch {
// Root tweet might be deleted
}
return tweets;
}
/**
* Get recent tweets from a specific user.
*/
export async function profile(
username: string,
opts: { count?: number; includeReplies?: boolean } = {}
): Promise<{ user: any; tweets: Tweet[] }> {
// First, look up user ID
const userUrl = `BASE/users/by/username/username?user.fields=public_metrics,description,created_at`;
const userData = await apiGet(userUrl);
if (!userData.data) {
throw new Error(`User @username not found`);
}
const user = (userData as any).data;
await sleep(RATE_DELAY_MS);
// Build search query
const replyFilter = opts.includeReplies ? "" : " -is:reply";
const query = `from:username -is:retweetreplyFilter`;
const tweets = await search(query, {
maxResults: Math.min(opts.count || 20, 100),
sortOrder: "recency",
});
return { user, tweets };
}
/**
* Fetch a single tweet by ID.
*/
export async function getTweet(tweetId: string): Promise<Tweet | null> {
const url = `BASE/tweets/tweetId?FIELDS`;
const raw = await apiGet(url);
// Single tweet returns { data: {...}, includes: {...} }
if (raw.data && !Array.isArray(raw.data)) {
const parsed = parseTweets({ ...raw, data: [raw.data] });
return parsed[0] || null;
}
return null;
}
/**
* Sort tweets by engagement metric.
*/
export function sortBy(
tweets: Tweet[],
metric: "likes" | "impressions" | "retweets" | "replies" = "likes"
): Tweet[] {
return [...tweets].sort((a, b) => b.metrics[metric] - a.metrics[metric]);
}
/**
* Filter tweets by minimum engagement.
*/
export function filterEngagement(
tweets: Tweet[],
opts: { minLikes?: number; minImpressions?: number }
): Tweet[] {
return tweets.filter((t) => {
if (opts.minLikes && t.metrics.likes < opts.minLikes) return false;
if (opts.minImpressions && t.metrics.impressions < opts.minImpressions)
return false;
return true;
});
}
/**
* Deduplicate tweets by ID.
*/
export function dedupe(tweets: Tweet[]): Tweet[] {
const seen = new Set<string>();
return tweets.filter((t) => {
if (seen.has(t.id)) return false;
seen.add(t.id);
return true;
});
}
FILE:lib/cache.ts
/**
* Simple file-based cache for X API results.
* Avoids re-fetching identical queries within a TTL window.
*/
import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } from "fs";
import { join } from "path";
import { createHash } from "crypto";
import type { Tweet } from "./api";
const CACHE_DIR = join(import.meta.dir, "..", "data", "cache");
const DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes
function ensureDir() {
if (!existsSync(CACHE_DIR)) mkdirSync(CACHE_DIR, { recursive: true });
}
function cacheKey(query: string, params: string = ""): string {
const hash = createHash("md5")
.update(`query|params`)
.digest("hex")
.slice(0, 12);
return hash;
}
interface CacheEntry {
query: string;
params: string;
timestamp: number;
tweets: Tweet[];
}
export function get(
query: string,
params: string = "",
ttlMs: number = DEFAULT_TTL_MS
): Tweet[] | null {
ensureDir();
const key = cacheKey(query, params);
const path = join(CACHE_DIR, `key.json`);
if (!existsSync(path)) return null;
try {
const entry: CacheEntry = JSON.parse(readFileSync(path, "utf-8"));
if (Date.now() - entry.timestamp > ttlMs) {
unlinkSync(path);
return null;
}
return entry.tweets;
} catch {
return null;
}
}
export function set(
query: string,
params: string = "",
tweets: Tweet[]
): void {
ensureDir();
const key = cacheKey(query, params);
const path = join(CACHE_DIR, `key.json`);
const entry: CacheEntry = {
query,
params,
timestamp: Date.now(),
tweets,
};
writeFileSync(path, JSON.stringify(entry, null, 2));
}
/**
* Clear expired cache entries.
*/
export function prune(ttlMs: number = DEFAULT_TTL_MS): number {
ensureDir();
let removed = 0;
const files = readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json"));
for (const file of files) {
const path = join(CACHE_DIR, file);
try {
const stat = statSync(path);
if (Date.now() - stat.mtimeMs > ttlMs) {
unlinkSync(path);
removed++;
}
} catch {}
}
return removed;
}
/**
* Clear all cache.
*/
export function clear(): number {
ensureDir();
const files = readdirSync(CACHE_DIR).filter((f) => f.endsWith(".json"));
for (const f of files) {
try {
unlinkSync(join(CACHE_DIR, f));
} catch {}
}
return files.length;
}
FILE:lib/format.ts
/**
* Format tweets for Telegram or markdown output.
*/
import type { Tweet } from "./api";
function compactNumber(n: number): string {
if (n >= 1_000_000) return `(n / 1_000_000).toFixed(1)M`;
if (n >= 1_000) return `(n / 1_000).toFixed(1)K`;
return String(n);
}
function timeAgo(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60_000);
if (mins < 60) return `minsm`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `hoursh`;
const days = Math.floor(hours / 24);
return `daysd`;
}
/**
* Format a single tweet for Telegram (monospace-friendly).
*/
export function formatTweetTelegram(t: Tweet, index?: number): string {
const prefix = index !== undefined ? `index + 1. ` : "";
const engagement = `compactNumber(t.metrics.likes)❤️ compactNumber(t.metrics.impressions)👁`;
const time = timeAgo(t.created_at);
// Truncate text to 200 chars for summary view
const text = t.text.length > 200 ? t.text.slice(0, 197) + "..." : t.text;
// Clean up t.co links from text
const cleanText = text.replace(/https:\/\/t\.co\/\S+/g, "").trim();
let out = `prefix@t.username (engagement · time)\ncleanText`;
if (t.urls.length > 0) {
out += `\n🔗 t.urls[0]`;
}
out += `\nt.tweet_url`;
return out;
}
/**
* Format a list of tweets for Telegram.
*/
export function formatResultsTelegram(
tweets: Tweet[],
opts: { query?: string; limit?: number } = {}
): string {
const limit = opts.limit || 15;
const shown = tweets.slice(0, limit);
let out = "";
if (opts.query) {
out += `🔍 "opts.query" — tweets.length results\n\n`;
}
out += shown.map((t, i) => formatTweetTelegram(t, i)).join("\n\n");
if (tweets.length > limit) {
out += `\n\n... +tweets.length - limit more`;
}
return out;
}
/**
* Format a single tweet for markdown (research docs).
*/
export function formatTweetMarkdown(t: Tweet): string {
const engagement = `t.metrics.likesL t.metrics.impressionsI`;
const cleanText = t.text.replace(/https:\/\/t\.co\/\S+/g, "").trim();
const quoted = cleanText.replace(/\n/g, "\n > ");
let out = `- **@t.username** (engagement) [Tweet](t.tweet_url)\n > quoted`;
if (t.urls.length > 0) {
out += `\n Links: t.urls.map((u) => `[${new URL(u).hostname](u)`).join(", ")}`;
}
return out;
}
/**
* Format results as a full markdown research document.
*/
export function formatResearchMarkdown(
query: string,
tweets: Tweet[],
opts: {
themes?: { title: string; tweetIds: string[] }[];
apiCalls?: number;
queries?: string[];
} = {}
): string {
const date = new Date().toISOString().split("T")[0];
let out = `# X Research: query\n\n`;
out += `**Date:** date\n`;
out += `**Tweets found:** tweets.length\n\n`;
if (opts.themes && opts.themes.length > 0) {
for (const theme of opts.themes) {
out += `## theme.title\n\n`;
const themeTweets = theme.tweetIds
.map((id) => tweets.find((t) => t.id === id))
.filter(Boolean) as Tweet[];
out += themeTweets.map(formatTweetMarkdown).join("\n\n");
out += "\n\n";
}
} else {
// No themes — just list by engagement
out += `## Top Results (by engagement)\n\n`;
out += tweets
.slice(0, 30)
.map(formatTweetMarkdown)
.join("\n\n");
out += "\n\n";
}
out += `---\n\n## Research Metadata\n`;
out += `- **Query:** query\n`;
out += `- **Date:** date\n`;
if (opts.apiCalls) out += `- **API calls:** opts.apiCalls\n`;
out += `- **Tweets scanned:** tweets.length\n`;
out += `- **Est. cost:** ~$((tweets.length * 0.005)).toFixed(2)\n`;
if (opts.queries) {
out += `- **Search queries:**\n`;
for (const q of opts.queries) {
out += ` - \`q\`\n`;
}
}
return out;
}
/**
* Format a user profile for Telegram.
*/
export function formatProfileTelegram(user: any, tweets: Tweet[]): string {
const m = user.public_metrics || {};
let out = `👤 @user.username — user.name\n`;
out += `compactNumber(m.followers_count || 0) followers · compactNumber(m.tweet_count || 0) tweets\n`;
if (user.description) {
out += `user.description.slice(0, 150)\n`;
}
out += `\nRecent:\n\n`;
out += tweets
.slice(0, 10)
.map((t, i) => formatTweetTelegram(t, i))
.join("\n\n");
return out;
}
FILE:references/x-api.md
# X API Reference
## Authentication
Bearer token from env var `X_BEARER_TOKEN`.
```
-H "Authorization: Bearer $X_BEARER_TOKEN"
```
## Search Endpoint
```
GET https://api.x.com/2/tweets/search/recent
```
Covers last 7 days. Max 100 results per request.
### Standard Query Params
```
tweet.fields=created_at,public_metrics,author_id,conversation_id,entities
expansions=author_id
user.fields=username,name,public_metrics
max_results=100
```
Add `sort_order=relevancy` for relevance ranking (default is recency).
Paginate with `next_token` from response `meta.next_token`.
### Search Operators
| Operator | Example | Notes |
|----------|---------|-------|
| keyword | `bun 2.0` | Implicit AND |
| `OR` | `bun OR deno` | Must be uppercase |
| `-` | `-is:retweet` | Negation |
| `()` | `(fast OR perf)` | Grouping |
| `from:` | `from:elonmusk` | Posts by user |
| `to:` | `to:elonmusk` | Replies to user |
| `#` | `#buildinpublic` | Hashtag |
| `$` | `$AAPL` | Cashtag |
| `lang:` | `lang:en` | BCP-47 language code |
| `is:retweet` | `-is:retweet` | Filter retweets |
| `is:reply` | `-is:reply` | Filter replies |
| `is:quote` | `is:quote` | Quote tweets |
| `has:media` | `has:media` | Contains media |
| `has:links` | `has:links` | Contains links |
| `url:` | `url:github.com` | Links to domain |
| `conversation_id:` | `conversation_id:123` | Thread by root tweet ID |
| `place_country:` | `place_country:US` | Country filter |
**Unavailable on current tier:** `min_likes`, `min_retweets`, `min_replies`. Filter engagement post-hoc from `public_metrics`.
**Limits:** Max query length 512 chars. Max ~10 operators per query.
### Response Structure
```json
{
"data": [{
"id": "tweet_id",
"text": "...",
"author_id": "user_id",
"created_at": "2026-...",
"conversation_id": "root_tweet_id",
"public_metrics": {
"retweet_count": 0,
"reply_count": 0,
"like_count": 0,
"quote_count": 0,
"bookmark_count": 0,
"impression_count": 0
},
"entities": {
"urls": [{"expanded_url": "https://..."}],
"mentions": [{"username": "..."}],
"hashtags": [{"tag": "..."}]
}
}],
"includes": {
"users": [{"id": "user_id", "username": "handle", "name": "Display Name", "public_metrics": {...}}]
},
"meta": {"next_token": "...", "result_count": 100}
}
```
### Constructing Tweet URLs
```
https://x.com/{username}/status/{tweet_id}
```
Both values available from response data + user expansions.
### Linked Content
External URLs from tweets are in `entities.urls[].expanded_url`. Use WebFetch to deep-dive into linked pages (GitHub READMEs, blog posts, docs, etc.).
### Rate Limits
- 450 requests per 15-minute window (app-level)
- 300 requests per 15-minute window (user-level)
### Cost (Pay-Per-Use — Updated Feb 2026)
X API uses **pay-per-use pricing** with prepaid credits. No subscriptions, no monthly caps.
**Per-resource costs:**
| Resource | Cost |
|----------|------|
| Post read | $0.005 |
| User lookup | $0.010 |
| Post create | $0.010 |
A typical research session: 5 queries × 100 tweets = 500 post reads = ~$2.50.
**24-hour deduplication:** Same post requested multiple times within a UTC day = 1 charge. Re-running the same search within 24h costs significantly less.
**Billing details:**
- Purchase credits upfront at [console.x.com](https://console.x.com)
- Set auto-recharge (trigger amount + threshold) to avoid interruptions
- Set spending limits per billing cycle
- Failed requests are not billed
- Streaming (Filtered Stream): each unique post delivered counts, with 24h dedup
**Usage monitoring endpoint:**
```
GET https://api.x.com/2/usage/tweets
Authorization: Bearer $BEARER_TOKEN
```
Returns daily post consumption counts per app. Use for budget tracking and alerts.
**xAI credit bonus:**
| Cumulative spend (per cycle) | xAI credit rate |
|------------------------------|-----------------|
| $0 – $199 | 0% |
| $200 – $499 | 10% |
| $500 – $999 | 15% |
| $1,000+ | 20% |
Credits are rolling — order/size of purchases doesn't affect total rewards.
**Tracked endpoints (all count toward usage):**
- Post lookup, Recent search, Full-archive search
- Filtered stream, Filtered stream webhooks
- User posts/mentions timelines
- Liked posts, Bookmarks, List posts, Spaces lookup
## Single Tweet Lookup
```
GET https://api.x.com/2/tweets/{id}
```
Same fields/expansions params. Use for fetching specific tweets by ID.
FILE:x-search.ts
#!/usr/bin/env bun
/**
* x-search — CLI for X/Twitter research.
*
* Commands:
* search <query> [options] Search recent tweets
* thread <tweet_id> Fetch full conversation thread
* profile <username> Recent tweets from a user
* tweet <tweet_id> Fetch a single tweet
* watchlist Show watchlist
* watchlist add <user> Add user to watchlist
* watchlist remove <user> Remove user from watchlist
* watchlist check Check recent tweets from all watchlist accounts
* cache clear Clear search cache
*
* Search options:
* --sort likes|impressions|retweets|recent Sort order (default: likes)
* --min-likes N Filter by minimum likes
* --min-impressions N Filter by minimum impressions
* --pages N Number of pages to fetch (default: 1, max 5)
* --no-replies Exclude replies
* --no-retweets Exclude retweets (added by default)
* --limit N Max results to display (default: 15)
* --quick Quick mode: 1 page, noise filter, 1hr cache
* --from <username> Shorthand for from:username in query
* --quality Pre-filter low-engagement (min_faves:10)
* --save Save results to ~/clawd/drafts/
* --json Output raw JSON
* --markdown Output as markdown (for research docs)
*/
import { readFileSync, writeFileSync, existsSync } from "fs";
import { join } from "path";
import * as api from "./lib/api";
import * as cache from "./lib/cache";
import * as fmt from "./lib/format";
const SKILL_DIR = import.meta.dir;
const WATCHLIST_PATH = join(SKILL_DIR, "data", "watchlist.json");
const DRAFTS_DIR = join(process.env.HOME!, "clawd", "drafts");
// --- Arg parsing ---
const args = process.argv.slice(2);
const command = args[0];
function getFlag(name: string): boolean {
const idx = args.indexOf(`--name`);
if (idx >= 0) {
args.splice(idx, 1);
return true;
}
return false;
}
function getOpt(name: string): string | undefined {
const idx = args.indexOf(`--name`);
if (idx >= 0 && idx + 1 < args.length) {
const val = args[idx + 1];
args.splice(idx, 2);
return val;
}
return undefined;
}
// --- Watchlist ---
interface Watchlist {
accounts: { username: string; note?: string; addedAt: string }[];
}
function loadWatchlist(): Watchlist {
if (!existsSync(WATCHLIST_PATH))
return { accounts: [] };
return JSON.parse(readFileSync(WATCHLIST_PATH, "utf-8"));
}
function saveWatchlist(wl: Watchlist) {
writeFileSync(WATCHLIST_PATH, JSON.stringify(wl, null, 2));
}
// --- Commands ---
async function cmdSearch() {
// Parse new flags first (before getOpt consumes positional args)
const quick = getFlag("quick");
const quality = getFlag("quality");
const fromUser = getOpt("from");
const sortOpt = getOpt("sort") || "likes";
const minLikes = parseInt(getOpt("min-likes") || "0");
const minImpressions = parseInt(getOpt("min-impressions") || "0");
let pages = Math.min(parseInt(getOpt("pages") || "1"), 5);
let limit = parseInt(getOpt("limit") || "15");
const since = getOpt("since");
const noReplies = getFlag("no-replies");
const noRetweets = getFlag("no-retweets");
const save = getFlag("save");
const asJson = getFlag("json");
const asMarkdown = getFlag("markdown");
// Quick mode overrides
if (quick) {
pages = 1;
limit = Math.min(limit, 10);
}
// Everything after "search" that isn't a flag is the query
const queryParts = args.slice(1).filter((a) => !a.startsWith("--"));
let query = queryParts.join(" ");
if (!query) {
console.error("Usage: x-search.ts search <query> [options]");
process.exit(1);
}
// --from shorthand: add from:username if not already in query
if (fromUser && !query.toLowerCase().includes("from:")) {
query += ` from:fromUser.replace(/^@/, "")`;
}
// Auto-add noise filters unless already present
if (!query.includes("is:retweet") && !noRetweets) {
query += " -is:retweet";
}
if (quick && !query.includes("is:reply")) {
query += " -is:reply";
} else if (noReplies && !query.includes("is:reply")) {
query += " -is:reply";
}
// Cache TTL: 1hr for quick mode, 15min default
const cacheTtlMs = quick ? 3_600_000 : 900_000;
// Check cache (cache key does NOT include quick flag — shared between modes)
const cacheParams = `sort=sortOpt&pages=pages&since=since || "7d"`;
const cached = cache.get(query, cacheParams, cacheTtlMs);
let tweets: api.Tweet[];
if (cached) {
tweets = cached;
console.error(`(cached — tweets.length tweets)`);
} else {
tweets = await api.search(query, {
pages,
sortOrder: sortOpt === "recent" ? "recency" : "relevancy",
since: since || undefined,
});
cache.set(query, cacheParams, tweets);
}
// Track raw count for cost (API charges per tweet read, regardless of post-hoc filters)
const rawTweetCount = tweets.length;
// Filter
if (minLikes > 0 || minImpressions > 0) {
tweets = api.filterEngagement(tweets, {
minLikes: minLikes || undefined,
minImpressions: minImpressions || undefined,
});
}
// --quality: post-hoc filter for min 10 likes (min_faves operator unavailable on Basic tier)
if (quality) {
tweets = api.filterEngagement(tweets, { minLikes: 10 });
}
// Sort
if (sortOpt !== "recent") {
const metric = sortOpt as "likes" | "impressions" | "retweets";
tweets = api.sortBy(tweets, metric);
}
tweets = api.dedupe(tweets);
// Output
if (asJson) {
console.log(JSON.stringify(tweets.slice(0, limit), null, 2));
} else if (asMarkdown) {
const md = fmt.formatResearchMarkdown(query, tweets, {
queries: [query],
});
console.log(md);
} else {
console.log(fmt.formatResultsTelegram(tweets, { query, limit }));
}
// Save
if (save) {
const slug = query
.replace(/[^a-zA-Z0-9]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 40)
.toLowerCase();
const date = new Date().toISOString().split("T")[0];
const path = join(DRAFTS_DIR, `x-research-slug-date.md`);
const md = fmt.formatResearchMarkdown(query, tweets, {
queries: [query],
});
writeFileSync(path, md);
console.error(`\nSaved to path`);
}
// Cost display (based on raw API reads, not post-filter count)
const cost = (rawTweetCount * 0.005).toFixed(2);
if (quick) {
console.error(`\n⚡ quick mode · rawTweetCount tweets read (~$cost)`);
} else {
console.error(`\n📊 rawTweetCount tweets read · est. cost ~$cost`);
}
// Stats to stderr
const filtered = rawTweetCount !== tweets.length ? ` → tweets.length after filters` : "";
const sinceLabel = since ? ` | since since` : "";
console.error(
`rawTweetCount tweetsfiltered | sorted by sortOpt | pages page(s)sinceLabel`
);
}
async function cmdThread() {
const tweetId = args[1];
if (!tweetId) {
console.error("Usage: x-search.ts thread <tweet_id>");
process.exit(1);
}
const pages = Math.min(parseInt(getOpt("pages") || "2"), 5);
const tweets = await api.thread(tweetId, { pages });
if (tweets.length === 0) {
console.log("No tweets found in thread.");
return;
}
console.log(`🧵 Thread (tweets.length tweets)\n`);
for (const t of tweets) {
console.log(fmt.formatTweetTelegram(t));
console.log();
}
}
async function cmdProfile() {
const username = args[1]?.replace(/^@/, "");
if (!username) {
console.error("Usage: x-search.ts profile <username>");
process.exit(1);
}
const count = parseInt(getOpt("count") || "20");
const includeReplies = getFlag("replies");
const asJson = getFlag("json");
const { user, tweets } = await api.profile(username, {
count,
includeReplies,
});
if (asJson) {
console.log(JSON.stringify({ user, tweets }, null, 2));
} else {
console.log(fmt.formatProfileTelegram(user, tweets));
}
}
async function cmdTweet() {
const tweetId = args[1];
if (!tweetId) {
console.error("Usage: x-search.ts tweet <tweet_id>");
process.exit(1);
}
const tweet = await api.getTweet(tweetId);
if (!tweet) {
console.log("Tweet not found.");
return;
}
const asJson = getFlag("json");
if (asJson) {
console.log(JSON.stringify(tweet, null, 2));
} else {
console.log(fmt.formatTweetTelegram(tweet));
}
}
async function cmdWatchlist() {
const sub = args[1];
const wl = loadWatchlist();
if (sub === "add") {
const username = args[2]?.replace(/^@/, "");
const note = args.slice(3).join(" ") || undefined;
if (!username) {
console.error("Usage: x-search.ts watchlist add <username> [note]");
process.exit(1);
}
if (wl.accounts.find((a) => a.username.toLowerCase() === username.toLowerCase())) {
console.log(`@username already on watchlist.`);
return;
}
wl.accounts.push({
username,
note,
addedAt: new Date().toISOString(),
});
saveWatchlist(wl);
console.log(`Added @username to watchlist.note ? ` (${note)` : ""}`);
return;
}
if (sub === "remove" || sub === "rm") {
const username = args[2]?.replace(/^@/, "");
if (!username) {
console.error("Usage: x-search.ts watchlist remove <username>");
process.exit(1);
}
const before = wl.accounts.length;
wl.accounts = wl.accounts.filter(
(a) => a.username.toLowerCase() !== username.toLowerCase()
);
saveWatchlist(wl);
console.log(
wl.accounts.length < before
? `Removed @username from watchlist.`
: `@username not found on watchlist.`
);
return;
}
if (sub === "check") {
if (wl.accounts.length === 0) {
console.log("Watchlist is empty. Add accounts with: watchlist add <username>");
return;
}
console.log(`Checking wl.accounts.length watchlist accounts...\n`);
for (const acct of wl.accounts) {
try {
const { user, tweets } = await api.profile(acct.username, { count: 5 });
const label = acct.note ? ` (acct.note)` : "";
console.log(`\n--- @acct.usernamelabel ---`);
if (tweets.length === 0) {
console.log(" No recent tweets.");
} else {
for (const t of tweets.slice(0, 3)) {
console.log(fmt.formatTweetTelegram(t));
console.log();
}
}
} catch (e: any) {
console.error(` Error checking @acct.username: e.message`);
}
}
return;
}
// Default: show watchlist
if (wl.accounts.length === 0) {
console.log("Watchlist is empty. Add accounts with: watchlist add <username>");
return;
}
console.log(`📋 Watchlist (wl.accounts.length accounts)\n`);
for (const acct of wl.accounts) {
const note = acct.note ? ` — acct.note` : "";
console.log(` @acct.usernamenote (added acct.addedAt.split("T")[0])`);
}
}
async function cmdCache() {
const sub = args[1];
if (sub === "clear") {
const removed = cache.clear();
console.log(`Cleared removed cached entries.`);
} else {
const removed = cache.prune();
console.log(`Pruned removed expired entries.`);
}
}
function usage() {
console.log(`x-search — X/Twitter research CLI
Commands:
search <query> [options] Search recent tweets (last 7 days)
thread <tweet_id> Fetch full conversation thread
profile <username> Recent tweets from a user
tweet <tweet_id> Fetch a single tweet
watchlist Show watchlist
watchlist add <user> [note] Add user to watchlist
watchlist remove <user> Remove user from watchlist
watchlist check Check recent from all watchlist accounts
cache clear Clear search cache
Search options:
--sort likes|impressions|retweets|recent (default: likes)
--since 1h|3h|12h|1d|7d Time filter (default: last 7 days)
--min-likes N Filter minimum likes
--min-impressions N Filter minimum impressions
--pages N Pages to fetch, 1-5 (default: 1)
--limit N Results to display (default: 15)
--quick Quick mode: 1 page, max 10 results, auto noise
filter, 1hr cache TTL, cost summary
--from <username> Shorthand for from:username in query
--quality Pre-filter low-engagement tweets (min_faves:10)
--no-replies Exclude replies
--save Save to ~/clawd/drafts/
--json Raw JSON output
--markdown Markdown output`);
}
// --- Main ---
async function main() {
switch (command) {
case "search":
case "s":
await cmdSearch();
break;
case "thread":
case "t":
await cmdThread();
break;
case "profile":
case "p":
await cmdProfile();
break;
case "tweet":
await cmdTweet();
break;
case "watchlist":
case "wl":
await cmdWatchlist();
break;
case "cache":
await cmdCache();
break;
default:
usage();
}
}
main().catch((e) => {
console.error(`Error: e.message`);
process.exit(1);
});
Smart contract security analysis skill. Detect vulnerabilities, suggest fixes, generate audit reports. Supports Hardhat/Foundry projects. Uses pattern matchi...
---
name: solidity-guardian
version: 1.0.0
description: Smart contract security analysis skill. Detect vulnerabilities, suggest fixes, generate audit reports. Supports Hardhat/Foundry projects. Uses pattern matching + best practices from Trail of Bits, OpenZeppelin, and Consensys.
author: aviclaw
tags:
- solidity
- security
- audit
- smart-contracts
- ethereum
- vulnerability
- scanner
---
# Solidity Guardian 🛡️
Security analysis for Solidity smart contracts. Find vulnerabilities, get fix suggestions, follow best practices.
## Quick Start
```bash
# Analyze a single contract
node skills/solidity-guardian/analyze.js contracts/MyContract.sol
# Analyze entire project
node skills/solidity-guardian/analyze.js ./contracts/
# Generate markdown report
node skills/solidity-guardian/analyze.js ./contracts/ --format markdown > AUDIT.md
```
## What It Detects (40+ Patterns)
### Critical (Must Fix)
| ID | Vulnerability | Description |
|----|--------------|-------------|
| SG-001 | Reentrancy | External calls before state updates |
| SG-002 | Unprotected selfdestruct | Missing access control on selfdestruct |
| SG-003 | Delegatecall to untrusted | Delegatecall with user-controlled address |
| SG-004 | Uninitialized storage pointer | Storage pointer overwrites slots |
| SG-005 | Signature replay | ecrecover without nonce/chainId |
| SG-006 | Arbitrary jump | Function type from user input |
### High (Should Fix)
| ID | Vulnerability | Description |
|----|--------------|-------------|
| SG-010 | Missing access control | Public functions that should be restricted |
| SG-011 | Unchecked transfer | ERC20 transfer without return check |
| SG-012 | Integer overflow | Arithmetic without SafeMath (pre-0.8) |
| SG-013 | tx.origin auth | Using tx.origin for authentication |
| SG-014 | Weak randomness | block.timestamp/blockhash for randomness |
| SG-015 | Unprotected withdrawal | Withdrawal without ownership check |
| SG-016 | Unchecked low-level call | .call() without success check |
| SG-017 | Dangerous equality | Strict balance check (manipulable) |
| SG-018 | Deprecated functions | suicide, sha3, throw, callcode |
| SG-019 | Wrong constructor | Function name matches contract |
### Medium (Consider Fixing)
| ID | Vulnerability | Description |
|----|--------------|-------------|
| SG-020 | Floating pragma | Non-pinned Solidity version |
| SG-021 | Missing zero check | No validation for zero address |
| SG-022 | Timestamp dependence | Logic depends on block.timestamp |
| SG-023 | DoS with revert | Loop with external call can revert |
| SG-024 | Front-running risk | Predictable state changes |
### Low (Best Practice)
| ID | Vulnerability | Description |
|----|--------------|-------------|
| SG-030 | Missing events | State changes without events |
| SG-031 | Magic numbers | Hardcoded values without constants |
| SG-032 | Implicit visibility | Functions without explicit visibility |
| SG-033 | Large contract | Contract exceeds size recommendations |
| SG-034 | Missing NatSpec | Public functions without documentation |
## Usage Examples
### Basic Analysis
```javascript
const { analyzeContract } = require('./analyzer');
const results = await analyzeContract('contracts/Token.sol');
console.log(results.findings);
```
### With Fix Suggestions
```javascript
const results = await analyzeContract('contracts/Vault.sol', {
includeFixes: true,
severity: ['critical', 'high']
});
for (const finding of results.findings) {
console.log(`[finding.severity] finding.title`);
console.log(` Line finding.line: finding.description`);
console.log(` Fix: finding.suggestion`);
}
```
### Generate Report
```javascript
const { generateReport } = require('./reporter');
const report = await generateReport('./contracts/', {
format: 'markdown',
includeGas: true,
includeBestPractices: true
});
fs.writeFileSync('SECURITY_AUDIT.md', report);
```
## Best Practices Checklist
When writing secure contracts, follow these guidelines:
### Access Control
- [ ] Use OpenZeppelin's `Ownable` or `AccessControl`
- [ ] Apply `onlyOwner` or role checks to sensitive functions
- [ ] Implement two-step ownership transfer
- [ ] Consider timelocks for critical operations
### Reentrancy Prevention
- [ ] Use `ReentrancyGuard` on all external-facing functions
- [ ] Follow checks-effects-interactions pattern
- [ ] Update state BEFORE external calls
- [ ] Use pull over push for payments
### Input Validation
- [ ] Validate all external inputs
- [ ] Check for zero addresses
- [ ] Validate array lengths match
- [ ] Use SafeERC20 for token transfers
### Arithmetic Safety
- [ ] Use Solidity 0.8+ or SafeMath
- [ ] Check for division by zero
- [ ] Validate percentage calculations (≤100)
- [ ] Be careful with token decimals
### Upgradeability (if applicable)
- [ ] Use initializer instead of constructor
- [ ] Protect initialize from re-initialization
- [ ] Follow storage layout rules
- [ ] Test upgrade paths
## Slither Integration
Guardian can run alongside Slither for comprehensive analysis:
```bash
# Combined analysis (auto-installs Slither if missing)
node skills/solidity-guardian/slither-integration.js ./contracts/ --install-slither
# Generate combined report
node skills/solidity-guardian/slither-integration.js . --format markdown --output AUDIT.md
# Guardian only (faster, no Slither dependency)
node skills/solidity-guardian/slither-integration.js ./contracts/ --guardian-only
# Slither only
node skills/solidity-guardian/slither-integration.js ./contracts/ --slither-only
```
**Why both?**
- Guardian: Fast pattern matching, custom rules, no compilation needed
- Slither: Deep dataflow analysis, CFG-based detection, more comprehensive
## Integration with Other Tools
### Hardhat
```javascript
// hardhat.config.js
require('./skills/solidity-guardian/hardhat-plugin');
// Run: npx hardhat guardian
```
### Foundry
```bash
# Add to CI
forge build
node skills/solidity-guardian/analyze.js ./src/
```
## References
- [Trail of Bits - Building Secure Contracts](https://github.com/crytic/building-secure-contracts)
- [OpenZeppelin - Security Best Practices](https://docs.openzeppelin.com/learn/preparing-for-mainnet)
- [Consensys - Smart Contract Best Practices](https://consensys.github.io/smart-contract-best-practices/)
- [SWC Registry](https://swcregistry.io/)
---
Built by Avi 🔐 | Security-first, ship always.
FILE:BEST_PRACTICES.md
# Solidity Security Best Practices
A comprehensive guide for writing secure smart contracts. Based on patterns from Trail of Bits, OpenZeppelin, Consensys, and real-world audits.
## 🔴 Critical: Must Follow
### 1. Reentrancy Protection
**Problem:** External calls can re-enter your contract before state is updated.
```solidity
// ❌ VULNERABLE
function withdraw() external {
uint256 amount = balances[msg.sender];
(bool success, ) = msg.sender.call{value: amount}("");
require(success);
balances[msg.sender] = 0; // State updated AFTER external call
}
// ✅ SECURE
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
function withdraw() external nonReentrant {
uint256 amount = balances[msg.sender];
balances[msg.sender] = 0; // State updated BEFORE external call
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
```
**Rules:**
- Always use `ReentrancyGuard` on functions with external calls
- Follow checks-effects-interactions pattern
- Update state BEFORE making external calls
- Use pull-over-push for payments when possible
### 2. Access Control
**Problem:** Sensitive functions accessible by anyone.
```solidity
// ❌ VULNERABLE
function setPrice(uint256 newPrice) external {
price = newPrice; // Anyone can call!
}
// ✅ SECURE
import "@openzeppelin/contracts/access/Ownable.sol";
function setPrice(uint256 newPrice) external onlyOwner {
price = newPrice;
}
```
**Rules:**
- Use `Ownable` or `AccessControl` from OpenZeppelin
- Apply modifiers to all admin functions
- Consider timelocks for critical operations
- Implement two-step ownership transfer
### 3. Input Validation
```solidity
// ❌ VULNERABLE
function setRecipient(address _recipient) external onlyOwner {
recipient = _recipient; // Could be set to zero address!
}
// ✅ SECURE
function setRecipient(address _recipient) external onlyOwner {
require(_recipient != address(0), "Zero address");
recipient = _recipient;
}
```
**Rules:**
- Always validate addresses are non-zero
- Check array lengths match when processing pairs
- Validate amounts are within expected ranges
- Use SafeERC20 for token transfers
## 🟠 High: Should Follow
### 4. Safe External Calls
```solidity
// ❌ RISKY - Some tokens don't return bool
token.transfer(recipient, amount);
// ✅ SAFE
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
using SafeERC20 for IERC20;
token.safeTransfer(recipient, amount);
```
### 5. Integer Safety
```solidity
// For Solidity < 0.8.0
// ❌ VULNERABLE
uint256 result = a + b; // Can overflow!
// ✅ SAFE
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
using SafeMath for uint256;
uint256 result = a.add(b);
// For Solidity >= 0.8.0
// ✅ Built-in overflow checks
uint256 result = a + b; // Reverts on overflow
```
### 6. Avoid tx.origin
```solidity
// ❌ VULNERABLE - Phishing attack vector
require(tx.origin == owner, "Not owner");
// ✅ SECURE
require(msg.sender == owner, "Not owner");
```
### 7. Secure Randomness
```solidity
// ❌ VULNERABLE - Predictable/manipulable
uint256 random = uint256(keccak256(abi.encodePacked(
block.timestamp,
block.difficulty
)));
// ✅ SECURE - Use Chainlink VRF
import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol";
function getRandomNumber() internal returns (bytes32 requestId) {
return requestRandomness(keyHash, fee);
}
```
## 🟡 Medium: Consider Following
### 8. Pin Pragma Version
```solidity
// ❌ RISKY - Different compiler versions
pragma solidity ^0.8.0;
// ✅ BETTER - Pinned version
pragma solidity 0.8.20;
```
### 9. Timestamp Considerations
```solidity
// ⚠️ ACCEPTABLE for coarse timing (days, hours)
require(block.timestamp >= unlockTime, "Locked");
// ❌ RISKY for fine-grained timing or randomness
uint256 random = block.timestamp % 10;
```
Miners can manipulate `block.timestamp` by ~15 seconds. Acceptable for subscription billing, not for gambling.
### 10. Avoid Loops with External Calls
```solidity
// ❌ VULNERABLE - DoS if one transfer fails
function distributeRewards(address[] calldata users) external {
for (uint i = 0; i < users.length; i++) {
payable(users[i]).transfer(rewards[users[i]]);
}
}
// ✅ SECURE - Pull pattern
mapping(address => uint256) public pendingRewards;
function claimReward() external {
uint256 amount = pendingRewards[msg.sender];
pendingRewards[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
```
## 🔵 Low: Best Practices
### 11. Emit Events
```solidity
event PriceUpdated(uint256 oldPrice, uint256 newPrice);
function setPrice(uint256 newPrice) external onlyOwner {
uint256 oldPrice = price;
price = newPrice;
emit PriceUpdated(oldPrice, newPrice); // Enable off-chain tracking
}
```
### 12. Use Named Constants
```solidity
// ❌ UNCLEAR
uint256 fee = amount * 25 / 10000;
// ✅ CLEAR
uint256 constant FEE_BPS = 25;
uint256 constant BPS_DENOMINATOR = 10000;
uint256 fee = amount * FEE_BPS / BPS_DENOMINATOR;
```
### 13. Explicit Visibility
```solidity
// ❌ IMPLICIT
function _internalHelper() { }
// ✅ EXPLICIT
function _internalHelper() internal { }
```
### 14. NatSpec Documentation
```solidity
/// @notice Transfers tokens to a recipient
/// @param recipient The address receiving tokens
/// @param amount The amount to transfer
/// @return success Whether the transfer succeeded
function transfer(address recipient, uint256 amount)
external
returns (bool success)
{
// ...
}
```
## 📋 Pre-Deployment Checklist
### Security
- [ ] All external/public functions reviewed for access control
- [ ] Reentrancy guards on functions with external calls
- [ ] SafeERC20 used for token transfers
- [ ] Zero address checks on address parameters
- [ ] No tx.origin authentication
- [ ] Chainlink VRF for randomness (if needed)
### Code Quality
- [ ] Pragma pinned to specific version
- [ ] All functions have explicit visibility
- [ ] Events emitted for state changes
- [ ] NatSpec on public functions
- [ ] No magic numbers
### Testing
- [ ] Unit tests for all functions
- [ ] Edge case tests (0 amounts, max values)
- [ ] Access control tests
- [ ] Reentrancy tests
- [ ] Fuzzing with Foundry/Echidna
### External Review
- [ ] Run Slither: `slither .`
- [ ] Run Solidity Guardian: `node analyze.js ./contracts/`
- [ ] Manual review by second developer
- [ ] Consider professional audit for high-value contracts
## 📚 Resources
- [Trail of Bits - Building Secure Contracts](https://github.com/crytic/building-secure-contracts)
- [OpenZeppelin Contracts](https://docs.openzeppelin.com/contracts)
- [Consensys Best Practices](https://consensys.github.io/smart-contract-best-practices/)
- [SWC Registry](https://swcregistry.io/)
- [Slither](https://github.com/crytic/slither)
- [Foundry Book - Security](https://book.getfoundry.sh/forge/security)
---
*Security is not a feature, it's a requirement.* 🔐
FILE:analyzer.js
#!/usr/bin/env node
/**
* Solidity Guardian - Smart Contract Security Analyzer
* Detects vulnerabilities, suggests fixes, generates reports.
*/
const fs = require('fs');
const path = require('path');
// Vulnerability patterns with detection regex and fix suggestions
const VULNERABILITY_PATTERNS = {
// Critical
'SG-001': {
id: 'SG-001',
title: 'Reentrancy Vulnerability',
severity: 'critical',
pattern: /\.call\{.*value.*\}|\.transfer\(|\.send\(/,
antiPattern: /ReentrancyGuard|nonReentrant|_status\s*=/,
contextCheck: (lines, lineNum, content) => {
// Check if state is modified AFTER external call
const beforeCall = lines.slice(Math.max(0, lineNum - 5), lineNum).join('\n');
const afterCall = lines.slice(lineNum, Math.min(lines.length, lineNum + 5)).join('\n');
const stateModAfter = /\w+\s*[+\-*/]?=\s*[^=]/.test(afterCall) &&
!afterCall.includes('bool success');
return stateModAfter;
},
description: 'External call found before state update. Attacker can re-enter.',
suggestion: 'Use ReentrancyGuard and follow checks-effects-interactions pattern. Update state BEFORE external calls.',
references: ['https://swcregistry.io/docs/SWC-107']
},
'SG-002': {
id: 'SG-002',
title: 'Unprotected Selfdestruct',
severity: 'critical',
pattern: /selfdestruct\s*\(/,
antiPattern: /onlyOwner|require\s*\(\s*msg\.sender\s*==|onlyRole/,
description: 'selfdestruct without access control can destroy the contract.',
suggestion: 'Add onlyOwner modifier or access control. Consider removing selfdestruct entirely.',
references: ['https://swcregistry.io/docs/SWC-106']
},
'SG-003': {
id: 'SG-003',
title: 'Delegatecall to Untrusted Contract',
severity: 'critical',
pattern: /\.delegatecall\s*\(/,
contextCheck: (lines, lineNum, content) => {
const line = lines[lineNum];
// Check if target is from user input or storage that could be manipulated
return /delegatecall\([^)]*\(/.test(line) ||
lines.slice(Math.max(0, lineNum - 3), lineNum).some(l =>
/address.*=.*_|address.*=.*param|address.*=.*arg/.test(l));
},
description: 'Delegatecall with potentially untrusted target allows code execution in contract context.',
suggestion: 'Only delegatecall to trusted, immutable implementation addresses. Use OpenZeppelin proxy patterns.',
references: ['https://swcregistry.io/docs/SWC-112']
},
// High
'SG-010': {
id: 'SG-010',
title: 'Missing Access Control',
severity: 'high',
pattern: /function\s+\w+.*public|function\s+\w+.*external/,
contextCheck: (lines, lineNum, content) => {
const funcLine = lines[lineNum];
const funcBody = lines.slice(lineNum, Math.min(lines.length, lineNum + 10)).join('\n');
// Sensitive operations without access control
const sensitiveOps = /\.transfer\(|\.call\{|selfdestruct|suicide|delegatecall|_mint\(|_burn\(|withdraw/;
const hasAccessControl = /onlyOwner|onlyRole|require\s*\(\s*msg\.sender\s*==|_checkRole|hasRole/;
return sensitiveOps.test(funcBody) && !hasAccessControl.test(funcBody) &&
!funcLine.includes('view') && !funcLine.includes('pure');
},
description: 'Sensitive function lacks access control.',
suggestion: 'Add onlyOwner, onlyRole, or custom access control modifier.',
references: ['https://swcregistry.io/docs/SWC-105']
},
'SG-011': {
id: 'SG-011',
title: 'Unchecked ERC20 Transfer',
severity: 'high',
pattern: /\.transfer\s*\(|\.transferFrom\s*\(/,
antiPattern: /SafeERC20|safeTransfer|require\s*\([^)]*transfer/,
contextCheck: (lines, lineNum, content) => {
const line = lines[lineNum];
// Skip ETH transfers (address.transfer)
if (/payable\s*\(/.test(line)) return false;
// Check if it's likely an ERC20 call
return /\w+\.transfer\s*\(/.test(line) && !/msg\.sender\.transfer|payable\(/.test(line);
},
description: 'ERC20 transfer without checking return value. Some tokens return false instead of reverting.',
suggestion: 'Use SafeERC20.safeTransfer() from OpenZeppelin.',
references: ['https://swcregistry.io/docs/SWC-104']
},
'SG-012': {
id: 'SG-012',
title: 'Integer Overflow/Underflow',
severity: 'high',
pattern: /\+\+|\-\-|\+\s*=|\-\s*=|\*\s*=|\/\s*=/,
contextCheck: (lines, lineNum, content) => {
// Only flag for Solidity < 0.8.0
const pragmaMatch = content.match(/pragma\s+solidity\s+[\^~]?(0\.\d+)/);
if (pragmaMatch) {
const version = parseFloat(pragmaMatch[1]);
return version < 0.8;
}
return false;
},
antiPattern: /SafeMath|using\s+SafeMath/,
description: 'Arithmetic operation in Solidity < 0.8 without SafeMath.',
suggestion: 'Upgrade to Solidity 0.8+ or use SafeMath library.',
references: ['https://swcregistry.io/docs/SWC-101']
},
'SG-013': {
id: 'SG-013',
title: 'tx.origin Authentication',
severity: 'high',
pattern: /tx\.origin/,
contextCheck: (lines, lineNum, content) => {
const line = lines[lineNum];
// Flag if used for authentication
return /require.*tx\.origin|tx\.origin\s*==|==\s*tx\.origin/.test(line);
},
description: 'Using tx.origin for authentication is vulnerable to phishing attacks.',
suggestion: 'Use msg.sender instead of tx.origin for authentication.',
references: ['https://swcregistry.io/docs/SWC-115']
},
'SG-014': {
id: 'SG-014',
title: 'Weak Randomness',
severity: 'high',
pattern: /block\.timestamp|block\.number|blockhash/,
contextCheck: (lines, lineNum, content) => {
const context = lines.slice(Math.max(0, lineNum - 2), lineNum + 3).join('\n');
// Flag if used for randomness
return /random|rand|lottery|winner|select|pick|shuffle|keccak256.*block\./i.test(context);
},
description: 'Block variables are predictable. Miners can manipulate them.',
suggestion: 'Use Chainlink VRF or commit-reveal scheme for secure randomness.',
references: ['https://swcregistry.io/docs/SWC-120']
},
// Medium
'SG-020': {
id: 'SG-020',
title: 'Floating Pragma',
severity: 'medium',
pattern: /pragma\s+solidity\s+[\^~]/,
description: 'Floating pragma allows compilation with different compiler versions.',
suggestion: 'Pin to a specific version: pragma solidity 0.8.20;',
references: ['https://swcregistry.io/docs/SWC-103']
},
'SG-021': {
id: 'SG-021',
title: 'Missing Zero Address Check',
severity: 'medium',
pattern: /function\s+\w+.*address\s+\w+/,
contextCheck: (lines, lineNum, content) => {
const funcBody = lines.slice(lineNum, Math.min(lines.length, lineNum + 15)).join('\n');
const hasAddressParam = /address\s+_?\w+/.test(lines[lineNum]);
const checksZero = /require.*!=\s*address\s*\(\s*0\s*\)|require.*!=\s*0x0|_require.*Zero/.test(funcBody);
return hasAddressParam && !checksZero && /=\s*_?\w+;/.test(funcBody);
},
description: 'Address parameter assigned without zero address validation.',
suggestion: 'Add: require(_addr != address(0), "Zero address");',
references: []
},
'SG-022': {
id: 'SG-022',
title: 'Timestamp Dependence',
severity: 'medium',
pattern: /block\.timestamp/,
contextCheck: (lines, lineNum, content) => {
const context = lines.slice(lineNum, lineNum + 3).join('\n');
// Flag if used in conditions
return /if\s*\(.*block\.timestamp|require.*block\.timestamp|block\.timestamp\s*[<>=]/.test(context);
},
description: 'Logic depends on block.timestamp which can be manipulated by miners.',
suggestion: 'Use block.number for time-based logic or accept ~15 second variance.',
references: ['https://swcregistry.io/docs/SWC-116']
},
'SG-023': {
id: 'SG-023',
title: 'DoS with Revert in Loop',
severity: 'medium',
pattern: /for\s*\(|while\s*\(/,
contextCheck: (lines, lineNum, content) => {
const loopBody = lines.slice(lineNum, Math.min(lines.length, lineNum + 15)).join('\n');
// Check for external calls or transfers in loop
return /\.transfer\(|\.call\{|\.send\(|require\s*\(/.test(loopBody);
},
description: 'External call in loop can cause DoS if one call reverts.',
suggestion: 'Use pull-over-push pattern. Let users withdraw individually.',
references: ['https://swcregistry.io/docs/SWC-113']
},
// Low
'SG-030': {
id: 'SG-030',
title: 'Missing Event Emission',
severity: 'low',
pattern: /function\s+\w+.*public|function\s+\w+.*external/,
contextCheck: (lines, lineNum, content) => {
const funcLine = lines[lineNum];
if (funcLine.includes('view') || funcLine.includes('pure')) return false;
// Find function body
let braceCount = 0;
let started = false;
let funcBody = '';
for (let i = lineNum; i < Math.min(lines.length, lineNum + 30); i++) {
funcBody += lines[i] + '\n';
braceCount += (lines[i].match(/\{/g) || []).length;
braceCount -= (lines[i].match(/\}/g) || []).length;
if (braceCount > 0) started = true;
if (started && braceCount === 0) break;
}
// Check if function modifies state but has no emit
const modifiesState = /\w+\s*=\s*[^=]/.test(funcBody) && !/bool\s+success/.test(funcBody);
const hasEmit = /emit\s+\w+/.test(funcBody);
return modifiesState && !hasEmit;
},
description: 'State-changing function without event emission.',
suggestion: 'Emit events for all state changes to enable off-chain tracking.',
references: []
},
'SG-031': {
id: 'SG-031',
title: 'Magic Numbers',
severity: 'low',
pattern: /[=<>]\s*\d{3,}|\/\s*\d{2,}|\*\s*\d{2,}/,
antiPattern: /constant|immutable|10\*\*|1e\d/,
description: 'Hardcoded numeric values reduce readability.',
suggestion: 'Use named constants: uint256 constant FEE_DENOMINATOR = 10000;',
references: []
},
'SG-032': {
id: 'SG-032',
title: 'Implicit Visibility',
severity: 'low',
pattern: /^\s*function\s+\w+\s*\([^)]*\)\s*(?!public|private|internal|external)/m,
description: 'Function without explicit visibility specifier.',
suggestion: 'Always specify visibility: public, private, internal, or external.',
references: ['https://swcregistry.io/docs/SWC-100']
},
// Additional Critical Patterns
'SG-004': {
id: 'SG-004',
title: 'Uninitialized Storage Pointer',
severity: 'critical',
pattern: /\w+\s+storage\s+\w+\s*;/,
description: 'Uninitialized storage pointer can overwrite arbitrary storage slots.',
suggestion: 'Always initialize storage pointers or use memory keyword.',
references: ['https://swcregistry.io/docs/SWC-109']
},
'SG-005': {
id: 'SG-005',
title: 'Signature Replay',
severity: 'critical',
pattern: /ecrecover\s*\(/,
antiPattern: /nonce|deadline|block\.chainid|_useNonce/,
description: 'Signature verification without replay protection.',
suggestion: 'Include nonce, deadline, and chainId in signed message. Use EIP-712.',
references: ['https://swcregistry.io/docs/SWC-121']
},
'SG-006': {
id: 'SG-006',
title: 'Arbitrary Jump with Function Type',
severity: 'critical',
pattern: /function\s*\([^)]*\)\s*(internal|external|public|private)?\s*\w*\s*;/,
contextCheck: (lines, lineNum, content) => {
const context = lines.slice(lineNum, lineNum + 5).join('\n');
return /=\s*\w+\[/.test(context); // Assignment from array
},
description: 'Function type variable assigned from user input can lead to arbitrary code execution.',
suggestion: 'Validate function selector or use fixed function references.',
references: ['https://swcregistry.io/docs/SWC-127']
},
// Additional High Patterns
'SG-015': {
id: 'SG-015',
title: 'Unprotected Ether Withdrawal',
severity: 'high',
pattern: /\.transfer\s*\(|\.send\s*\(|\.call\{.*value/,
contextCheck: (lines, lineNum, content) => {
const funcStart = lines.slice(Math.max(0, lineNum - 15), lineNum).join('\n');
const hasAccessControl = /onlyOwner|onlyRole|require\s*\(\s*msg\.sender|_checkRole/.test(funcStart);
const isWithdraw = /withdraw|claim|redeem/i.test(funcStart);
return isWithdraw && !hasAccessControl;
},
description: 'Withdrawal function without access control or ownership check.',
suggestion: 'Add access control or ensure only rightful owner can withdraw.',
references: ['https://swcregistry.io/docs/SWC-105']
},
'SG-016': {
id: 'SG-016',
title: 'Unchecked Low-Level Call',
severity: 'high',
pattern: /\.call\s*\(|\.delegatecall\s*\(|\.staticcall\s*\(/,
antiPattern: /require\s*\(\s*success|if\s*\(\s*!?\s*success/,
contextCheck: (lines, lineNum, content) => {
const context = lines.slice(lineNum, lineNum + 3).join('\n');
return !/(bool\s+)?success.*=/.test(lines[lineNum]) ||
!/require\s*\(\s*success|if\s*\(\s*!?\s*success/.test(context);
},
description: 'Low-level call without checking return value.',
suggestion: 'Always check the success return value: (bool success, ) = addr.call(...); require(success);',
references: ['https://swcregistry.io/docs/SWC-104']
},
'SG-017': {
id: 'SG-017',
title: 'Dangerous Strict Equality',
severity: 'high',
pattern: /==\s*(address\s*\(\s*this\s*\)\s*\.)?balance|balance\s*==/,
description: 'Strict equality check on contract balance can be manipulated via selfdestruct.',
suggestion: 'Use >= or <= instead of == for balance checks.',
references: ['https://swcregistry.io/docs/SWC-132']
},
'SG-018': {
id: 'SG-018',
title: 'Use of Deprecated Functions',
severity: 'high',
pattern: /suicide\s*\(|block\.blockhash\s*\(|sha3\s*\(|callcode\s*\(|throw\s*;/,
description: 'Deprecated Solidity function used.',
suggestion: 'Replace: suicide→selfdestruct, block.blockhash→blockhash, sha3→keccak256, throw→revert().',
references: ['https://swcregistry.io/docs/SWC-111']
},
'SG-019': {
id: 'SG-019',
title: 'Incorrect Constructor Name',
severity: 'high',
pattern: /function\s+\w+\s*\([^)]*\)\s*(public|external)?\s*\{/,
contextCheck: (lines, lineNum, content) => {
// Check if function name matches contract name (old constructor style)
const contractMatch = content.match(/contract\s+(\w+)/);
if (!contractMatch) return false;
const contractName = contractMatch[1];
const funcMatch = lines[lineNum].match(/function\s+(\w+)/);
return funcMatch && funcMatch[1] === contractName;
},
description: 'Function has same name as contract (pre-0.4.22 constructor pattern).',
suggestion: 'Use constructor() instead of function ContractName().',
references: ['https://swcregistry.io/docs/SWC-118']
},
// Additional Medium Patterns
'SG-025': {
id: 'SG-025',
title: 'Shadowing State Variables',
severity: 'medium',
pattern: /function\s+\w+[^{]*\{/,
contextCheck: (lines, lineNum, content) => {
// Find state variables
const stateVars = content.match(/^\s*(uint|int|address|bool|bytes|string|mapping)\d*\s+(?:public|private|internal)?\s*(\w+)/gm);
if (!stateVars) return false;
const stateVarNames = stateVars.map(v => v.match(/(\w+)\s*[;=]/)?.[1]).filter(Boolean);
// Check function params
const funcLine = lines[lineNum];
const params = funcLine.match(/\(([^)]+)\)/)?.[1] || '';
return stateVarNames.some(name => params.includes(name));
},
description: 'Function parameter shadows state variable.',
suggestion: 'Use different names for parameters and state variables (prefix with _).',
references: ['https://swcregistry.io/docs/SWC-119']
},
'SG-026': {
id: 'SG-026',
title: 'Unbounded Loop',
severity: 'medium',
pattern: /for\s*\([^;]*;\s*\w+\s*<\s*\w+\.length/,
description: 'Loop iterates over dynamic array. Can exceed gas limit.',
suggestion: 'Limit iterations or use pagination. Consider pull-over-push pattern.',
references: ['https://swcregistry.io/docs/SWC-128']
},
'SG-027': {
id: 'SG-027',
title: 'Block Gas Limit DoS',
severity: 'medium',
pattern: /\.push\s*\(/,
contextCheck: (lines, lineNum, content) => {
// Check if pushing to array that's iterated
const context = lines.slice(Math.max(0, lineNum - 5), lineNum + 10).join('\n');
return /for\s*\([^)]*\.length/.test(context);
},
description: 'Array grows unbounded and is iterated. May exceed block gas limit.',
suggestion: 'Implement array size limits or pagination.',
references: ['https://swcregistry.io/docs/SWC-128']
},
'SG-028': {
id: 'SG-028',
title: 'State Change After External Call',
severity: 'medium',
pattern: /\.call\{|\.transfer\(|\.send\(/,
contextCheck: (lines, lineNum, content) => {
const afterCall = lines.slice(lineNum + 1, lineNum + 5).join('\n');
// Check for state changes (assignments to storage)
return /\w+\s*=\s*[^=]/.test(afterCall) && !/bool\s+success|success\s*=/.test(afterCall);
},
description: 'State modified after external call (potential reentrancy).',
suggestion: 'Move state changes before external calls (checks-effects-interactions).',
references: ['https://swcregistry.io/docs/SWC-107']
},
'SG-029': {
id: 'SG-029',
title: 'Assert Instead of Require',
severity: 'medium',
pattern: /assert\s*\(/,
contextCheck: (lines, lineNum, content) => {
const line = lines[lineNum];
// Assert should only be used for invariants, not input validation
return /assert\s*\(\s*\w+\s*[<>=!]/.test(line);
},
description: 'assert() used for input validation. Consumes all gas on failure.',
suggestion: 'Use require() for input validation, assert() only for invariants.',
references: ['https://swcregistry.io/docs/SWC-110']
},
// Additional Low Patterns
'SG-033': {
id: 'SG-033',
title: 'Missing Indexed Event Parameters',
severity: 'low',
pattern: /event\s+\w+\s*\([^)]*address[^)]*\)/,
antiPattern: /indexed\s+address|address\s+indexed/,
description: 'Address parameters in events should be indexed for efficient filtering.',
suggestion: 'Add indexed keyword: event Transfer(address indexed from, address indexed to, uint256 value);',
references: []
},
'SG-034': {
id: 'SG-034',
title: 'Public Function Could Be External',
severity: 'low',
pattern: /function\s+\w+[^{]*public[^{]*\{/,
contextCheck: (lines, lineNum, content) => {
const funcMatch = lines[lineNum].match(/function\s+(\w+)/);
if (!funcMatch) return false;
const funcName = funcMatch[1];
// Check if function is called internally
const internalCalls = content.match(new RegExp(`\\bfuncName\\s*\\(`, 'g')) || [];
return internalCalls.length <= 1; // Only the definition
},
description: 'Public function not called internally could be external (saves gas).',
suggestion: 'Change public to external if function is only called externally.',
references: []
},
'SG-035': {
id: 'SG-035',
title: 'Unused Return Value',
severity: 'low',
pattern: /\w+\.\w+\s*\([^)]*\)\s*;/,
contextCheck: (lines, lineNum, content) => {
const line = lines[lineNum];
// Check for common functions that return values
return /\.add\(|\.sub\(|\.mul\(|\.div\(|\.allowance\(|\.balanceOf\(/.test(line);
},
description: 'Return value of function call not used.',
suggestion: 'Check return value or explicitly ignore: (, ) = func();',
references: []
},
'SG-036': {
id: 'SG-036',
title: 'Use of Assembly',
severity: 'low',
pattern: /assembly\s*\{/,
description: 'Inline assembly bypasses Solidity safety checks.',
suggestion: 'Ensure assembly is thoroughly reviewed. Document why it is necessary.',
references: []
},
'SG-037': {
id: 'SG-037',
title: 'Gas-Inefficient Storage Reads',
severity: 'low',
pattern: /for\s*\([^{]*\{/,
contextCheck: (lines, lineNum, content) => {
const loopBody = lines.slice(lineNum, lineNum + 10).join('\n');
// Check for repeated storage access in loop
const storageAccess = loopBody.match(/\b(balances|allowances|_balances|_allowances|mapping)\b/g);
return storageAccess && storageAccess.length > 2;
},
description: 'Repeated storage reads in loop. Cache in memory for gas savings.',
suggestion: 'Cache storage values in memory variables before loop.',
references: []
},
'SG-038': {
id: 'SG-038',
title: 'Missing Error Messages',
severity: 'low',
pattern: /require\s*\([^,)]+\)\s*;/,
description: 'require() without error message makes debugging difficult.',
suggestion: 'Add error message: require(condition, "Descriptive error");',
references: []
},
'SG-039': {
id: 'SG-039',
title: 'Use Custom Errors',
severity: 'low',
pattern: /require\s*\([^)]+,\s*"[^"]+"\s*\)/,
contextCheck: (lines, lineNum, content) => {
// Only flag for Solidity >= 0.8.4
const pragmaMatch = content.match(/pragma\s+solidity\s+[\^~]?(0\.8\.(\d+))/);
if (pragmaMatch && parseInt(pragmaMatch[2]) >= 4) return true;
return false;
},
description: 'String error messages cost more gas than custom errors (Solidity 0.8.4+).',
suggestion: 'Use custom errors: error Unauthorized(); ... revert Unauthorized();',
references: []
},
'SG-040': {
id: 'SG-040',
title: 'Hardcoded Gas Amount',
severity: 'low',
pattern: /\.call\{[^}]*gas\s*:\s*\d+/,
description: 'Hardcoded gas values may break with EVM updates.',
suggestion: 'Avoid hardcoding gas. Let EVM determine gas or use gasleft().',
references: ['https://swcregistry.io/docs/SWC-134']
}
};
// Analyze a single Solidity file
function analyzeFile(filePath, options = {}) {
const content = fs.readFileSync(filePath, 'utf8');
const lines = content.split('\n');
const findings = [];
for (const [id, vuln] of Object.entries(VULNERABILITY_PATTERNS)) {
// Skip if severity filter doesn't match
if (options.severity && !options.severity.includes(vuln.severity)) continue;
// Check anti-pattern first (if file has protection, skip)
if (vuln.antiPattern && vuln.antiPattern.test(content)) continue;
// Find pattern matches
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Skip comments
if (line.trim().startsWith('//') || line.trim().startsWith('*')) continue;
if (vuln.pattern.test(line)) {
// Run context check if defined
if (vuln.contextCheck && !vuln.contextCheck(lines, i, content)) continue;
findings.push({
id: vuln.id,
title: vuln.title,
severity: vuln.severity,
file: filePath,
line: i + 1,
code: line.trim(),
description: vuln.description,
suggestion: vuln.suggestion,
references: vuln.references || []
});
}
}
}
return findings;
}
// Analyze a directory recursively
function analyzeDirectory(dirPath, options = {}) {
const findings = [];
function walk(dir) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
// Skip node_modules and common non-source directories
if (!['node_modules', '.git', 'artifacts', 'cache', 'out'].includes(file)) {
walk(fullPath);
}
} else if (file.endsWith('.sol')) {
findings.push(...analyzeFile(fullPath, options));
}
}
}
walk(dirPath);
return findings;
}
// Generate markdown report
function generateMarkdownReport(findings, projectPath) {
const now = new Date().toISOString().split('T')[0];
const bySeverity = {
critical: findings.filter(f => f.severity === 'critical'),
high: findings.filter(f => f.severity === 'high'),
medium: findings.filter(f => f.severity === 'medium'),
low: findings.filter(f => f.severity === 'low')
};
let report = `# Security Audit Report
**Project:** projectPath
**Date:** now
**Tool:** Solidity Guardian
## Summary
| Severity | Count |
|----------|-------|
| 🔴 Critical | bySeverity.critical.length |
| 🟠 High | bySeverity.high.length |
| 🟡 Medium | bySeverity.medium.length |
| 🔵 Low | bySeverity.low.length |
| **Total** | **findings.length** |
`;
for (const [severity, items] of Object.entries(bySeverity)) {
if (items.length === 0) continue;
const emoji = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' }[severity];
report += `## emoji severity.charAt(0).toUpperCase() + severity.slice(1) Findings
`;
for (const finding of items) {
report += `### finding.id: finding.title
**File:** \`finding.file\`
**Line:** finding.line
\`\`\`solidity
finding.code
\`\`\`
**Issue:** finding.description
**Recommendation:** finding.suggestion
** ${finding.references.join(', ')` : ''}
---
`;
}
}
return report;
}
// CLI
if (require.main === module) {
const args = process.argv.slice(2);
if (args.length === 0) {
console.log('Usage: node analyze.js <path> [--format json|markdown] [--severity critical,high,medium,low]');
process.exit(1);
}
const targetPath = args[0];
const formatIdx = args.indexOf('--format');
const format = formatIdx !== -1 ? args[formatIdx + 1] : 'table';
const sevIdx = args.indexOf('--severity');
const severity = sevIdx !== -1 ? args[sevIdx + 1].split(',') : null;
const options = { severity };
let findings;
const stat = fs.statSync(targetPath);
if (stat.isDirectory()) {
findings = analyzeDirectory(targetPath, options);
} else {
findings = analyzeFile(targetPath, options);
}
if (format === 'json') {
console.log(JSON.stringify(findings, null, 2));
} else if (format === 'markdown') {
console.log(generateMarkdownReport(findings, targetPath));
} else {
// Table format
const colors = { critical: '\x1b[31m', high: '\x1b[33m', medium: '\x1b[36m', low: '\x1b[37m' };
const reset = '\x1b[0m';
console.log(`\nSolidity Guardian Analysis: targetPath\n`);
console.log('='.repeat(80));
if (findings.length === 0) {
console.log('\n✅ No issues found!\n');
} else {
for (const f of findings) {
console.log(`colors[f.severity][f.severity.toUpperCase()]reset f.id: f.title`);
console.log(` 📁 f.file:f.line`);
console.log(` 💡 f.suggestion`);
console.log();
}
console.log('='.repeat(80));
console.log(`Total: findings.length issues found`);
console.log(` Critical: findings.filter(f => f.severity === 'critical').length`);
console.log(` High: findings.filter(f => f.severity === 'high').length`);
console.log(` Medium: findings.filter(f => f.severity === 'medium').length`);
console.log(` Low: findings.filter(f => f.severity === 'low').length`);
}
}
}
module.exports = { analyzeFile, analyzeDirectory, generateMarkdownReport, VULNERABILITY_PATTERNS };
FILE:package.json
{
"name": "solidity-guardian",
"version": "1.0.0",
"description": "Smart contract security analysis skill for OpenClaw agents",
"main": "analyzer.js",
"bin": {
"solidity-guardian": "./analyzer.js"
},
"scripts": {
"analyze": "node analyzer.js",
"test": "node analyzer.js test/fixtures/"
},
"keywords": [
"solidity",
"security",
"audit",
"smart-contracts",
"ethereum",
"openclaw",
"vulnerability",
"scanner"
],
"author": "Avi (aviclaw)",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/aviclaw/solidity-guardian"
}
}
FILE:slither-integration.js
#!/usr/bin/env node
/**
* Slither Integration for Solidity Guardian
* Runs Slither analysis and merges results with Guardian findings.
*/
const { execSync, spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
// Check if Slither is installed
function checkSlither() {
try {
execSync('slither --version', { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
// Install Slither
async function installSlither() {
console.log('Installing Slither...');
// Try different installation methods
const methods = [
'pipx install slither-analyzer',
'pip3 install slither-analyzer',
'python3 -m pip install slither-analyzer'
];
for (const method of methods) {
try {
execSync(method, { stdio: 'inherit' });
console.log('✅ Slither installed successfully');
return true;
} catch {
continue;
}
}
console.log('⚠️ Could not install Slither. Install manually:');
console.log(' pip3 install slither-analyzer');
console.log(' or: pipx install slither-analyzer');
return false;
}
// Map Slither severity to Guardian severity
function mapSeverity(slitherImpact) {
const mapping = {
'High': 'critical',
'Medium': 'high',
'Low': 'medium',
'Informational': 'low',
'Optimization': 'low'
};
return mapping[slitherImpact] || 'medium';
}
// Run Slither and get JSON output
function runSlither(projectPath, options = {}) {
if (!checkSlither()) {
console.log('Slither not found.');
if (options.autoInstall) {
if (!installSlither()) {
return null;
}
} else {
console.log('Run with --install-slither to auto-install, or install manually.');
return null;
}
}
const outputFile = path.join('/tmp', `slither-Date.now().json`);
try {
// Run Slither with JSON output
const cmd = `slither projectPath --json outputFile 2>/dev/null`;
execSync(cmd, {
stdio: 'pipe',
timeout: 300000, // 5 minute timeout
cwd: projectPath.includes('/') ? path.dirname(projectPath) : process.cwd()
});
} catch (e) {
// Slither exits with non-zero on findings, but still writes JSON
}
if (!fs.existsSync(outputFile)) {
console.log('Slither did not produce output. Check if project compiles.');
return null;
}
try {
const results = JSON.parse(fs.readFileSync(outputFile, 'utf8'));
fs.unlinkSync(outputFile); // Clean up
return results;
} catch (e) {
console.log('Failed to parse Slither output:', e.message);
return null;
}
}
// Convert Slither findings to Guardian format
function convertSlitherFindings(slitherResults) {
if (!slitherResults || !slitherResults.results || !slitherResults.results.detectors) {
return [];
}
return slitherResults.results.detectors.map((detector, index) => {
// Get first element location
const firstElement = detector.elements?.[0];
const sourceMapping = firstElement?.source_mapping;
return {
id: `SLITHER-detector.check`,
title: detector.check.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase()),
severity: mapSeverity(detector.impact),
file: sourceMapping?.filename || 'unknown',
line: sourceMapping?.lines?.[0] || 0,
code: detector.first_markdown_element || '',
description: detector.description,
suggestion: detector.markdown || 'Review and fix the identified issue.',
references: [`https://github.com/crytic/slither/wiki/Detector-Documentation#detector.check`],
source: 'slither',
confidence: detector.confidence
};
});
}
// Merge Slither findings with Guardian findings
function mergeFindings(guardianFindings, slitherFindings) {
const merged = [...guardianFindings];
// Add Slither findings, avoiding duplicates
for (const sf of slitherFindings) {
// Check for similar finding at same location
const isDuplicate = guardianFindings.some(gf =>
gf.file === sf.file &&
Math.abs(gf.line - sf.line) < 3 &&
gf.severity === sf.severity
);
if (!isDuplicate) {
merged.push(sf);
}
}
// Sort by severity then line number
const severityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
merged.sort((a, b) => {
const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
if (sevDiff !== 0) return sevDiff;
return a.line - b.line;
});
return merged;
}
// Generate combined report
function generateCombinedReport(projectPath, guardianFindings, slitherFindings) {
const merged = mergeFindings(guardianFindings, slitherFindings);
const now = new Date().toISOString().split('T')[0];
const guardianCount = guardianFindings.length;
const slitherCount = slitherFindings.length;
const uniqueSlither = merged.length - guardianCount;
let report = `# Combined Security Audit Report
**Project:** projectPath
**Date:** now
**Tools:** Solidity Guardian + Slither
## Analysis Summary
| Source | Findings |
|--------|----------|
| Solidity Guardian | guardianCount |
| Slither (unique) | uniqueSlither |
| **Total** | **merged.length** |
## Severity Breakdown
| Severity | Count |
|----------|-------|
| 🔴 Critical | merged.filter(f => f.severity === 'critical').length |
| 🟠 High | merged.filter(f => f.severity === 'high').length |
| 🟡 Medium | merged.filter(f => f.severity === 'medium').length |
| 🔵 Low | merged.filter(f => f.severity === 'low').length |
`;
// Group by severity
const bySeverity = {
critical: merged.filter(f => f.severity === 'critical'),
high: merged.filter(f => f.severity === 'high'),
medium: merged.filter(f => f.severity === 'medium'),
low: merged.filter(f => f.severity === 'low')
};
for (const [severity, items] of Object.entries(bySeverity)) {
if (items.length === 0) continue;
const emoji = { critical: '🔴', high: '🟠', medium: '🟡', low: '🔵' }[severity];
report += `## emoji severity.charAt(0).toUpperCase() + severity.slice(1) Findings\n\n`;
for (const finding of items) {
const source = finding.source === 'slither' ? ' [Slither]' : ' [Guardian]';
report += `### finding.id: finding.titlesource\n\n`;
report += `**File:** \`finding.file\`\n`;
report += `**Line:** finding.line\n`;
if (finding.confidence) {
report += `**Confidence:** finding.confidence\n`;
}
report += `\n**Issue:** finding.description\n\n`;
report += `**Recommendation:** finding.suggestion\n\n`;
if (finding.references && finding.references.length > 0) {
report += `**References:** finding.references.join(', ')\n`;
}
report += '\n---\n\n';
}
}
return report;
}
// CLI
async function main() {
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('--help')) {
console.log(`
Slither Integration for Solidity Guardian
Usage: node slither-integration.js <path> [options]
Options:
--install-slither Auto-install Slither if not found
--format <type> Output format: json, markdown (default: markdown)
--output <file> Write to file instead of stdout
--guardian-only Run Guardian analysis only (skip Slither)
--slither-only Run Slither analysis only (skip Guardian)
--help Show this help
Examples:
node slither-integration.js ./contracts/
node slither-integration.js . --format markdown --output AUDIT.md
node slither-integration.js ./src/ --install-slither
`);
process.exit(0);
}
const projectPath = args[0];
const autoInstall = args.includes('--install-slither');
const formatIdx = args.indexOf('--format');
const format = formatIdx !== -1 ? args[formatIdx + 1] : 'markdown';
const outputIdx = args.indexOf('--output');
const outputFile = outputIdx !== -1 ? args[outputIdx + 1] : null;
const guardianOnly = args.includes('--guardian-only');
const slitherOnly = args.includes('--slither-only');
// Import Guardian analyzer
const { analyzeDirectory, analyzeFile, generateMarkdownReport } = require('./analyzer');
let guardianFindings = [];
let slitherFindings = [];
// Run Guardian
if (!slitherOnly) {
console.log('Running Solidity Guardian analysis...');
const stat = fs.statSync(projectPath);
if (stat.isDirectory()) {
guardianFindings = analyzeDirectory(projectPath);
} else {
guardianFindings = analyzeFile(projectPath);
}
console.log(`Guardian found guardianFindings.length issues.`);
}
// Run Slither
if (!guardianOnly) {
console.log('Running Slither analysis...');
const slitherResults = runSlither(projectPath, { autoInstall });
if (slitherResults) {
slitherFindings = convertSlitherFindings(slitherResults);
console.log(`Slither found slitherFindings.length issues.`);
}
}
// Generate output
let output;
if (format === 'json') {
output = JSON.stringify({
guardian: guardianFindings,
slither: slitherFindings,
merged: mergeFindings(guardianFindings, slitherFindings)
}, null, 2);
} else {
output = generateCombinedReport(projectPath, guardianFindings, slitherFindings);
}
if (outputFile) {
fs.writeFileSync(outputFile, output);
console.log(`\n✅ Report written to outputFile`);
} else {
console.log('\n' + output);
}
}
main().catch(console.error);
module.exports = {
checkSlither,
installSlither,
runSlither,
convertSlitherFindings,
mergeFindings,
generateCombinedReport
};