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
- Verify
okxCLI installed:which okx. If missing, install vianpm install -g @okx_ai/okx-trade-cli. On OpenClaw, also verify the in-sessioncrontool is available in the agent tool list (used for scheduling — not theopenclawCLI). - Check optional dependent skills:
Optional skills (not required for scanning/notifications):okx skill list --jsonokx-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:
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}Installation failed; scanning and notification features are unaffected. Subscription guidance and authentication recovery require this skill, and it can be installed manually later." - Preflight continues regardless of skill installation result
-
Auth mode detection — run both, first match wins:
okx config show --json→ has non-emptyapi_keyfield → API Key mode. Add--profile liveto all commands.- No API key +
okx auth status --json→"status":"logged_in"→ OAuth mode. No--profileflag needed. - Neither → stop. Load
okx-cex-authskill and follow login steps.
-
Init config and state:
- If
~/.okx/earn-hunter/directory does not exist →mkdir -p ~/.okx/earn-hunter - If
~/.okx/earn-hunter/config.jsondoes not exist → copy{baseDir}/config/default.jsonto it - If
~/.okx/earn-hunter/state.jsondoes not exist → write{"flash":{},"fixed":{},"flexible":{},"consecutive_failures":0,"last_error":""} - If
~/.okx/earn-hunter/platform.jsondoes not exist → run Platform Detection - 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.snapshotwith resolved tool paths (cron cannot rely on the user's login PATH):
The scan script sources this file to resolve tool paths even under cron's minimalcat > ~/.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 SNAPPATH=/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) —jqis required for scanning. Verify withwhich jq; if missing, install (brew install jq/apt-get install jq). - If
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):
- Probe environment clues:
OPENCLAW_HOMEenv var exists? → hint: OpenClaw- Agent tool list contains
cron/deliverytools? → hint: OpenClaw HERMES_HOMEenv var exists orwhich hermessucceeds? → hint: Hermes Agent- Running inside Claude Code session? → hint: Claude Code
- None of the above matched → hint: Generic
- Present detection result and ask user to confirm:
- "Detected that you are using {detected_platform}, is that correct?"
- User confirms → proceed
- User says no → ask: "Which platform are you using? 1) OpenClaw 2) Claude Code 3) Hermes Agent 4) Other"
- Initialize platform config:
- OpenClaw / Claude Code → copy
{baseDir}/config/<confirmed_platform>.default.jsonto~/.okx/earn-hunter/platform.json - Hermes Agent → copy
{baseDir}/config/claude-code.default.jsonas base, set.platformto"hermes",.scheduler.typeto"cron" - Generic → copy
{baseDir}/config/claude-code.default.jsonas base, set.platformto"generic",.scheduler.typeto"manual"
- OpenClaw / Claude Code → copy
- 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: "The current client does not support scheduled tasks, please upgrade to the latest version." Generic platform → no automatic scheduler. Inform: "The current platform does not support automatic scheduling; you can manually say 'run earn-hunter scan' to trigger it."
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-sessioncronagent 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 cronannouncedelivery. notify.channel defaults to"session"so the scan prints to stdout forannounceto 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):
- Telegram —
$TELEGRAM_BOT_TOKENand$TELEGRAM_CHAT_IDboth set → TG ready - Lark —
platform.notify.lark_webhooknon-empty → Lark ready - 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 |
|---|---|
| "Notify me about Flash Earn" / "monitor earn" / "Help me monitor earn" / "Notify me when flexible APY is high" | → Activation Flow |
| "Change APY threshold" / "Only watch USDT" / "change config" / "Add BTC to flexible" | → Config Management |
| "Subscribe USDT Fixed 7D" / "subscribe" / "I want to buy" | → Purchase Guide |
| "Run earn-hunter scan" (cron OR interactive) | → Scan Cycle — run scripts/scan.sh and relay its output |
| "Stop monitoring" / "Pause" / "stop" | → Pause/Resume |
| "Uninstall earn-hunter" / "uninstall" | → Uninstall |
| "Test earn-hunter" / "smoke test" / "Test scheduled task" | → Test Mode |
Activation Flow
First-time setup. Only confirm platform — everything else uses smart defaults.
Step 1 — Platform Detection & Confirmation
See Platform Detection. 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):
- Check
$TELEGRAM_BOT_TOKENand$TELEGRAM_CHAT_IDenv vars:- Both set → TG ready
- Token set but chat_id missing → warn: "Telegram configuration is incomplete (missing TELEGRAM_CHAT_ID), skipping TG" → continue to next channel
- Neither set → TG not available
- Check
platform.notify.lark_webhookor 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 format is invalid, skipping Lark" → continue to next channel - Not configured and no Lark MCP → Lark not available
- Webhook set and valid (starts with
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:
"The following push channels are detected as available:
- {list of detected channels, e.g. Telegram / Lark}
Where do you want notifications sent?
- {detected channel 1}
- {detected channel 2, if any}
- Show only in the current session (cannot receive when offline)"
If no external channel detected:
"Only new opportunities can be pushed to you. Where do you want notifications sent?
- Telegram — requires Bot Token and Chat ID (via environment variables)
- Lark/Feishu — requires Webhook URL
- Show only in the current session (⚠ cannot receive notifications when offline)
It is recommended to configure Telegram or Lark so that you can receive reminders even when not in the conversation."
- If user picks Telegram → guide setting
TELEGRAM_BOT_TOKENandTELEGRAM_CHAT_IDenv vars - If user picks Lark → ask for webhook URL, validate format (starts with
https://, contains/hook/), write toplatform.notify.lark_webhook - If user picks session → write
"session"and warn: "⚠ You will not receive notifications while offline; it is recommended to configure an external channel later."
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 — Scan scope: "Scan scope (multiple selections allowed): [1] Flash Earn [2] Fixed Earn [3] Flexible Earn Default: all selected"
- 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 — Monitor coins: For Fixed Earn: "Fixed Earn coins to monitor: all (default, press Enter) or enter specific coins (e.g. USDT, SOL)"
- Default:
"all"(all currencies) - If user specifies → set
config.currenciesto array (e.g.["USDT", "SOL"])
For Flexible Earn: "Flexible Earn coins to monitor: USDT, USDC (default, press Enter) or enter specific coins"
- Default:
["USDT", "USDC"] - If user specifies → set
config.flexible.currenciesto array - Note: flexible requires per-currency API calls, so recommend keeping the list small
Step 3/3 — APY threshold: For Fixed Earn: "Fixed Earn minimum APY threshold: no limit (default, press Enter) or enter a percentage (e.g. 8)"
- Default:
0(no limit) - If user specifies → set
config.fixed.globalMinApytovalue / 100(e.g. 8 →0.08)
For Flexible Earn: "Flexible Earn minimum APY threshold: 8% (default, press Enter) or enter a percentage"
- Default:
0.08(8%) - If user specifies → set
config.flexible.globalMinApytovalue / 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.
- Run one scan cycle immediately with
verboseLogforced on so output is always produced, even when there are no opportunities. Temporarily flipverboseLog, run the script, then restore it — and useEH_TEST_NAMESPACE=1so smoke-test dedup keys go under thetest: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) - If new opportunities found → the script sends the normal notification (rendered from templates)
- If no opportunities found → with
verboseLogforced on, the script sends the brief status. Optionally append the activation confirmation message: "Earn Hunter is now active. There are currently no new opportunities; it will automatically scan in the next round." (Use{baseDir}/templates/activation.mdas base, append the no-opportunity note) - TG or Lark channel → ask: "A test message has been sent to {channel}. Please confirm whether you received it?"
- User confirms → proceed to Step 5
- Not received → troubleshoot (see
notify-channels.md) - 5 min no response → ping once
- 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:
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.snapshotis 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: "Verified that it executes normally under the cron minimal environment. If this step fails, it means the tool paths in env.snapshot are incorrect."
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 }— deriveeveryMsfromscheduler.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: trueruns 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,writeflag no longer exists). The scan stays cheap because it runs theokxCLI throughexecand 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.
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:
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
- 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:
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:
"The macOS cron service is not running; automatically switched to LaunchAgent scheduling (no sudo required, automatically restored after restart)."
Notes:
- OAuth mode → omit
OKX_PROFILEfrom the plistEnvironmentVariables(or set to empty). - LaunchAgent plist paths must be absolute (no
~). The activation flow expands$HOMEat generation time. RunAtLoad: truemeans the first scan runs immediately after loading.- The script reads
config.json/platform.json, writesstate.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 ("run earn-hunter scan"), 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)
# 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=falsecase), tell the user "This scan round is complete, no new opportunities" — 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)
- Reads
~/.okx/earn-hunter/config.json - 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 torate-history+fixedOfferson CLI <1.3.3) - Flexible: for each currency in
flexible.currencies,okx [--profile live] earn savings rate-history --ccy <ccy> --limit 1 --json
- Flash:
- Filters (two-layer APY threshold, terms filter, currency filter; flexible uses threshold-crossing model)
- Dedups against
~/.okx/earn-hunter/state.json(state.flash["<id>:<status>"],state.fixed["<ccy>:<term>:<rate>"],state.flexible["<ccy>"]) - If new opportunities → renders the matching template (flash / fixed / flexible / mixed) and sends via the detected channel (TG → Lark → session), logging to
notify.log - 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) - If no new opportunities:
verboseLog=true→ brief status;verboseLog=false→ silent exit 0, no output, nothing sent - 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 <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.
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:
- Parallel balance check (funding + trading + flexible earn)
- Compare fixed APR vs flexible lendingRate
- Calculate recommended amount:
min(idle + movable_simple_earn, lendQuota) - Present recommendation with comparison hint
- 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:
- Parse intent → map to config field (see
config-reference.mdfor field mapping) - Read the target JSON file (
config.jsonorplatform.json) → modify the field → write back - 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
cronwithaction: "update", targeting theearn-hunter-hourlyjob, patch{ "enabled": false }(oraction: "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":
- 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 - Ask: "Keep configuration and historical data?"
- Yes → only remove scheduler
- No → also remove
~/.okx/earn-hunter/directory
Test Mode
Trigger phrases: "test earn-hunter" / "earn-hunter smoke test" / "test scheduled task trigger"
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
- Execute a full Scan Cycle — the script runs the same scan, but
EH_TEST_NAMESPACE=1isolates state writes - Force-send notification — temporarily set
config.verboseLog=trueso the script always sends output regardless of whether opportunities are found (restore afterwards) - Dedup writes to test namespace —
EH_TEST_NAMESPACE=1prefixes dedup keys withtest:(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 - 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:
crontoolaction: "list"shows theearn-hunter-hourlyjob?) - Last 5 lines of
~/.okx/earn-hunter/notify.log
- Completion message: "Test complete. State with the test: prefix does not affect formal deduplication; formal scanning is unaffected."
Error Handling
Read {baseDir}/templates/error-alert.md for exact alert message templates.
| Error | Action |
|---|---|
okx 401 / "Session expired" |
Stop scan. Send alert (credential invalid 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 (consecutive failures 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 is not configured, please run the first-time activation process." |
| Dual-client suspected (user mentions both platforms) | Warn: "It is recommended to run earn-hunter on only one client to avoid duplicate notifications." |
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 initfor OKX auth. - Output: Use
--jsonfor 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.simulatedTradingis alwaysfalse.
Defensive Design Principles
These constraints apply to all changes to earn-hunter:
- 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_binfallback. - Sanity check first. Before running any CLI command, verify the binary exists. Missing tool → immediate exit with clear error, not silent failure.
- Never swallow stderr. Use
2>&1(capture into variable) instead of2>/dev/nullfor CLI calls. Otherwise error messages vanish andlast_erroris empty. - 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. - Alert templates must handle empty fields. If
last_erroris empty string, append "check~/.okx/earn-hunter/cron.log; empty error usually means PATH issue" guidance. - Tool paths use snapshot + fallback, not global PATH.
env.snapshotwritten at activation with exact paths;resolve_bintries snapshot →command -v→ hardcoded common paths. Survivesnvm switch/ Homebrew upgrades.