Install
openclaw skills install earn-hunterAutomatically monitors OKX Flash Earn, Fixed Earn and Flexible Earn opportunities, sends push notifications, and guides subscription. 自动监控 OKX 闪赚、定期和活期赚币机会,推送通知并引导申购。Use when user says: 有闪赚通知我, 监控赚币, monitor earn, notify me about earn, 定时检查理财, 执行 earn-hunter 扫描, earn-hunter scan, 活期年化高了通知我, 监控活期.
openclaw skills install earn-hunterAutomated monitor for OKX Flash Earn, Fixed Earn, and Flexible Earn (Simple Earn) opportunities.
{baseDir} = the directory containing this SKILL.md file. All relative paths (references/, templates/, config/) are resolved from here.
Verify okx CLI installed: which okx. If missing, install via npm install -g @okx_ai/okx-trade-cli.
On OpenClaw, also verify the in-session cron tool is available in the agent tool list (used for scheduling — not the openclaw CLI).
Check optional dependent skills:
okx skill list --json
Optional skills (not required for scanning/notifications):
okx-cex-earn — needed for purchase guide (subscription execution)okx-cex-auth — needed for authentication recoveryIf either is missing, attempt to install but do not block if installation fails:
okx skill add okx-cex-earn
okx skill add okx-cex-auth
{skill_name} 安装失败,扫描和通知功能不受影响。申购引导和认证恢复需要该 skill,后续可手动安装。"Auth mode detection — run both, first match wins:
okx config show --json → has non-empty api_key field → API Key mode. Add --profile live to all commands.okx auth status --json → "status":"logged_in" → OAuth mode. No --profile flag needed.okx-cex-auth skill and follow login steps.Init config and state:
~/.okx/earn-hunter/ directory does not exist → mkdir -p ~/.okx/earn-hunter~/.okx/earn-hunter/config.json does not exist → copy {baseDir}/config/default.json to it~/.okx/earn-hunter/state.json does not exist → write {"flash":{},"fixed":{},"flexible":{},"consecutive_failures":0,"last_error":""}~/.okx/earn-hunter/platform.json does not exist → run Platform Detectioncp {baseDir}/scripts/scan.sh ~/.okx/earn-hunter/scan.sh && chmod +x ~/.okx/earn-hunter/scan.sh. This is the script cron and interactive scans both call.~/.okx/earn-hunter/env.snapshot with resolved tool paths (cron cannot rely on the user's login PATH):
cat > ~/.okx/earn-hunter/env.snapshot << SNAP
# auto-generated by earn-hunter activation — $(date -Iseconds)
OKX_BIN=$(command -v okx)
NODE_BIN=$(command -v node)
JQ_BIN=$(command -v jq)
ACTIVATION_PATH=$PATH
SNAP
The scan script sources this file to resolve tool paths even under cron's minimal PATH=/usr/bin:/bin.Config/state/platform JSON read-write done by the agent is only for activation/config management. The recurring scan itself is performed entirely by scripts/scan.sh (shell + jq) — jq is required for scanning. Verify with which jq; if missing, install (brew install jq / apt-get install jq).
Three independent dimensions: platform (where the agent runs), scheduler (what triggers scans), notification channel (where alerts go).
First run (no platform.json exists):
OPENCLAW_HOME env var exists? → hint: OpenClawcron / delivery tools? → hint: OpenClawHERMES_HOME env var exists or which hermes succeeds? → hint: Hermes Agent{baseDir}/config/<confirmed_platform>.default.json to ~/.okx/earn-hunter/platform.json{baseDir}/config/claude-code.default.json as base, set .platform to "hermes", .scheduler.type to "cron"{baseDir}/config/claude-code.default.json as base, set .platform to "generic", .scheduler.type to "manual"~/.okx/earn-hunter/platform.json, subsequent runs skip detection.Subsequent runs (platform.json exists):
Read ~/.okx/earn-hunter/platform.json and extract the .platform field (returns "openclaw", "claude-code", "hermes", or "generic").
No scheduler available on detected platform (only applies to platforms that should have one but don't) → error: "当前客户端不支持定时任务,请升级到最新版本。" Generic platform → no automatic scheduler. Inform: "当前平台不支持自动调度,你可以手动说'执行 earn-hunter 扫描'来触发。"
| File | Scope | Content |
|---|---|---|
config.json | Shared | Scan scope (flash/fixed/flexible), currencies, APY thresholds, terms, language, verboseLog |
platform.json | Platform-specific | Scheduler type/interval, notification channel, TG/Lark credentials |
state.json | Shared | Dedup state |
Core config (config.json) is identical across platforms. Platform config (platform.json) differs — the scheduler.type field determines how scans are triggered:
OpenClaw (openclaw.default.json):
"openclaw-cron" — scheduled via the in-session cron agent tool (no OS crontab, no CLI commands). The job runs as an isolated, light-context agent turn and delivers its output back to the conversation channel via cron announce delivery. notify.channel defaults to "session" so the scan prints to stdout for announce to push (avoids double-send).Claude Code / Hermes / Generic (claude-code.default.json):
"cron" — scheduled via OS crontab → scripts/scan.sh (zero LLM token cost), notification via TG / Lark curl from the script itself.Detect in priority order (PRD requirement: TG first):
$TELEGRAM_BOT_TOKEN and $TELEGRAM_CHAT_ID both set → TG readyplatform.notify.lark_webhook non-empty → Lark readyTG and Lark are standalone push channels — they work regardless of whether the agent client is open. On OS-crontab platforms, scheduled scans send notifications via direct curl. On OpenClaw, the scheduled scan runs in an isolated cron agent turn and delivers via cron announce to the conversation channel (channel = "session"); TG/Lark curl is not used unless the user explicitly switches the channel.
| User intent | Route |
|---|---|
| "有闪赚通知我" / "monitor earn" / "帮我监控赚币" / "活期年化高了通知我" | → Activation Flow |
| "改 APY 阈值" / "只看 USDT" / "change config" / "活期加上 BTC" | → Config Management |
| "申购 USDT 定期 7D" / "subscribe" / "我要买" | → Purchase Guide |
| "执行 earn-hunter 扫描" (cron OR interactive) | → Scan Cycle — run scripts/scan.sh and relay its output |
| "停止监控" / "暂停" / "stop" | → Pause/Resume |
| "卸载 earn-hunter" / "uninstall" | → Uninstall |
| "测试 earn-hunter" / "smoke test" / "测试定时任务" | → Test Mode |
First-time setup. Only confirm platform — everything else uses smart defaults.
See Platform Detection. Probe environment → ask user to confirm → write platform.json.
Must actively check available channels before proceeding. Do NOT silently fall back to session.
On OS-crontab platforms, scheduled notifications go out via direct curl; on OpenClaw they go out via cron announce to the conversation. Detection order (check each, report status for all):
$TELEGRAM_BOT_TOKEN and $TELEGRAM_CHAT_ID env vars:
platform.notify.lark_webhook or Lark MCP tools:
https:// and contains /hook/) → Lark readyhttps:// or missing /hook/) → warn: "Lark webhook 格式无效,跳过 Lark" → continue to next channelAlways ask the user to confirm notification channel — never silently default to session. For a monitoring tool, notification is critical; defaulting to session means alerts are lost when the user is not in the conversation.
If one or more external channels detected:
"检测到以下推送渠道可用:
你希望通知发到哪里?
If no external channel detected:
"新机会才能推送到你手上。你希望通知发到哪里?
推荐配置 Telegram 或 Lark,这样即使不在对话中也能收到提醒。"
TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID env varshttps://, contains /hook/), write to platform.notify.lark_webhook"session" and warn: "⚠ 离线状态下不会收到通知,建议后续配置外部渠道。"Write confirmed channel to platform.json notify.channel.
Present default config and ask user to confirm or customize. Each step offers a default — user can press enter to accept.
Step 1/3 — 扫描范围: "扫描范围(可多选): [1] Flash Earn(闪赚) [2] Fixed Earn(定期赚币) [3] Flexible Earn(活期赚币) 默认:全选"
config.fixed.enabled = false, config.flexible.enabled = false; skip Step 2/3 and 3/3config.flash.enabled = false, config.fixed.enabled = false; go to flexible-specific config (Step 2/3 asks flexible currencies, Step 3/3 asks flexible APY threshold)Step 2/3 — 监控币种: For Fixed Earn: "定期监控币种:全部(默认,按回车)或输入指定币种(如 USDT, SOL)"
"all" (all currencies)config.currencies to array (e.g. ["USDT", "SOL"])For Flexible Earn: "活期监控币种:USDT, USDC(默认,按回车)或输入指定币种"
["USDT", "USDC"]config.flexible.currencies to arrayStep 3/3 — APY 阈值: For Fixed Earn: "定期最低 APY 阈值:不限(默认,按回车)或输入百分比(如 8)"
0 (no limit)config.fixed.globalMinApy to value / 100 (e.g. 8 → 0.08)For Flexible Earn: "活期最低 APY 阈值:8%(默认,按回车)或输入百分比"
0.08 (8%)config.flexible.globalMinApy to value / 100Auto-detect language from conversation and write to config.json notify.language.
Write config to ~/.okx/earn-hunter/config.json.
Display summary using {baseDir}/templates/activation.md template (in user's language).
Smoke test always sends a notification, regardless of verboseLog setting.
verboseLog forced on so output is always produced, even when there are no opportunities. Temporarily flip verboseLog, run the script, then restore it — and use EH_TEST_NAMESPACE=1 so smoke-test dedup keys go under the test: prefix and don't pollute production state:
jq '.verboseLog=true' ~/.okx/earn-hunter/config.json > ~/.okx/earn-hunter/config.tmp \
&& mv ~/.okx/earn-hunter/config.tmp ~/.okx/earn-hunter/config.json
EH_TEST_NAMESPACE=1 OKX_PROFILE=live ~/.okx/earn-hunter/scan.sh
# then restore verboseLog to the user's original value (e.g. false)
verboseLog forced on, the script sends the brief status. Optionally append the activation confirmation message:
"Earn Hunter 已激活,当前暂无新机会,将在下一轮自动扫描。"
(Use {baseDir}/templates/activation.md as base, append the no-opportunity note)notify-channels.md)Note: The smoke test ignores verboseLog setting — it always produces output to verify the full pipeline works end-to-end.
Critical: The user's interactive shell has a full PATH, but cron does not. Run a second smoke test simulating cron's minimal environment to verify env.snapshot works:
env -i HOME="$HOME" PATH=/usr/bin:/bin \
EH_TEST_NAMESPACE=1 OKX_PROFILE=live \
bash ~/.okx/earn-hunter/scan.sh
env.snapshot is incomplete or missing. Re-run Preflight step to regenerate it. Do NOT proceed to Step 5 — the cron job will fail silently.The scheduling mechanism depends on platform.json .scheduler.type. Branch on the platform.
scheduler.type = "openclaw-cron")On OpenClaw, scheduling is done inside the conversation by calling the in-session cron agent tool — never an OS command or openclaw cron CLI (the CLI path has permission issues in this context). Encourage the user to set it up right here in the chat: the cron job you create inherits the current session's channel, so its scan output is delivered straight back to this conversation.
Call the cron tool with action: "add" and a job shaped like this (read .scheduler.interval from platform.json for the frequency):
name: "earn-hunter-hourly"schedule: { "kind": "every", "everyMs": 3600000 } — derive everyMs from scheduler.interval ("1h" → 3600000, "30m" → 1800000, "2h" → 7200000)sessionTarget: "isolated" — run in an isolated session, not the main onepayload: { "kind": "agentTurn", "message": "执行 earn-hunter 扫描", "lightContext": true }
lightContext: true runs the turn with a lightweight bootstrap context (skips workspace bootstrap files) → lower token cost per tick.--tools exec,read,write flag no longer exists). The scan stays cheap because it runs the okx CLI through exec and does not depend on the 160+ okx MCP tools — so as long as the isolated cron agent isn't configured to load the okx MCP server, only the regular tools (exec/read/write) are in play. Which tools load is governed by the agent's config, not by this job.delivery: { "mode": "announce" } — pushes the turn's output back to the conversation channel that created the job.When it fires, the isolated agent runs the prompt "执行 earn-hunter 扫描" → Scan Cycle (which runs scripts/scan.sh with channel session/stdout) → relays the result → announce delivers it here. Any new opportunity is therefore sent automatically.
Do NOT emit any shell/openclaw cron CLI command in the conversation — drive scheduling only through the cron tool.
scheduler.type = "cron" or "launchagent" — Claude Code / Hermes / Generic)These use OS scheduler → scripts/scan.sh. The script does everything (CLI calls, filter, dedup, render, curl notifications) with zero LLM cost.
Install the script — copy the skill's scripts/scan.sh into the state dir so the scheduler has a stable path:
mkdir -p ~/.okx/earn-hunter
cp {baseDir}/scripts/scan.sh ~/.okx/earn-hunter/scan.sh
chmod +x ~/.okx/earn-hunter/scan.sh
Set up scheduler — try crontab first, fallback to LaunchAgent on macOS if cron daemon is not running:
# Resolve tool directories from the current shell
NODE_DIR=$(dirname "$(command -v node)")
OKX_DIR=$(dirname "$(command -v okx)")
JQ_DIR=$(dirname "$(command -v jq)")
CRON_PATH=$(printf '%s\n' "$NODE_DIR" "$OKX_DIR" "$JQ_DIR" /usr/bin /bin | awk '!seen[$0]++' | paste -sd: -)
Step A: Try crontab + verify cron daemon (macOS)
(crontab -l 2>/dev/null; echo "0 * * * * PATH=$CRON_PATH OKX_PROFILE=live ~/.okx/earn-hunter/scan.sh >> ~/.okx/earn-hunter/cron.log 2>&1") | crontab -
On macOS (uname -s == Darwin), immediately check if the cron daemon is running:
if [[ "$(uname -s)" == "Darwin" ]] && ! launchctl list com.vix.cron >/dev/null 2>&1; then
# cron daemon not running — fallback to LaunchAgent
fi
scheduler.type = "cron".scheduler.type = "cron".Step B: macOS LaunchAgent fallback (scheduler.type = "launchagent")
Generate ~/Library/LaunchAgents/com.okx.earn-hunter.plist with the resolved paths:
SCAN_SCRIPT="$HOME/.okx/earn-hunter/scan.sh"
LOG_FILE="$HOME/.okx/earn-hunter/cron.log"
INTERVAL=3600 # derive from scheduler.interval: "1h"→3600, "30m"→1800, "10m"→600
cat > ~/Library/LaunchAgents/com.okx.earn-hunter.plist << PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.okx.earn-hunter</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>${SCAN_SCRIPT}</string>
</array>
<key>StartInterval</key>
<integer>${INTERVAL}</integer>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>${CRON_PATH}</string>
<key>OKX_PROFILE</key>
<string>live</string>
<key>HOME</key>
<string>${HOME}</string>
</dict>
<key>StandardOutPath</key>
<string>${LOG_FILE}</string>
<key>StandardErrorPath</key>
<string>${LOG_FILE}</string>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
PLIST
launchctl load ~/Library/LaunchAgents/com.okx.earn-hunter.plist
Write scheduler.type = "launchagent" to platform.json. Inform user:
"macOS cron 服务未运行,已自动切换为 LaunchAgent 调度(无需 sudo,重启自动恢复)。"
Notes:
OKX_PROFILE from the plist EnvironmentVariables (or set to empty).~). The activation flow expands $HOME at generation time.RunAtLoad: true means the first scan runs immediately after loading.config.json / platform.json, writes state.json / notify.log, and sends notifications via curl to TG Bot API or Lark Webhook itself. No agent involvement needed at tick time.verboseLog=false — this is the intended silent behavior.IMPORTANT: On OS-scheduler platforms, do NOT use agent-platform /loop / Routines (Claude Code /loop, cloud Routines). These spawn LLM sessions per tick and cannot reliably push external notifications. (OpenClaw is the exception above — it uses its in-session cron tool with announce delivery by design.)
The entire Scan Cycle is implemented by scripts/scan.sh (pure shell + jq, zero LLM cost). Whether triggered by OS crontab, by an OpenClaw isolated cron agent turn, or by a user in an interactive session ("执行 earn-hunter 扫描"), the cycle is the same: run the script and relay its output. Do NOT re-implement the scan steps in natural language — the script is the single source of truth.
OpenClaw isolated cron turn: the job's prompt routes here. Run scripts/scan.sh (with platform.json notify.channel = "session", the script prints any notification to stdout); relay that stdout as the turn's response. The cron job's announce delivery then pushes it to the conversation channel. Do not curl TG/Lark from this turn — delivery is handled by announce.
# Profile: pass OKX_PROFILE only in API Key mode (omit for OAuth mode).
OKX_PROFILE=live ~/.okx/earn-hunter/scan.sh
Then relay the script's stdout verbatim to the user:
verboseLog=false case), tell the user "本轮扫描完成,无新机会" — do NOT fabricate a "scan complete" message into a channel; the silence is intentional.references/scan-logic.md for the spec it implements)~/.okx/earn-hunter/config.jsonflash.enabled / fixed.enabled / flexible.enabled):
okx [--profile live] earn flash-earn projects --status 0,100 --jsonokx [--profile live] earn savings fixed-products --json (auto-fallback to rate-history + fixedOffers on CLI <1.3.3)flexible.currencies, okx [--profile live] earn savings rate-history --ccy <ccy> --limit 1 --json~/.okx/earn-hunter/state.json (state.flash["<id>:<status>"], state.fixed["<ccy>:<term>:<rate>"], state.flexible["<ccy>"])notify.logstate.json (write new keys; flash ID-level diff cleanup; fixed key-level diff cleanup; flexible threshold-crossing diff cleanup; 7-day TTL; failure counter)verboseLog=true → brief status; verboseLog=false → silent exit 0, no output, nothing sentChannel routing inside the script: detection order TG ($TELEGRAM_BOT_TOKEN+$TELEGRAM_CHAT_ID) → Lark (platform.json .notify.lark_webhook) → session (stdout). A notify.channel of telegram/lark/session in platform.json forces that channel.
Auth / profile: the script never reads or prints credentials. It delegates all auth to the okx CLI (which reads ~/.okx/config.toml). Profile is injected only via the OKX_PROFILE env var.
Flexible Earn dedup model: Unlike Flash/Fixed which dedup by specific opportunity, Flexible uses a threshold-crossing model — key is just <ccy>. Notifies once when APY crosses above threshold; stays silent while it remains above; resets when APY drops below threshold (diff cleanup removes the key). This avoids frequent notifications from rate fluctuations.
When user wants to subscribe to a Fixed Earn product after receiving a notification.
Read {baseDir}/references/purchase-guide.md for the complete flow.
Summary:
min(idle + movable_simple_earn, lendQuota)okx-cex-earnEdge cases covered in purchase-guide.md:
Important: earn-hunter does NOT execute write operations directly. It transfers control to okx-cex-earn.
Read {baseDir}/references/config-reference.md for field definitions and natural language examples.
When user wants to change settings:
config-reference.md for field mapping)config.json or platform.json) → modify the field → write backException: TG credentials cannot be changed via natural language. Tell user to set environment variables directly.
Branch on platform.json .scheduler.type:
OpenClaw (openclaw-cron) — manage via the in-session cron tool (no CLI):
cron with action: "update", targeting the earn-hunter-hourly job, patch { "enabled": false } (or action: "remove" to delete it).action: "update" with { "enabled": true } (or re-create as in Activation Step 5).action: "list" to find the job id.OS-crontab platforms (cron):
crontab -l | grep -v 'earn-hunter' | crontab -macOS LaunchAgent (launchagent):
launchctl unload ~/Library/LaunchAgents/com.okx.earn-hunter.plistlaunchctl load ~/Library/LaunchAgents/com.okx.earn-hunter.plistConfig and state are preserved — resuming picks up where it left off.
When user says "卸载" / "uninstall":
launchctl unload ~/Library/LaunchAgents/com.okx.earn-hunter.plist && rm -f ~/Library/LaunchAgents/com.okx.earn-hunter.plist~/.okx/earn-hunter/ directoryTrigger phrases: "测试 earn-hunter" / "earn-hunter smoke test" / "测试定时任务触发"
Behavior — run the script with the Test Mode hooks (no separate logic needed):
# Force verbose so output is always produced; write dedup keys under test: prefix.
EH_TEST_NAMESPACE=1 OKX_PROFILE=live ~/.okx/earn-hunter/scan.sh # with config.verboseLog temporarily set to true
EH_TEST_NAMESPACE=1 isolates state writesconfig.verboseLog=true so the script always sends output regardless of whether opportunities are found (restore afterwards)EH_TEST_NAMESPACE=1 prefixes dedup keys with test: (e.g. test:flash:12345:100); these keys are immune to diff cleanup (only TTL removes them), so test runs do not pollute production statecron tool action: "list" shows the earn-hunter-hourly job?)~/.okx/earn-hunter/notify.logRead {baseDir}/templates/error-alert.md for exact alert message templates.
| Error | Action |
|---|---|
okx 401 / "Session expired" | Stop scan. Send alert (凭证失效 template). Load okx-cex-auth if interactive. |
| Network error / timeout | Retry once silently. If still fails, skip this cycle. |
| 3 consecutive scan failures | Send alert (连续失败 template). Counter stored in state.consecutive_failures, reset after alert. |
state.json corrupted | Reset by writing {"flash":{},"fixed":{},"consecutive_failures":0,"last_error":""}. May cause one round of duplicate notifications. |
| Notification send fails | Log to notify.log, continue scan. Dedup key NOT added (next cycle retries). |
config.json missing at runtime | Send alert: "earn-hunter 未配置,请运行首次激活流程。" |
| Dual-client suspected (user mentions both platforms) | Warn: "建议仅在一个客户端运行 earn-hunter,避免重复通知。" |
verboseLog = true + no hits | Send brief status (not silent). |
config.notify.language)(translation unavailable, sent in zh-CN)okx config init for OKX auth.--json for all okx commands. Render results as markdown tables.~/.okx/earn-hunter/notify.log.config.simulatedTrading is always false.These constraints apply to all changes to earn-hunter:
env.snapshot + resolve_bin fallback.2>&1 (capture into variable) instead of 2>/dev/null for CLI calls. Otherwise error messages vanish and last_error is empty.env -i PATH=/usr/bin:/bin (verify cron can run). Don't pass Step 5 if cron smoke fails.last_error is empty string, append "check ~/.okx/earn-hunter/cron.log; empty error usually means PATH issue" guidance.env.snapshot written at activation with exact paths; resolve_bin tries snapshot → command -v → hardcoded common paths. Survives nvm switch / Homebrew upgrades.