Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ All notable changes to ClawRouter.
- Endpoint count 84 → 83, removed the Chat section, added the required-param pre-check note up front.
- Fixed param names that the agent would otherwise feed incorrectly and trip the pre-check 400: `social/mindshare` is `q` + `interval` (not `project` + `window`), `search/*` family is `q` (not `query`), `token/holders` + `token/transfers` need `address` AND `chain`, `onchain/gas-price` needs `chain`, `onchain/tx` needs `hash` + `chain`, exchange family universally needs `pair`, market family uses `symbol`, prediction-market endpoints use their specific identifier params (`market_slug`, `event_slug`, `condition_id`, `market_ticker`, `event_ticker`, `ticker`, `address`), and `fund/ranking` + `project/defi/*` need `metric`.
- Reworded the example flows that previously used wrong param names (`search/project?query=ethena` → `?q=ethena`, mindshare example fixed).

- **Phone skill — voice/call `from` is now optional with server-side auto-pick.** blockrun moved `from` from required → optional on `/v1/voice/call`. After payment verification the server picks a caller-ID from the wallet's owned numbers: 0 active → `403 no_active_number` with buy-first hint, exactly 1 → auto-used, 2+ → `400 ambiguous_from` listing all candidates. The prior skill said "Otherwise Bland picks one" — wrong, and would have made agents leave `from` off and confuse users with the 403/400 responses. Updated `skills/phone/SKILL.md` with the actual auto-pick rule table and the ownership-mismatch 403 behavior.
- **No code change for `real_face_asset_id`, no new partner tools, no proxy whitelist change.** Surf + Phone are both base+skill integrations (per the v0.12.193 rule). All blockrun upstream changes here flow through the existing `proxyPaidApiRequest` path transparently.

Expand Down Expand Up @@ -87,7 +88,7 @@ All notable changes to ClawRouter.
- **Bug 1 — phantom $0.54 charge on 4xx voice POST.** First smoke test POSTed `/v1/voice/call` with empty `{}` body to exercise routing without spending money. BlockRun returned 400 (Zod validation: "expected string, received undefined"). The wallet wasn't charged, but the telemetry hook saw `paymentStore.amountUsd = 0` and fell back to `estimatePhoneCost("/v1/voice/call") = $0.54`. Stats would record a phantom voice call. Fix: gate the fallback on `upstream.status` being 2xx — any 4xx/5xx skips the fallback and logs `$0`.
- **Bug 2 — GET poll miscounted as another $0.54 voice call.** After placing a real call, polling `GET /v1/voice/call/{call_id}` for transcript status (free upstream) was being logged at $0.54 because the longest-prefix match on `voice/call/` triggered the same fallback row as the initiating POST. Every 30s poll would inflate stats by $0.54. Fix: also gate the fallback on `req.method === "POST"` — GET polls log `$0`.
- **Refactor**: gate logic was originally inline inside `proxyPaidApiRequest`. Pulled it out into `resolvePhoneTelemetryCost(args)` so the rules are independently testable (the call site is now four lines passing an args bag through the helper). Adds 8 vitest cases covering: paid-amount-wins, 4xx phantom guard, GET poll guard, 5xx guard, missing-method guard, non-phone-passthrough, and the original "successful POST with empty paymentStore → fallback" path. Without the helper extraction, locking these gates in tests would have required a full integration test with a mocked upstream — too heavy for telemetry-only logic.
- **Tests** — new `src/proxy.phone-routing.test.ts` (regex matching for /v1/phone/*, /v1/voice/*, /v1/voice/call/{id} poll, plus negative case for /v1/phonebook), `src/proxy.phone-pricing.test.ts` (longest-prefix matching + the 8 `resolvePhoneTelemetryCost` gate cases above), `src/parse-call-args.test.ts` (both flag forms, quoted task spans, E.164 first-token detection). Total 31 new test cases; all 569 vitest tests green; typecheck + lint clean.
- **Tests** — new `src/proxy.phone-routing.test.ts` (regex matching for /v1/phone/\*, /v1/voice/\*, /v1/voice/call/{id} poll, plus negative case for /v1/phonebook), `src/proxy.phone-pricing.test.ts` (longest-prefix matching + the 8 `resolvePhoneTelemetryCost` gate cases above), `src/parse-call-args.test.ts` (both flag forms, quoted task spans, E.164 first-token detection). Total 31 new test cases; all 569 vitest tests green; typecheck + lint clean.
- **Smoke test record** (free-tier verification before the real call): list-numbers ($0.001) returned an existing wallet-owned number `+15707043521` (PA, expires 2026-06-15); lookup ($0.01) on that same number returned full Twilio carrier metadata (`type: nonFixedVoip`, `carrier_name: Twilio - SMS/MMS-SVR`); negative test `/v1/phonebook/test` correctly rejected by the partner regex (502 from chat-completion fallback rather than partner routing); CLI table formatting + expiry-warning logic verified by `clawrouter phone numbers list`.

---
Expand Down
30 changes: 15 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,16 @@ Verify phone numbers and place AI-powered outbound voice calls directly from cha

Calls are **fire-and-forget**: the request returns a `call_id` and `poll_url` immediately. The call itself runs in the cloud for up to 30 minutes. Poll `GET /v1/voice/call/{call_id}` (or `clawrouter share`/transcripts dashboard) to retrieve the transcript and recording when status is `completed`.

| Operation | Provider | Price |
| --------------------------------- | -------- | ---------------------------- |
| Phone lookup (carrier, line type) | Twilio | $0.01 |
| Fraud check (SIM-swap, fwd) | Twilio | $0.05 |
| Buy phone number (30-day lease) | Twilio | $5.00 |
| Renew lease (+30 days) | Twilio | $5.00 |
| List wallet's owned numbers | Twilio | $0.001 |
| Release a number | Twilio | free |
| **AI voice call (≤30 min)** | Bland.ai | **$0.54 flat per call** |
| Poll call status / transcript | Bland.ai | free |
| Operation | Provider | Price |
| --------------------------------- | -------- | ----------------------- |
| Phone lookup (carrier, line type) | Twilio | $0.01 |
| Fraud check (SIM-swap, fwd) | Twilio | $0.05 |
| Buy phone number (30-day lease) | Twilio | $5.00 |
| Renew lease (+30 days) | Twilio | $5.00 |
| List wallet's owned numbers | Twilio | $0.001 |
| Release a number | Twilio | free |
| **AI voice call (≤30 min)** | Bland.ai | **$0.54 flat per call** |
| Poll call status / transcript | Bland.ai | free |

**CLI for wallet-owned numbers:**

Expand Down Expand Up @@ -354,11 +354,11 @@ Surf is BlockRun's unified crypto data API — **84 endpoints across 13 domains*

ClawRouter ships Surf as a **skill, not as typed wrappers**. The proxy whitelists `/v1/surf/*` so any call through the local proxy is paid x402 from the same wallet; the agent reads `skills/surf/SKILL.md` for the endpoint catalog and crafts the HTTP call. No `blockrun_surf_*` tool definitions to maintain; a new Surf endpoint requires zero ClawRouter release.

| Tier | Cost | Examples |
| ---- | --------: | ------------------------------------------------------------- |
| 1 | **$0.001**| prices, rankings, lists, news |
| 2 | **$0.005**| orderbooks, candles, search, wallet details, social mindshare |
| 3 | **$0.020**| on-chain SQL / query / schema, chat completions |
| Tier | Cost | Examples |
| ---- | ---------: | ------------------------------------------------------------- |
| 1 | **$0.001** | prices, rankings, lists, news |
| 2 | **$0.005** | orderbooks, candles, search, wallet details, social mindshare |
| 3 | **$0.020** | on-chain SQL / query / schema, chat completions |

**Usage (HTTP):**

Expand Down
162 changes: 153 additions & 9 deletions scripts/update.sh
Original file line number Diff line number Diff line change
Expand Up @@ -324,32 +324,164 @@ for stale in "$HOME/.openclaw/extensions/clawrouter.backup."* "$HOME/.openclaw/e
[ -d "$stale" ] && rm -rf "$stale"
done

apply_scoped_model_trim() {
local rejected_path="$1"
if [ -z "$rejected_path" ] || [ ! -f "$rejected_path" ] || [ ! -f "$CONFIG_PATH" ]; then
return 1
fi

CONFIG_PATH="$CONFIG_PATH" REJECTED_CONFIG_PATH="$rejected_path" node <<'NODE'
const fs = require('fs');

const activePath = process.env.CONFIG_PATH;
const rejectedPath = process.env.REJECTED_CONFIG_PATH;

function fail(message) {
console.log(` Skipped scoped config trim: ${message}`);
process.exit(1);
}

function byteSize(value) {
return Buffer.byteLength(JSON.stringify(value ?? null, null, 2));
}

function objectKeys(value) {
return value && typeof value === 'object' && !Array.isArray(value) ? Object.keys(value).sort() : [];
}

function clone(value) {
return JSON.parse(JSON.stringify(value));
}

function getBlockrunModels(config) {
return config?.models?.providers?.blockrun?.models;
}

const active = JSON.parse(fs.readFileSync(activePath, 'utf8'));
const rejected = JSON.parse(fs.readFileSync(rejectedPath, 'utf8'));

const activeTopKeys = objectKeys(active);
const rejectedTopKeys = objectKeys(rejected);
if (activeTopKeys.join('\0') !== rejectedTopKeys.join('\0')) {
fail('top-level config keys changed');
}

for (const key of ['auth', 'channels', 'gateway', 'plugins', 'models']) {
if (!(key in active) || !(key in rejected)) fail(`missing required ${key} section`);
}

const activeModels = getBlockrunModels(active);
const rejectedModels = getBlockrunModels(rejected);
if (!Array.isArray(activeModels) || !Array.isArray(rejectedModels)) {
fail('blockrun model list is missing or invalid');
}

if (activeModels.length <= rejectedModels.length) {
fail(`model count did not shrink (${activeModels.length} -> ${rejectedModels.length})`);
}

if (rejectedModels.length < 20 || rejectedModels.length > 100) {
fail(`unexpected curated model count (${rejectedModels.length})`);
}

for (const key of activeTopKeys) {
if (key === 'models') continue;
const delta = Math.abs(byteSize(active[key]) - byteSize(rejected[key]));
if (delta > 2048) fail(`non-model section changed too much: ${key}`);
}

const activeWithoutModelList = clone(active.models);
const rejectedWithoutModelList = clone(rejected.models);
activeWithoutModelList.providers.blockrun.models = [];
rejectedWithoutModelList.providers.blockrun.models = [];
const residualModelDelta = Math.abs(byteSize(activeWithoutModelList) - byteSize(rejectedWithoutModelList));
if (residualModelDelta > 4096) {
fail('models section changed beyond the blockrun model list');
}

const totalDrop = byteSize(active) - byteSize(rejected);
const modelListDrop = byteSize(activeModels) - byteSize(rejectedModels);
if (totalDrop <= 0 || modelListDrop / totalDrop < 0.65) {
fail('size drop is not primarily from the blockrun model list');
}

active.models.providers.blockrun.models = rejectedModels;
const tmpPath = `${activePath}.tmp.${process.pid}`;
fs.writeFileSync(tmpPath, JSON.stringify(active, null, 2));
fs.renameSync(tmpPath, activePath);

console.log(
` ✓ Applied scoped BlockRun model-list trim (${activeModels.length} -> ${rejectedModels.length})`,
);
NODE
}

handle_openclaw_install_failure() {
local exit_code="$1"
if [ "$exit_code" -eq 124 ]; then
echo " (install command timed out — this is normal with OpenClaw v2026.4.5)"
if [ -f "$PLUGIN_DIR/package.json" ]; then
echo " Plugin package.json is present; treating install as completed before the hang."
return 0
fi
echo " Plugin package.json is missing after timeout; continuing with direct npm install."
OPENCLAW_INSTALL_RECOVERABLE=1
return "$exit_code"
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.

if grep -q "Config write rejected: .*size-drop:" "$OPENCLAW_INSTALL_LOG"; then
local rejected_path
rejected_path=$(node -e "const fs=require('fs'); const text=fs.readFileSync(process.argv[1],'utf8'); const m=text.match(/Rejected payload saved to ([^\\s]+)\\./); if (m) console.log(m[1]);" "$OPENCLAW_INSTALL_LOG")
echo " ⚠ OpenClaw rejected a config size-drop during plugin registration."
if apply_scoped_model_trim "$rejected_path"; then
echo " Continuing with direct npm install after the scoped config update."
else
echo " Continuing with direct npm install while preserving your existing config."
fi
OPENCLAW_INSTALL_RECOVERABLE=1
return 0
fi

return "$exit_code"
}

echo "→ Installing latest ClawRouter..."
# `--force` is required when the plugin is already installed at the same path
# (which is always true on update). Without it, OpenClaw exits non-zero with
# "plugin already exists" and our EXIT trap rolls back, stranding the user on
# the previous version. `--force` is idempotent for both fresh + upgrade flows.
#
# OpenClaw can also reject its own config rewrite when it would shrink a large
# user config. Treat that as recoverable: keep the user's restored config and
# install ClawRouter files directly from npm below.
OPENCLAW_INSTALL_RECOVERABLE=0
OPENCLAW_INSTALL_LOG="$(mktemp)"
#
# Run with timeout — openclaw plugins install may hang after printing
# "Installed plugin: clawrouter" in OpenClaw v2026.4.5 (parallel plugin loading).
# 120s is enough for slow connections; the install itself completes in ~30s.
if command -v timeout >/dev/null 2>&1; then
timeout 120 openclaw plugins install --force @blockrun/clawrouter || {
timeout 120 openclaw plugins install --force @blockrun/clawrouter 2>&1 | tee "$OPENCLAW_INSTALL_LOG" || {
exit_code=$?
if [ $exit_code -eq 124 ]; then
echo " (install command timed out — this is normal with OpenClaw v2026.4.5)"
echo " Plugin was installed successfully before the hang."
else
exit $exit_code
fi
handle_openclaw_install_failure "$exit_code" || {
[ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ] || exit $exit_code
}
}
else
openclaw plugins install --force @blockrun/clawrouter
openclaw plugins install --force @blockrun/clawrouter 2>&1 | tee "$OPENCLAW_INSTALL_LOG" || {
exit_code=$?
handle_openclaw_install_failure "$exit_code" || {
[ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ] || exit $exit_code
}
}
fi
rm -f "$OPENCLAW_INSTALL_LOG"

# Install is complete — clear the rollback trap immediately.
# From this point on, Ctrl+C or errors should NOT roll back the install.
trap - EXIT INT TERM
if [ "$OPENCLAW_INSTALL_RECOVERABLE" != "1" ]; then
trap - EXIT INT TERM
fi

# Restore credentials after plugin install (always restore to preserve user's channels)
if [ -n "$CREDS_BACKUP" ] && [ -d "$CREDS_BACKUP" ]; then
Expand Down Expand Up @@ -431,6 +563,18 @@ if [ -d "$PLUGIN_DIR" ] && [ -f "$PLUGIN_DIR/package.json" ]; then
fi
INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('?');}" 2>/dev/null || echo "?")
echo " ✓ ClawRouter v${INSTALLED_VER} installed"
elif [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ]; then
LATEST_VER=$(npm view @blockrun/clawrouter@latest version 2>/dev/null || echo "")
if [ -z "$LATEST_VER" ]; then
echo " ✗ Could not resolve latest ClawRouter version from npm"
exit 1
fi
force_install_from_npm "$LATEST_VER"
INSTALLED_VER=$(node -e "try{const p=require('$PLUGIN_DIR/package.json');console.log(p.version);}catch{console.log('?');}" 2>/dev/null || echo "?")
echo " ✓ ClawRouter v${INSTALLED_VER} installed"
fi
if [ "$OPENCLAW_INSTALL_RECOVERABLE" = "1" ]; then
trap - EXIT INT TERM
fi
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

# ── Step 4c: Ensure all dependencies are installed ────────────
Expand Down
Loading
Loading