# TeamoRouter Skill Document **Target**: OpenClaw >= 2026.3.x ## Language & Runtime Requirements - **Language**: Communicate with the user in the language configured in OpenClaw (follow the agent's locale / language setting). All user-facing messages in this document are templates — translate them to match the user's language. - **ClawBot compatibility**: If the current runtime is ClawBot (not OpenClaw), immediately inform the user that TeamoRouter is not supported on ClawBot and they need to upgrade to OpenClaw. Do NOT proceed with installation. Send this message (translated to the user's language): > TeamoRouter is not supported on ClawBot. Please upgrade to OpenClaw to use TeamoRouter. > Upgrade guide: https://docs.openclaw.ai/migration/clawbot-to-openclaw ## 0) Defaults ```bash TEAMOROUTER_SITE="https://router.teamolab.com" TEAMOROUTER_API_BASE="${TEAMOROUTER_API_BASE:-$TEAMOROUTER_SITE}" TEAMOROUTER_OPENAI_BASE="${TEAMOROUTER_OPENAI_BASE:-$TEAMOROUTER_SITE/v1}" TEAMOROUTER_STATE_DIR="${TEAMOROUTER_STATE_DIR:-$HOME/.openclaw/workspace/.teamo-router}" TEAMOROUTER_STATE_FILE="${TEAMOROUTER_STATE_FILE:-$HOME/.openclaw/workspace/.teamo-router/heartbeat-state.json}" ``` Required tools: ```bash for c in curl jq; do command -v "$c" >/dev/null || { echo "Missing required command: $c"; exit 1; } done ``` ## 1) Register the Agent and Get the Claim Link Register this runtime first, then extract `claim_url`, `teamo_key`, and related fields. ```bash mkdir -p "$TEAMOROUTER_STATE_DIR" REGISTER_PAYLOAD='{ "agent_name": "'"${AGENT_NAME:-OpenClawAgent}"'", "runtime": "'"${AGENT_RUNTIME:-openclaw}"'", "metadata": { "hostname": "'"$(hostname 2>/dev/null || echo unknown)"'", "skill_version": "codex-gpt5-r2-en" } }' REGISTER_RESP="$(curl -fsS -X POST "$TEAMOROUTER_API_BASE/agents/register" \ -H "Content-Type: application/json" \ -d "$REGISTER_PAYLOAD")" TEAMOROUTER_AGENT_ID="$(echo "$REGISTER_RESP" | jq -r '.agent_id')" TEAMO_KEY="$(echo "$REGISTER_RESP" | jq -r '.teamo_key')" CLAIM_URL="$(echo "$REGISTER_RESP" | jq -r '.claim_url')" CLAIM_EXPIRES_AT_RAW="$(echo "$REGISTER_RESP" | jq -r '.claim_expires_at // empty')" CLAIM_EXPIRES_AT="$CLAIM_EXPIRES_AT_RAW" if [ -n "$CLAIM_EXPIRES_AT_RAW" ]; then CLAIM_EXPIRES_AT="$(printf '%s' "$CLAIM_EXPIRES_AT_RAW" | \ sed -E 's/\.[0-9]+([+-][0-9:]+|Z)$/\1/' | \ jq -Rr 'fromdateiso8601 | strflocaltime("%Y-%m-%d %H:%M")')" fi ``` Hard fail if `TEAMO_KEY` or `CLAIM_URL` is empty. Immediately send this English message to the user: ```text TeamoRouter is now installed. Complete the binding step below, and I will be ready to connect you to the world's top LLMs without any API key. Please click the link below to complete TeamoRouter sign-in and wallet binding: This claim link is valid until (local time): After completion, please reply: "Claim completed". ``` ## 2) Verify Claim Status After User Confirms Wait for the user to reply "Claim completed" (or equivalent confirmation). Then verify the claim status with a single API call: ```bash STATUS_RESP="$(curl -fsS "$TEAMOROUTER_API_BASE/agents/status" \ -H "Authorization: Bearer $TEAMO_KEY" \ -H "x-agent-id: $TEAMOROUTER_AGENT_ID")" CLAIM_STATUS="$(echo "$STATUS_RESP" | jq -r '.status')" WALLET_STATUS="$(echo "$STATUS_RESP" | jq -r '.wallet.status // "unknown"')" CLAIM_OK=false if ([ "$CLAIM_STATUS" = "claimed" ] || [ "$CLAIM_STATUS" = "active" ]) && [ "$WALLET_STATUS" = "bound" ]; then TEAMO_WALLET_ID="$(echo "$STATUS_RESP" | jq -r '.wallet.wallet_id // empty')" CLAIM_OK=true fi ``` - If `CLAIM_OK=true`, proceed to Step 3. - The server may return `status` as either `claimed` or `active` — both indicate a successful claim. If the status is neither, or wallet is not `bound`, inform the user that binding is not yet complete and ask them to finish the claim step and reply again. Do not proceed until verification passes. ## 3) Fetch Models, Fetch Balance, and Write Config After claim and wallet binding are confirmed, fetch the model list and current balance, then write the OpenClaw config. ### 3a) Fetch raw data ```bash MODELS_RESP="$(curl -fsS "$TEAMOROUTER_API_BASE/v1/models" \ -H "Authorization: Bearer $TEAMO_KEY")" BALANCE_RESP="$(curl -fsS "$TEAMOROUTER_API_BASE/v1/billing/me/balance" \ -H "Authorization: Bearer $TEAMO_KEY")" BALANCE_AMOUNT_RAW="$(echo "$BALANCE_RESP" | jq -r '.data.available_balance')" BALANCE_AMOUNT="$(printf '%.1f' "$BALANCE_AMOUNT_RAW")" ALL_MODELS="$(echo "$MODELS_RESP" | jq -r '.data[].id')" META_MODELS="$(echo "$MODELS_RESP" | jq -r '.data[] | select(.metadata.is_meta_model == true) | "\(.id) → \(.metadata.actual_model)"')" CONFIG_RESP="$(curl -fsS "$TEAMOROUTER_API_BASE/v1/models/config" \ -H "Authorization: Bearer $TEAMO_KEY")" ``` ### 3b) Transform and write config for OpenClaw 2026.3.x The config transformation is complex. To avoid quoting issues, first write the jq filter to a temp file, then execute it. **Step 1**: Write the jq filter file: ```bash cat > /tmp/teamorouter-transform.jq << 'JQEOF' [ .models.providers | to_entries[] | .key as $orig_name | .value.baseUrl as $base | .value | .models[] | { orig_provider: $orig_name, base_url: $base, model: . } ] | map( .model.api as $raw_api | (if $raw_api == "anthropic-messages" then "anthropic-messages" else "openai-completions" end) as $oc_api | (if $raw_api == "anthropic-messages" then "msg" else "oai" end) as $suffix | (if .orig_provider == "teamorouter" then ("teamorouter-" + $suffix) else ("teamorouter-" + .orig_provider + "-" + $suffix) end) as $new_name | . + { oc_api: $oc_api, new_provider: $new_name } ) | group_by(.new_provider) | map({ key: .[0].new_provider, value: { baseUrl: (.[0].base_url + "/v1"), apiKey: $key, api: .[0].oc_api, models: [ .[].model | { id: .id, name: (.name // .id), api: (if .api == "anthropic-messages" then "anthropic-messages" else "openai-completions" end), reasoning: (.reasoning // false), input: (.input // ["text"]), cost: ((.cost // {}) | { input: (.input // 0), output: (.output // 0), cacheRead: (.cache_read // .cacheRead // 0), cacheWrite: (.cache_creation // .cacheWrite // 0) }), contextWindow: (.contextWindow // 200000), maxTokens: (.maxTokens // 8192) } ] } }) | from_entries JQEOF ``` **Step 2**: Run the transformation: ```bash OPENCLAW_CONFIG="$(echo "$CONFIG_RESP" | jq --arg key "$TEAMO_KEY" -f /tmp/teamorouter-transform.jq)" ``` **Step 3**: Pick the default model (with provider prefix): ```bash DEFAULT_MODEL="$(echo "$OPENCLAW_CONFIG" | jq -r ' to_entries[] | .key as $prov | .value.models[] | select(.id == "teamo-free") | ($prov + "/" + .id) ' | head -1)" if [ -z "$DEFAULT_MODEL" ]; then DEFAULT_MODEL="$(echo "$OPENCLAW_CONFIG" | jq -r ' to_entries[] | select(.value.api == "openai-completions") | .key as $prov | .value.models[0] | ($prov + "/" + .id) ' | head -1)" fi ``` **Step 4**: Write to OpenClaw config file: ```bash OPENCLAW_DIR="$HOME/.openclaw" OPENCLAW_FILE="$OPENCLAW_DIR/openclaw.json" mkdir -p "$OPENCLAW_DIR" if [ -f "$OPENCLAW_FILE" ]; then jq --argjson new_providers "$OPENCLAW_CONFIG" \ --arg default_model "$DEFAULT_MODEL" ' .models = (.models // {}) | .models.providers = ((.models.providers // {}) + $new_providers) | .agents = (.agents // {}) | .agents.defaults = (.agents.defaults // {}) | .agents.defaults.model = (.agents.defaults.model // {}) | .agents.defaults.model.primary = $default_model ' "$OPENCLAW_FILE" > "${OPENCLAW_FILE}.tmp" && mv "${OPENCLAW_FILE}.tmp" "$OPENCLAW_FILE" else jq -n --argjson providers "$OPENCLAW_CONFIG" \ --arg default_model "$DEFAULT_MODEL" '{ models: { mode: "merge", providers: $providers }, agents: { defaults: { model: { primary: $default_model, fallbacks: [] } } } }' > "$OPENCLAW_FILE" fi ``` **Step 5**: Save agent state: ```bash mkdir -p "$TEAMOROUTER_STATE_DIR" cat > "$TEAMOROUTER_STATE_DIR/agent.env.tmp" < "$TEAMOROUTER_STATE_DIR/heartbeat.sh" << 'HBEOF' #!/bin/sh STATE_DIR="$HOME/.openclaw/workspace/.teamo-router" STATE_FILE="$STATE_DIR/heartbeat-state.json" . "$STATE_DIR/agent.env" 2>/dev/null if [ -z "$TEAMO_KEY" ] || [ -z "$TEAMOROUTER_AGENT_ID" ]; then echo "ERROR: agent.env missing or incomplete" exit 1 fi TEAMOROUTER_API_BASE="${TEAMOROUTER_API_BASE:-https://router.teamolab.com}" mkdir -p "$STATE_DIR" test -f "$STATE_FILE" || printf '{"last_heartbeat_at":null,"last_notified_version":null,"last_balance_alert_at":null}\n' > "$STATE_FILE" HB_RESP="$(curl -fsS -X POST "$TEAMOROUTER_API_BASE/agents/heartbeat" \ -H "Authorization: Bearer $TEAMO_KEY" \ -H "Content-Type: application/json" \ -d "{\"agent_id\":\"$TEAMOROUTER_AGENT_ID\",\"current_version\":\"${GATEWAY_VERSION:-unknown}\"}")" # Persist alert state for proactive check NEEDS_UPDATE="$(echo "$HB_RESP" | jq -r '.needs_update // false')" LATEST_VER="$(echo "$HB_RESP" | jq -r '.latest_version // empty')" IS_LOW="$(echo "$HB_RESP" | jq -r '.balance.is_low // false')" BAL_AMT="$(echo "$HB_RESP" | jq -r '.balance.amount // empty')" NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)" jq -n \ --arg ts "$NOW" \ --argjson needs_update "$NEEDS_UPDATE" \ --arg latest_version "$LATEST_VER" \ --argjson is_low "$IS_LOW" \ --arg balance "$BAL_AMT" \ '{last_heartbeat_at: $ts, needs_update: $needs_update, latest_version: $latest_version, balance_is_low: $is_low, balance_amount: $balance}' \ > "$STATE_FILE" echo "$HB_RESP" HBEOF chmod +x "$TEAMOROUTER_STATE_DIR/heartbeat.sh" ``` **IMPORTANT**: You MUST execute the steps above exactly as written. The jq filter file (`/tmp/teamorouter-transform.jq`) must be written first using the heredoc, then referenced with `jq -f`. Do NOT try to inline the jq filter into a single command — it will break due to quoting. Do NOT try to write the config manually or with `openclaw config set` — the transformations are too complex. ## 4) Connectivity Verification After the config is written, connectivity must be verified before telling the user installation is complete. Test with a real request using the default model: ```bash DEFAULT_PROVIDER="${DEFAULT_MODEL%%/*}" DEFAULT_MODEL_ID="${DEFAULT_MODEL##*/}" # Look up the api type for this provider DEFAULT_API="$(echo "$OPENCLAW_CONFIG" | jq -r \ --arg p "$DEFAULT_PROVIDER" '.[$p].api')" PROBE_BODY='{"model":"'"$DEFAULT_MODEL_ID"'","messages":[{"role":"user","content":"hi"}],"max_tokens":1}' if [ "$DEFAULT_API" = "openai-completions" ]; then curl -fsS "$TEAMOROUTER_API_BASE/v1/chat/completions" \ -H "Authorization: Bearer $TEAMO_KEY" \ -H "Content-Type: application/json" \ -d "$PROBE_BODY" >/dev/null else curl -fsS "$TEAMOROUTER_API_BASE/v1/messages" \ -H "Authorization: Bearer $TEAMO_KEY" \ -H "Content-Type: application/json" \ -H "anthropic-version: 2023-06-01" \ -d "$PROBE_BODY" >/dev/null fi ``` If the probe fails, report the step, cause, and next action explicitly. ## 5) Auto-Send These Messages After Installation Completes Only after claim is complete, wallet binding is complete, config writing succeeds, and connectivity verification passes, send the following five parts. **Part 1 - Installation Success** ```text TeamoRouter installation is complete. You can now use global LLMs through TeamoRouter without any API key. ``` **Part 2 - Available Models** Render every model ID from `ALL_MODELS` as a markdown list: ```text Available models: - - - ... ``` **Part 3 - Current Routed Models** Render the dynamic mappings from `META_MODELS`: ```text Current routed models: - - ... ``` If no meta models exist, this section may be omitted. **Part 4 - How to Switch Models** Assume the user can only talk to the bot through IM, such as Feishu, and cannot edit API request bodies directly. Show this message: ```text How to switch models: just tell me which model you want to use in this chat, for example, "Switch to gpt-5". ``` Also follow these behavior rules: - If the user explicitly asks to switch to a model in chat, look up which provider contains that model and use the full `provider/model` format (e.g. `teamorouter-openai-oai/gpt-5`) when setting it. The user only needs to say the model name (e.g. "gpt-5"); the agent resolves the provider prefix automatically. - If the user does not specify a model, keep using the current default route. - If the requested model is not in `ALL_MODELS`, explain that the model is currently unavailable and show the available model list again. **Part 5 - Current Balance** Display rule: every balance value shown to the user must be rounded to 1 decimal place, for example `1.0`, instead of showing the raw long decimal from the API. ```text Current balance: . This is complimentary sign-up credit and is about to run out. To continue using TeamoRouter, please top up at the link below. Top up and manage your account here: https://router.teamolab.com/router/dashboard ``` ## 6) Register Heartbeat in HEARTBEAT.md and AGENTS.md This step configures two built-in agent mechanisms to handle TeamoRouter alerts — no cron job needed. 1. **HEARTBEAT.md** — the agent's built-in heartbeat (every ~30 min in main session) will execute `heartbeat.sh` to call the API, write alert state, and notify the user. 2. **AGENTS.md** — on every new session start, the agent executes `heartbeat.sh` to get fresh status, then notifies the user immediately. ### 6a) Append heartbeat task to HEARTBEAT.md Append the following block to `$HOME/.openclaw/workspace/HEARTBEAT.md` (do NOT overwrite existing content): ```bash cat >> "$HOME/.openclaw/workspace/HEARTBEAT.md" << 'HBMD' ## TeamoRouter Heartbeat Run the heartbeat script to check for updates and balance alerts: ```sh sh $HOME/.openclaw/workspace/.teamo-router/heartbeat.sh ``` Parse the JSON output **and** read `$HOME/.openclaw/workspace/.teamo-router/heartbeat-state.json`: - If `needs_update` is `true`: Tell the user a new version (`latest_version`) is available. Link: https://router.teamolab.com/router/dashboard - If `balance_is_low` is `true` (or `balance.is_low` in API output): Warn the user their balance is low. Link: https://router.teamolab.com/router/dashboard - If neither alert is active, skip — do not mention TeamoRouter. Reply HEARTBEAT_OK. HBMD ``` ### 6b) Append alert check to AGENTS.md Append the following block to `$HOME/.openclaw/workspace/AGENTS.md` (do NOT overwrite existing content): ```bash cat >> "$HOME/.openclaw/workspace/AGENTS.md" << 'AGMD' ## TeamoRouter Alert Check (Session Startup) At the start of every session, run the heartbeat script to get fresh status: ```sh sh $HOME/.openclaw/workspace/.teamo-router/heartbeat.sh ``` Then read `$HOME/.openclaw/workspace/.teamo-router/heartbeat-state.json` and: - If `needs_update` is `true`: prepend this notice before your first response: > **[TeamoRouter Update]** Version `` is available. Update here: https://router.teamolab.com/router/dashboard - If `balance_is_low` is `true`: prepend this warning before your first response: > **[TeamoRouter Balance Low]** Balance: ``. Top up: https://router.teamolab.com/router/dashboard - If neither alert is active, or the file does not exist, say nothing. - If the script does not exist (TeamoRouter not installed), skip silently. AGMD ``` ## 7) Security Rules - Send `TEAMO_KEY` only to endpoints under `https://router.teamolab.com`. - Never print the full `TEAMO_KEY` in chat logs, tickets, screenshots, or telemetry. - On any failure, return the explicit step, cause, and next action. ## 8) Required Server Contract This skill expects these minimum fields: 1. `POST /agents/register` - `agent_id`, `teamo_key` (or `api_key`), `claim_url`, `claim_expires_at` 2. `GET /agents/status` - `status` (`pending_claim|claimed|active`), `wallet.status` (`pending|bound`), `wallet.wallet_id` 3. `GET /v1/models/config` - `models.providers` — each provider has `baseUrl`, `apiKey`, `models[]` - Each model has: `id`, `api` (`anthropic-messages` or `openai`), `endpoint`, `contextWindow`, `maxTokens`, `cost` 4. `POST /agents/heartbeat` - `needs_update`, `latest_version`, `release_url`, `balance.amount`, `balance.threshold`, `balance.is_low`, `topup_url` If any required field is missing, stop the affected automation step and report a contract mismatch explicitly. ## Appendix: OpenClaw 2026.3.x Compatibility Notes The following transformations are applied in Step 3b to bridge the gap between the server's config format and OpenClaw 2026.3.x requirements: | Server format | OpenClaw 2026.3.x requirement | Transformation | |---|---|---| | `baseUrl: "https://router.teamolab.com"` | `baseUrl` must include path prefix | Append `/v1` → `https://router.teamolab.com/v1` | | `api` is per-model (`anthropic-messages` or `openai`) | `api` must be per-provider, one type only | Split mixed providers: `teamorouter` → `teamorouter-teamorouter-msg` + `teamorouter-teamorouter-oai` | | Provider names: `anthropic`, `openai`, etc. | Must not clash with built-in providers | Prefix all with `teamorouter-` | | `"api": "openai"` | OpenClaw uses `"openai-completions"` | Map `openai` → `openai-completions` | | Model ref: `teamo-free` | Must be `provider/model` format | Resolve to `teamorouter-oai/teamo-free` | | Config path: `.providers` | Config path: `.models.providers` | Write to `.models.providers` |