Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Live crypto trading on Hyperliquid via Katbot.ai. Includes BMI market analysis, token selection, and AI-powered trade execution.
.claude/skills/katbot-trading/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-12 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-04 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
This skill teaches the agent how to use the Katbot.ai API to manage a Hyperliquid trading portfolio.
| Type | Description | |------|-------------| | HL_PAPER | Paper trading on Hyperliquid (no real funds). Formerly called PAPER. | | HYPERLIQUID | Live trading on Hyperliquid (agent key required, builder fee must be approved). |
btc_momentum.py: Calculates the BMI (BTC Momentum Index) based on trend, MACD, body, volume, and RSI. Returns a signal (BULLISH, BEARISH, NEUTRAL).bmi_alert.py: Runs btc_momentum.py and sends a Telegram alert if the market direction has changed. Uses portfolio_tokens.json for custom token tracking.All tool scripts live exclusively in {baseDir}/tools/ — this is the single canonical location. There are no copies elsewhere in the project. Always reference tools via {baseDir}/tools/<script> and set PYTHONPATH={baseDir}/tools so inter-tool imports resolve correctly.
Dependencies are listed in {baseDir}/requirements.txt.
ensure_env.sh: Run before any tool. Checks if dependencies are installed for the current skill version and re-installs if needed. Safe to call every time — it exits immediately if already up to date.katbot_onboard.py: First-time setup wizard. Authenticates via SIWE using your Wallet Key, creates/selects a portfolio, and saves credentials locally to the secure identity directory.katbot_client.py: Core API client. Handles authentication, token refresh, portfolio management, recommendations, trade execution, and chat. Also usable as a CLI script.katbot_workflow.py: End-to-end trading workflow (BMI -> token selection -> recommendation). Imports katbot_client and token_selector — requires PYTHONPATH={baseDir}/tools.token_selector.py: Momentum-based token selection via CoinGecko.btc_momentum.py: Calculates BTC Momentum Index (BMI).bmi_alert.py: Telegram alerting workflow for BMI changes.The BMI (BTC Momentum Index) is a proprietary indicator used to determine market bias.
PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/btc_momentum.py --jsonOPENCLAW_NOTIFY_CHANNEL=<channel> OPENCLAW_NOTIFY_TARGET=<target> PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/btc_momentum.py --sendOPENCLAW_NOTIFY_CHANNEL=<channel> OPENCLAW_NOTIFY_TARGET=<target> PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/bmi_alert.py (sends an alert if market direction changed)OPENCLAW_NOTIFY_CHANNEL or OPENCLAW_NOTIFY_TARGET is not set, the --send flag and bmi_alert.py will print the message to stdout instead of sending it.The bmi_alert.py script reads ~/.openclaw/workspace/portfolio_tokens.json to include specific token performance in the alert message.
> Note for contributors: The scripts/ directory contains only publish tooling (publish.sh, publish.py, etc.). Do NOT add copies of tool scripts there — all trading logic lives solely in {baseDir}/tools/.
Normal operation requires only KATBOT_HL_AGENT_PRIVATE_KEY. The skill reads this from katbot_secrets.json automatically after onboarding, so it does not need to be set in the environment at all during day-to-day use.
WALLET_PRIVATE_KEY is not a runtime requirement. It is an emergency fallback used only when both the access token and refresh token have expired and the session must be fully re-established. It must never be pre-set in the environment — supply it interactively only when onboarding or re-onboarding explicitly requires it.
| Variable | When needed | Description | |----------|-------------|-------------| | KATBOT_HL_AGENT_PRIVATE_KEY | First run only (if not yet onboarded) | Agent private key for Hyperliquid trade execution. Onboarding saves it to katbot_secrets.json (mode 600). After that the skill loads it from the secrets file automatically — no env var needed for daily operation. | | WALLET_PRIVATE_KEY | Emergency re-auth only | MetaMask wallet key. Used only for SIWE login when session tokens are fully expired. Never pre-set this in the environment. Never export to a shell profile. Provide interactively only when re-onboarding is required. | | KATBOT_BASE_URL | Optional override | API base URL. Default: https://api.katbot.ai | | KATBOT_IDENTITY_DIR | Optional override | Path to identity files directory. Default: ~/.openclaw/workspace/katbot-identity | | CHAIN_ID | Optional override | EVM chain ID. Default: 42161 (Arbitrum) | | OPENCLAW_NOTIFY_CHANNEL | Required for alerting | The openclaw channel name for btc_momentum.py --send and bmi_alert.py (e.g. telegram, slack, discord). If unset, both tools print to stdout and skip the send. | | OPENCLAW_NOTIFY_TARGET | Required for alerting | The target ID within the channel (e.g. a chat ID or user handle). Must be set together with OPENCLAW_NOTIFY_CHANNEL. |
.env File Loader — CLI/Development Use Onlykatbot_client.py contains a .env file loader for CLI use outside OpenClaw (tubman-bobtail-py mode). At import time it searches these paths for a katbot_client.env file:
{projectRoot}/env/local/katbot_client.env{baseDir}/../env/local/katbot_client.env{baseDir}/tools/katbot_client.envIf a file is found, it loads only non-secret config from it: KATBOT_BASE_URL, KATBOT_IDENTITY_DIR, and CHAIN_ID. Private keys (WALLET_PRIVATE_KEY and KATBOT_HL_AGENT_PRIVATE_KEY) are explicitly not read from .env files — they must come from the environment or the identity directory only.
Agent rules:
katbot_client.env containing private keys.WALLET_PRIVATE_KEY or KATBOT_HL_AGENT_PRIVATE_KEY in any .env file.katbot_client.env is acceptable only for non-secret config (KATBOT_BASE_URL, CHAIN_ID, KATBOT_IDENTITY_DIR, PORTFOLIO_ID, WALLET_ADDRESS).All persistent credentials are stored in KATBOT_IDENTITY_DIR (default: ~/.openclaw/workspace/katbot-identity/). This directory is outside the project tree deliberately — its contents are never committed to git.
| File | Mode | Contents | |------|------|----------| | katbot_config.json | 644 | base_url, wallet_address, portfolio_id, portfolio_name, chain_id | | katbot_token.json | 600 | access_token, refresh_token | | katbot_secrets.json | 600 | agent_private_key |
katbot_client.py reads all three files automatically. The agent key is loaded from katbot_secrets.json if KATBOT_HL_AGENT_PRIVATE_KEY is not set in the environment.
Security properties of identity files:
katbot_token.json and katbot_secrets.json are written with mode 600 (owner read/write only).WALLET_PRIVATE_KEY (MetaMask key) is never written to any identity file — it is used only in-memory during onboarding and authentication.~/.openclaw/workspace/katbot-identity/ is compromised, an attacker gains the agent trading key and session tokens but not the MetaMask wallet key, limiting the blast radius to funds accessible via the Hyperliquid agent wallet.The skill manages tokens automatically via katbot_client.get_token(). Never call this manually — all API functions call it internally.
exp claim from katbot_token.json. If valid (not expiring within 60s), use it directly.POST /refresh with {"refresh_token": "<token>"}. The API rotates the refresh token on every call — both the new access_token and new refresh_token are written to katbot_token.json (mode 600) immediately. The old refresh token is invalid as soon as the response arrives./refresh call fails, fall back to full SIWE re-authentication via POST /login. This requires WALLET_PRIVATE_KEY.Never call /login if /refresh can succeed first.
Refresh tokens are opaque (not JWTs) and expire after 7 days of inactivity. If the session is fully expired, re-run onboarding.
> The agent must present this notice to the user and obtain acknowledgement before running onboarding or any trading operation for the first time.
This skill transmits the agent trading private key (KATBOT_HL_AGENT_PRIVATE_KEY) to the remote Katbot API (api.katbot.ai) on certain calls. This is required for the API to sign and submit trades on-chain via Hyperliquid.
What leaves your machine and when:
| Credential | Sent to | On which calls | Why | |------------|---------|----------------|-----| | KATBOT_HL_AGENT_PRIVATE_KEY | api.katbot.ai | request_recommendation, execute_recommendation, close_position, get_portfolio_timeseries — in the X-Agent-Private-Key header and/or the JSON request body | The Katbot API uses it to sign Hyperliquid on-chain transactions on your behalf | | access_token / refresh_token | api.katbot.ai | All authenticated API calls — in the Authorization: Bearer header | Session authentication | | WALLET_PRIVATE_KEY | Never sent remotely | Used only locally to sign the SIWE message during onboarding/re-auth | Signature is computed locally; only the resulting signature is sent |
What this means:
WALLET_PRIVATE_KEY) is never transmitted — it signs a message locally and the signature alone is sent.api.katbot.ai with your agent trading key, do not use this skill.Agent instruction: Before running onboarding or any recommendation/execution call for the first time in a session, inform the user: "This skill will send your Hyperliquid agent private key to api.katbot.ai to authorize on-chain trades. Do you want to proceed?" Do not proceed without affirmative confirmation.
katbot_client.py API ReferenceAll functions require a token argument obtained from get_token().
Key transmission legend used below:
[key→remote] — the agent private key is sent to api.katbot.ai in this call[local only] — no private key is transmitted; only the JWT bearer token is sent[local only]pythontoken = get_token() # Returns valid access token (refreshes automatically) config = get_config() # Returns dict from katbot_config.json
[local only unless noted]pythonportfolios = list_portfolios(token) # Create a paper portfolio (HL_PAPER) or live portfolio (HYPERLIQUID) portfolio = create_portfolio( token, name, portfolio_type="HL_PAPER", # "HL_PAPER" or "HYPERLIQUID" (was "PAPER") is_testnet=True, # Always True for safety; set False for mainnet amount=10000.0, # Initial USD balance (paper only, ignored for HYPERLIQUID) primary_agent_id=None, # Optional: assign an agent immediately on creation arbitrum_rpc_url=None, # Optional: custom Arbitrum RPC URL ) portfolio = get_portfolio(token, portfolio_id, require_agent=True) # Returns full PortfolioInfo: positions, PnL, risk metrics, active_agent, etc. # require_agent=False for paper portfolios that don't need the agent key. updated = update_portfolio(token, portfolio_id, name=None, tokens_selected=["BTC","ETH","SOL"], max_history_messages=None) result = delete_portfolio(token, portfolio_id) # Portfolio metadata tokens = get_portfolio_tokens(token, portfolio_id) # ["BTC","ETH","SOL",...] chain_info = get_portfolio_chain_info(token, portfolio_id) # {chain_id, is_testnet, network_name} # Timeseries data [key→remote for HYPERLIQUID] ts = get_portfolio_timeseries( token, portfolio_id, granularity="1h", # "1m","5m","15m","1h","4h","1d","1w","1M" limit=100, window="24H" # "24H","7D","30D" ) # Hyperliquid-specific result = validate_hyperliquid(token, agent_private_key=None, is_testnet=True) result = approve_builder_fee(token, portfolio_id, action, signature, nonce) # Note: approve_builder_fee is called after the frontend signs EIP-712; not a routine agent call.
[key→remote]> The agent private key is sent in the JSON body of request_recommendation. > A primary agent must be assigned to the portfolio before calling this — the API returns HTTP 422 otherwise. > Confirm user consent before calling.
pythonticket = request_recommendation( token, portfolio_id, message, agent_id=None # Optional: select a specific agent; uses portfolio primary if None ) # ticket = {"ticket_id": "...", "status": "PENDING"} result = poll_recommendation(token, ticket["ticket_id"], max_wait=60) # result = {"ticket_id": "...", "status": "COMPLETED"|"FAILED", "done": True, "response": {...}, "error": None} recs = get_recommendations(token, portfolio_id) # List saved recommendations
python# Submit another agent's recommendation for analysis ticket = submit_recommendation_response( token, portfolio_id, recommendation={ "agent_name": "...", "symbol": "ETH", "action": "BUY", "confidence": 0.8, "entry_price": 3000.0, "take_profit_pct": 5.0, "stop_loss_pct": 2.0, "rationale": "...", "katbot_portfolio_id": portfolio_id }, pack_goals=None, # Optional katpack goals string ) result = poll_recommendation_response(token, ticket["ticket_id"], max_wait=60)
[key→remote]> Always require explicit user confirmation before calling execute_recommendation or close_position.
python# Execute a saved recommendation [key→remote] result = execute_recommendation( token, portfolio_id, rec_id, execute_onchain=False, # True to submit to Hyperliquid on-chain user_master_address=None # Optional: override wallet address ) # Close an open position [key→remote] result = close_position( token, portfolio_id, "ETH", reason="Manual close via agent", # Optional reason string execute_onchain=False, user_master_address=None ) # List trades and events [local only] trades = list_trades(token, portfolio_id) events = get_position_events(token, portfolio_id, limit=20, event_type=None) # event_type: "TP_HIT" | "SL_HIT" | "LIQUIDATED" | "MANUAL_CLOSE"
[local only]> A portfolio must have a primary agent assigned before request_recommendation will succeed.
python# CRUD agents = list_agents(token) agent = create_agent(token, name, max_history_messages=10) # name must be a slug: lowercase letters/numbers/hyphens; server appends a 6-char suffix agent = get_agent(token, agent_id) agent = update_agent(token, agent_id, name=None, max_history_messages=None, avatar_seed=None) result = delete_agent(token, agent_id) # fails if agent is primary on any portfolio # Search (cross-user, for invite flow — min 3 chars) results = search_agents(token, q="my-agent", portfolio_id=None) # Portfolio assignments assignment = get_portfolio_agent(token, portfolio_id) # Get active primary agent assignments = list_portfolio_agents(token, portfolio_id) # All agents (primary + observers) assignment = assign_agent(token, portfolio_id, agent_id, role="primary") result = unassign_agent(token, portfolio_id, agent_id)
[local only]pythoninvite = create_agent_invitation(token, portfolio_id, agent_id) invites = list_portfolio_invitations(token, portfolio_id) pending = list_pending_invitations(token) # Invitations for agents you own result = respond_to_invitation(token, agent_id, invitation_id, action="accepted") # action: "accepted" or "rejected" observed = list_observer_portfolios(token) # Portfolios you observe via accepted invite
[local only]pythonhistory = get_conversation(token, portfolio_id) # {"exists": True, "message_count": N, "conversation": [...], ...} result = delete_conversation(token, portfolio_id) # Clear history, preserve portfolio state
[local only]pythonuser = get_user(token) # {sub, id, is_whitelisted, subscription, plan, feature_usage} plans = get_plans() # No auth required; list of subscription plans
katbot_client.py can be run as a standalone script (reads PORTFOLIO_ID from .env or environment):
bashPYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py portfolio-state PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py recommendations PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py request-recommendation "Analyze and recommend" PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py poll-recommendation <ticket_id> PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py execute <rec_id> PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py close-position ETH PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py list-agents PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py get-agent <agent_id> PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py list-portfolio-agents PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py assign-agent <agent_id> [--role primary|observer] PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py conversation PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py clear-conversation PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py user PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py plans PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py tokens PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py chain-info PYTHONPATH={baseDir}/tools python3 {baseDir}/tools/katbot_client.py update-portfolio --tokens BTC,ETH,SOL [--name "New Name"]
get_token() handle token refresh automatically — do not manually manage tokens.request_recommendation. If the API returns HTTP 422 ("No primary agent assigned"), guide the user to create an agent and call assign_agent() first."PAPER" — it has been renamed to "HL_PAPER". Always use "HL_PAPER" for paper trading.builder_fee_approved is True in the portfolio info. If it is False, inform the user they must complete the builder fee approval step.WALLET_PRIVATE_KEY in the environment. It is an emergency re-auth key only. If the agent detects it already set in the environment outside of an active onboarding/re-auth session, warn the user and suggest unsetting it.katbot_client.env file containing WALLET_PRIVATE_KEY or KATBOT_HL_AGENT_PRIVATE_KEY. The .env loader will not inject private keys into the process, but placing them in such a file is still a bad practice that stores secrets on disk unnecessarily.katbot_token.json, katbot_secrets.json, or any file in the identity directory.This skill tracks its installed dependency version using a stamp file at {baseDir}/.installed_version. When the skill is upgraded, the stamp version will not match the skill version, and ensure_env.sh will automatically re-run pip install.
The agent MUST run ensure_env.sh before every tool invocation:
bashbash {baseDir}/tools/ensure_env.sh {baseDir}
pip install -r requirements.txt and writes the new stamp.python3 is missing: prints a clear error and exits with code 1.If a tool fails with ImportError or ModuleNotFoundError, always run ensure_env.sh first to sync dependencies before retrying.
bash# 1. Install dependencies bash {baseDir}/tools/ensure_env.sh {baseDir} # 2. Run onboarding wizard (interactive) python3 {baseDir}/tools/katbot_onboard.py
The wizard will:
WALLET_PRIVATE_KEY (hidden input — never stored to disk).KATBOT_HL_AGENT_PRIVATE_KEY, katbot_config.json, and katbot_token.json to ~/.openclaw/workspace/katbot-identity/.After onboarding, the skill runs autonomously using the saved credentials. WALLET_PRIVATE_KEY is no longer needed unless the session fully expires.
When the skill is updated (new version published to clawhub):
bash# Re-run ensure_env.sh — it detects the version change and re-installs dependencies bash {baseDir}/tools/ensure_env.sh {baseDir}
No re-onboarding is needed for upgrades. The identity files in ~/.openclaw/workspace/katbot-identity/ are preserved across upgrades. If a tool fails after upgrade, run ensure_env.sh first.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 20 counted toward the lift figure. The other 2 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +64 percentage points is the difference between those two pass rates over the 20 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.