--- name: earn-hunter description: "Automatically 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, 活期年化高了通知我, 监控活期." license: MIT metadata: author: okx version: "1.4.6" homepage: "https://www.okx.com" agent: requires: bins: ["okx"] install: - id: okx-cli kind: node package: "@okx_ai/okx-trade-cli@1.4.6" bins: ["okx"] label: "Install okx CLI (npm)" --- # Earn Hunter Automated 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. ## Preflight 1. 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). 2. Check optional dependent skills: ```bash 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 recovery If either is missing, attempt to install but **do not block** if installation fails: ```bash okx skill add okx-cex-earn okx skill add okx-cex-auth ``` - Install succeeds → continue - Install fails (network error, marketplace unavailable, etc.) → **warn and continue**: "⚠ `{skill_name}` 安装失败,扫描和通知功能不受影响。申购引导和认证恢复需要该 skill,后续可手动安装。" - Preflight continues regardless of skill installation result 4. 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. - No API key + `okx auth status --json` → `"status":"logged_in"` → **OAuth mode**. No `--profile` flag needed. - Neither → **stop**. Load `okx-cex-auth` skill and follow login steps. 5. Init config and state: - If `~/.okx/earn-hunter/` directory does not exist → `mkdir -p ~/.okx/earn-hunter` - If `~/.okx/earn-hunter/config.json` does not exist → copy `{baseDir}/config/default.json` to it - If `~/.okx/earn-hunter/state.json` does not exist → write `{"flash":{},"fixed":{},"flexible":{},"consecutive_failures":0,"last_error":""}` - If `~/.okx/earn-hunter/platform.json` does not exist → run [Platform Detection](#platform-detection-active-probe--user-confirmation) - Always (re)install the scan script: `cp {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. - Write `~/.okx/earn-hunter/env.snapshot` with resolved tool paths (cron cannot rely on the user's login PATH): ```bash 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`). ## Platform & Channel Detection Three independent dimensions: **platform** (where the agent runs), **scheduler** (what triggers scans), **notification channel** (where alerts go). ### Platform Detection (active probe + user confirmation) **First run (no `platform.json` exists):** 1. Probe environment clues: - `OPENCLAW_HOME` env var exists? → hint: OpenClaw - Agent tool list contains `cron` / `delivery` tools? → hint: OpenClaw - `HERMES_HOME` env var exists or `which hermes` succeeds? → hint: Hermes Agent - Running inside Claude Code session? → hint: Claude Code - None of the above matched → hint: Generic 2. Present detection result and **ask user to confirm**: - "检测到你正在使用 **{detected_platform}**,是否正确?" - User confirms → proceed - User says no → ask: "你使用的是哪个平台?1) OpenClaw 2) Claude Code 3) Hermes Agent 4) 其他" 3. Initialize platform config: - OpenClaw / Claude Code → copy `{baseDir}/config/.default.json` to `~/.okx/earn-hunter/platform.json` - Hermes Agent → copy `{baseDir}/config/claude-code.default.json` as base, set `.platform` to `"hermes"`, `.scheduler.type` to `"cron"` - Generic → copy `{baseDir}/config/claude-code.default.json` as base, set `.platform` to `"generic"`, `.scheduler.type` to `"manual"` 4. Result written to `~/.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 扫描'来触发。" ### Configuration Files | 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`):** - scheduler.type = `"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`):** - scheduler.type = `"cron"` — scheduled via **OS crontab → `scripts/scan.sh`** (zero LLM token cost), notification via TG / Lark curl from the script itself. ### Notification Channels (independent of platform) Detect in priority order (PRD requirement: TG first): 1. **Telegram** — `$TELEGRAM_BOT_TOKEN` and `$TELEGRAM_CHAT_ID` both set → TG ready 2. **Lark** — `platform.notify.lark_webhook` non-empty → Lark ready 3. **Session** — fallback, only works in interactive mode TG 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. --- ## Skill Routing | User intent | Route | |---|---| | "有闪赚通知我" / "monitor earn" / "帮我监控赚币" / "活期年化高了通知我" | → [Activation Flow](#activation-flow) | | "改 APY 阈值" / "只看 USDT" / "change config" / "活期加上 BTC" | → [Config Management](#config-management) | | "申购 USDT 定期 7D" / "subscribe" / "我要买" | → [Purchase Guide](#purchase-guide) | | "执行 earn-hunter 扫描" (cron OR interactive) | → [Scan Cycle](#scan-cycle) — run `scripts/scan.sh` and relay its output | | "停止监控" / "暂停" / "stop" | → [Pause/Resume](#pauseresume) | | "卸载 earn-hunter" / "uninstall" | → [Uninstall](#uninstall) | | "测试 earn-hunter" / "smoke test" / "测试定时任务" | → [Test Mode](#test-mode) | --- ## Activation Flow First-time setup. Only confirm platform — everything else uses smart defaults. ### Step 1 — Platform Detection & Confirmation See [Platform Detection](#platform-detection-active-probe--user-confirmation). Probe environment → ask user to confirm → write `platform.json`. ### Step 2 — Detect Notification Channel & Confirm **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): 1. Check `$TELEGRAM_BOT_TOKEN` and `$TELEGRAM_CHAT_ID` env vars: - Both set → TG ready - Token set but chat_id missing → warn: "Telegram 配置不完整(缺少 TELEGRAM_CHAT_ID),跳过 TG" → continue to next channel - Neither set → TG not available 2. Check `platform.notify.lark_webhook` or Lark MCP tools: - Webhook set and valid (starts with `https://` and contains `/hook/`) → Lark ready - Webhook set but format invalid (does not start with `https://` or missing `/hook/`) → warn: "Lark webhook 格式无效,跳过 Lark" → continue to next channel - Not configured and no Lark MCP → Lark not available **Always 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:** "检测到以下推送渠道可用: - {list of detected channels, e.g. Telegram / Lark} 你希望通知发到哪里? 1. {detected channel 1} 2. {detected channel 2, if any} 3. 仅在当前会话显示(离线收不到)" **If no external channel detected:** "新机会才能推送到你手上。你希望通知发到哪里? 1. Telegram — 需要提供 Bot Token 和 Chat ID(通过环境变量) 2. Lark/飞书 — 需要提供 Webhook URL 3. 仅在当前会话显示(⚠ 离线收不到通知) 推荐配置 Telegram 或 Lark,这样即使不在对话中也能收到提醒。" - If user picks Telegram → guide setting `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID` env vars - If user picks Lark → ask for webhook URL, validate format (starts with `https://`, contains `/hook/`), write to `platform.notify.lark_webhook` - If user picks session → write `"session"` and warn: "⚠ 离线状态下不会收到通知,建议后续配置外部渠道。" Write confirmed channel to `platform.json` `notify.channel`. ### Step 3 — Confirm Scan Config (3-step with defaults) 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(活期赚币) 默认:全选" - Default: all three enabled - If user picks specific items → disable the others - If user only picks [1] → set `config.fixed.enabled = false`, `config.flexible.enabled = false`; skip Step 2/3 and 3/3 - If user only picks [3] → set `config.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)" - Default: `"all"` (all currencies) - If user specifies → set `config.currencies` to array (e.g. `["USDT", "SOL"]`) For Flexible Earn: "活期监控币种:USDT, USDC(默认,按回车)或输入指定币种" - Default: `["USDT", "USDC"]` - If user specifies → set `config.flexible.currencies` to array - Note: flexible requires per-currency API calls, so recommend keeping the list small **Step 3/3 — APY 阈值:** For Fixed Earn: "定期最低 APY 阈值:不限(默认,按回车)或输入百分比(如 8)" - Default: `0` (no limit) - If user specifies → set `config.fixed.globalMinApy` to `value / 100` (e.g. 8 → `0.08`) For Flexible Earn: "活期最低 APY 阈值:8%(默认,按回车)或输入百分比" - Default: `0.08` (8%) - If user specifies → set `config.flexible.globalMinApy` to `value / 100` Auto-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). ### Step 4 — Smoke Test & Delivery Confirmation **Smoke test always sends a notification, regardless of `verboseLog` setting.** 1. Run one scan cycle immediately with `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: ```bash 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) ``` 2. **If new opportunities found** → the script sends the normal notification (rendered from templates) 3. **If no opportunities found** → with `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) 4. TG or Lark channel → **ask:** "已向 {channel} 发送测试消息,请确认是否收到?" 5. User confirms → proceed to Step 5 6. Not received → troubleshoot (see `notify-channels.md`) 7. 5 min no response → ping once 8. Session channel → skip confirmation **Note:** The smoke test ignores `verboseLog` setting — it always produces output to verify the full pipeline works end-to-end. ### Step 4b — Cron Environment Smoke Test (OS-crontab platforms only) **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: ```bash env -i HOME="$HOME" PATH=/usr/bin:/bin \ EH_TEST_NAMESPACE=1 OKX_PROFILE=live \ bash ~/.okx/earn-hunter/scan.sh ``` - If exit 0 → cron will work. Proceed to Step 5. - If exit 127 / "FATAL: 'okx' not found" → `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. - Show the user: "已验证 cron 最小环境下可正常执行。如果此步失败,说明 env.snapshot 中的工具路径有误。" ### Step 5 — Set Up Scheduler The scheduling mechanism depends on `platform.json` `.scheduler.type`. Branch on the platform. #### OpenClaw (`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 one - `payload`: `{ "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. - **Token budget:** OpenClaw cron jobs have **no per-job tool-whitelist field** (the old `--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](#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. #### OS-crontab platforms (`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: ```bash 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: ```bash # 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)** ```bash (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: ```bash if [[ "$(uname -s)" == "Darwin" ]] && ! launchctl list com.vix.cron >/dev/null 2>&1; then # cron daemon not running — fallback to LaunchAgent fi ``` - If cron daemon is running → done, `scheduler.type = "cron"`. - If cron daemon is **not** running → remove the crontab entry and proceed to Step B. - On Linux → skip the check (cron is always available), `scheduler.type = "cron"`. **Step B: macOS LaunchAgent fallback** (`scheduler.type = "launchagent"`) Generate `~/Library/LaunchAgents/com.okx.earn-hunter.plist` with the resolved paths: ```bash 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 Label com.okx.earn-hunter ProgramArguments /bin/bash ${SCAN_SCRIPT} StartInterval ${INTERVAL} EnvironmentVariables PATH ${CRON_PATH} OKX_PROFILE live HOME ${HOME} StandardOutPath ${LOG_FILE} StandardErrorPath ${LOG_FILE} RunAtLoad PLIST launchctl load ~/Library/LaunchAgents/com.okx.earn-hunter.plist ``` Write `scheduler.type = "launchagent"` to `platform.json`. Inform user: "macOS cron 服务未运行,已自动切换为 LaunchAgent 调度(无需 sudo,重启自动恢复)。" **Notes:** - **OAuth mode** → omit `OKX_PROFILE` from the plist `EnvironmentVariables` (or set to empty). - LaunchAgent plist paths must be absolute (no `~`). The activation flow expands `$HOME` at generation time. - `RunAtLoad: true` means the first scan runs immediately after loading. - The script reads `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. - The script exits 0 and produces **no output** when there are no new opportunities and `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.) --- ## Scan Cycle **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`. ### How the agent runs a scan (interactive trigger) ```bash # 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: - If the script prints a notification (Flash / Fixed / mixed / verbose status), show it. - If the script prints **nothing** (silent exit 0, the no-new-opportunity + `verboseLog=false` case), tell the user "本轮扫描完成,无新机会" — do NOT fabricate a "scan complete" message into a channel; the silence is intentional. ### What the script does (see `references/scan-logic.md` for the spec it implements) 1. Reads `~/.okx/earn-hunter/config.json` 2. Runs scan commands (based on `flash.enabled` / `fixed.enabled` / `flexible.enabled`): - Flash: `okx [--profile live] earn flash-earn projects --status 0,100 --json` - Fixed: `okx [--profile live] earn savings fixed-products --json` (auto-fallback to `rate-history` + `fixedOffers` on CLI <1.3.3) - Flexible: for each currency in `flexible.currencies`, `okx [--profile live] earn savings rate-history --ccy --limit 1 --json` 3. Filters (two-layer APY threshold, terms filter, currency filter; flexible uses threshold-crossing model) 4. Dedups against `~/.okx/earn-hunter/state.json` (`state.flash[":"]`, `state.fixed["::"]`, `state.flexible[""]`) 5. If new opportunities → renders the matching template (flash / fixed / flexible / mixed) and sends via the detected channel (TG → Lark → session), logging to `notify.log` 6. Updates `state.json` (write new keys; flash ID-level diff cleanup; fixed key-level diff cleanup; flexible threshold-crossing diff cleanup; 7-day TTL; failure counter) 7. If no new opportunities: `verboseLog=true` → brief status; `verboseLog=false` → **silent exit 0, no output, nothing sent** 8. Error handling: consecutive-failure counter (alert at 3, then reset); 401/session-expired → credential alert + stop **Channel 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 ``. 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. --- ## Purchase Guide 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: 1. Parallel balance check (funding + trading + flexible earn) 2. Compare fixed APR vs flexible lendingRate 3. Calculate recommended amount: `min(idle + movable_simple_earn, lendQuota)` 4. Present recommendation with comparison hint 5. User confirms → **re-check offer availability** (soldOut guard) → hand off to `okx-cex-earn` Edge cases covered in purchase-guide.md: - Balance < minLend → show deficit - Amount > lendQuota → auto-cap with notice - Redeem succeeded but subscribe failed → warn user, funds are in funding account - Offer sold out between notification and subscription → inform user **Important:** earn-hunter does NOT execute write operations directly. It transfers control to `okx-cex-earn`. --- ## Config Management Read `{baseDir}/references/config-reference.md` for field definitions and natural language examples. When user wants to change settings: 1. Parse intent → map to config field (see `config-reference.md` for field mapping) 2. Read the target JSON file (`config.json` or `platform.json`) → modify the field → write back 3. Read the updated file → confirm the change to user **Exception:** TG credentials cannot be changed via natural language. Tell user to set environment variables directly. --- ## Pause/Resume Branch on `platform.json` `.scheduler.type`: **OpenClaw (`openclaw-cron`)** — manage via the in-session `cron` tool (no CLI): - **Pause:** call `cron` with `action: "update"`, targeting the `earn-hunter-hourly` job, patch `{ "enabled": false }` (or `action: "remove"` to delete it). - **Resume:** `action: "update"` with `{ "enabled": true }` (or re-create as in Activation Step 5). - Use `action: "list"` to find the job id. **OS-crontab platforms (`cron`):** - **Pause:** `crontab -l | grep -v 'earn-hunter' | crontab -` - **Resume:** Re-add the crontab entry (same as Activation Step 5). **macOS LaunchAgent (`launchagent`):** - **Pause:** `launchctl unload ~/Library/LaunchAgents/com.okx.earn-hunter.plist` - **Resume:** `launchctl load ~/Library/LaunchAgents/com.okx.earn-hunter.plist` Config and state are preserved — resuming picks up where it left off. --- ## Uninstall When user says "卸载" / "uninstall": 1. Stop the scheduler (same as Pause). For LaunchAgent, also remove the plist: `launchctl unload ~/Library/LaunchAgents/com.okx.earn-hunter.plist && rm -f ~/Library/LaunchAgents/com.okx.earn-hunter.plist` 2. Ask: "是否保留配置和历史数据?" - Yes → only remove scheduler - No → also remove `~/.okx/earn-hunter/` directory --- ## Test Mode Trigger phrases: "测试 earn-hunter" / "earn-hunter smoke test" / "测试定时任务触发" Behavior — run the script with the Test Mode hooks (no separate logic needed): ```bash # 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 ``` 1. **Execute a full Scan Cycle** — the script runs the same scan, but `EH_TEST_NAMESPACE=1` isolates state writes 2. **Force-send notification** — temporarily set `config.verboseLog=true` so the script always sends output regardless of whether opportunities are found (restore afterwards) 3. **Dedup writes to test namespace** — `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 state 4. **Output diagnostics** after scan completes: - okx auth status (logged in / expired / not configured) - Scan command results (flash project count + fixed product count + flexible rate count) - Post-filter results (how many passed filters per type) - Notification channel status (which channel is configured, send result) - Scheduler status (OS-crontab platforms: crontab entry exists? / OpenClaw: `cron` tool `action: "list"` shows the `earn-hunter-hourly` job?) - Last 5 lines of `~/.okx/earn-hunter/notify.log` 5. **Completion message:** "测试完成。test: 前缀的 state 不影响正式去重,正式扫描不受影响。" --- ## Error Handling Read `{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). | --- ## i18n - **All notifications rendered in user's language** (detected at activation, stored in `config.notify.language`) - **Locked terms (never translate):** Flash Earn, Fixed Earn, Simple Earn, DCD, APY, APR, OKX, earn-hunter, Telegram, project names, currency codes (USDT, BTC, etc.) - **Fallback:** If LLM rendering fails, send Chinese template + append `(translation unavailable, sent in zh-CN)` --- ## Global Notes - **Security:** Never accept credentials in chat. TG token only via env vars. Guide users to `okx config init` for OKX auth. - **Output:** Use `--json` for all okx commands. Render results as markdown tables. - **Logging:** All notification send results logged to `~/.okx/earn-hunter/notify.log`. - **Scope:** Covers Flash Earn, Simple Earn Fixed, and Simple Earn Flexible (活期). DCD, on-chain, auto-earn are out of scope. - **Mode:** Live trading only. `config.simulatedTrading` is always `false`. ## Defensive Design Principles These constraints apply to all changes to earn-hunter: 1. **cron PATH ≠ user shell PATH.** Any cron-triggered script must resolve tool paths independently — never assume cron inherits the user's login PATH. The script uses `env.snapshot` + `resolve_bin` fallback. 2. **Sanity check first.** Before running any CLI command, verify the binary exists. Missing tool → immediate exit with clear error, not silent failure. 3. **Never swallow stderr.** Use `2>&1` (capture into variable) instead of `2>/dev/null` for CLI calls. Otherwise error messages vanish and `last_error` is empty. 4. **Smoke test must simulate production.** Activation smoke test runs twice: once in user shell (verify credentials/network), once in minimal `env -i PATH=/usr/bin:/bin` (verify cron can run). Don't pass Step 5 if cron smoke fails. 5. **Alert templates must handle empty fields.** If `last_error` is empty string, append "check `~/.okx/earn-hunter/cron.log`; empty error usually means PATH issue" guidance. 6. **Tool paths use snapshot + fallback, not global PATH.** `env.snapshot` written at activation with exact paths; `resolve_bin` tries snapshot → `command -v` → hardcoded common paths. Survives `nvm switch` / Homebrew upgrades.