From 7f2eb7482f7694c6241c2c5fed6d9a37a8721940 Mon Sep 17 00:00:00 2001 From: 1bcMax Date: Mon, 27 Jul 2026 16:04:10 -0700 Subject: [PATCH] feat: consume the published brand-numbers artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The model count appeared in AGENTS.md, CLAUDE.md, CONTEXT.md, the README and three docs pages, each maintained by hand and all stale at 55+ against a catalog of 66. Franklin also claimed "up to 89% savings vs. always using Opus" — a figure with no published method behind it. They now regenerate from blockrun.ai/brand/numbers.json via markers, and CI fails offline against a committed snapshot when they disagree. Only git-tracked files are rewritten. Franklin-Trading has an ignored directory literally named " " holding a stale copy of its own docs; walking the working tree would have edited files nobody ships. --- .github/workflows/brand-numbers.yml | 24 ++ AGENTS.md | 2 +- CLAUDE.md | 4 +- CONTEXT.md | 2 +- README.md | 2 +- brand-numbers.json | 34 +++ .../0002-blockrun-gateway-as-aggregator.md | 2 +- docs/anatomy-of-an-economic-agent.md | 2 +- docs/why-ai-agents-need-a-wallet.md | 4 +- scripts/sync-brand-numbers.mjs | 270 ++++++++++++++++++ 10 files changed, 337 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/brand-numbers.yml create mode 100644 brand-numbers.json create mode 100644 scripts/sync-brand-numbers.mjs diff --git a/.github/workflows/brand-numbers.yml b/.github/workflows/brand-numbers.yml new file mode 100644 index 0000000..c1f63f9 --- /dev/null +++ b/.github/workflows/brand-numbers.yml @@ -0,0 +1,24 @@ +name: Brand numbers + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +# Fails when a marketing number in this repo disagrees with brand-numbers.json. +# +# --check is deliberately OFFLINE. It compares against the committed snapshot +# and never fetches, so a blockrun.ai deploy in progress cannot fail this repo's +# CI. Pulling a newer artifact is a separate, deliberate act: +# +# node scripts/sync-brand-numbers.mjs --refresh +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: node scripts/sync-brand-numbers.mjs --check diff --git a/AGENTS.md b/AGENTS.md index f05d77f..cd319f2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ # Franklin Agent -Open-source AI agent with a wallet. 55+ models. Pay per use with USDC via x402. +Open-source AI agent with a wallet. 66 models. Pay per use with USDC via x402. ## Commands diff --git a/CLAUDE.md b/CLAUDE.md index 920e1a7..6c09963 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,13 +5,13 @@ Franklin is the first AI agent in the **Autonomous Economic Agent** category — it doesn't just write text, it autonomously spends USDC from a user-funded wallet to execute real work: coding, trading, content generation. Three verticals under one brand: -- **Dev agent** — coding, debugging, review across 55+ models with wallet-bound spending +- **Dev agent** — coding, debugging, review across 66 models with wallet-bound spending - **Trading agent** — signals, portfolio, risk, persistent P&L across sessions - **Content agent** — ImageGen, VideoGen, budget-tracked media production Built on three layers: 1. **x402 micropayment protocol** — HTTP 402 native payments -2. **BlockRun Gateway** — aggregates 55+ LLMs + paid APIs (Exa, DALL-E, future Runway/Suno/CoinGecko) +2. **BlockRun Gateway** — aggregates 66 LLMs + paid APIs (Exa, DALL-E, future Runway/Suno/CoinGecko) 3. **Franklin Agent** — this repo, the reference client ## Commands diff --git a/CONTEXT.md b/CONTEXT.md index d23505c..aac2a01 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -19,7 +19,7 @@ The first, unpaid HTTP request whose 402 response carries the payment terms; Fra _Avoid_: Pre-flight, handshake. **BlockRun Gateway**: -The single upstream service Franklin calls for both LLM completions and paid tools (Exa, ImageGen, VideoGen, MusicGen, market data); aggregates 55+ models and accepts x402. +The single upstream service Franklin calls for both LLM completions and paid tools (Exa, ImageGen, VideoGen, MusicGen, market data); aggregates 66 models and accepts x402. _Avoid_: API, provider, backend, BlockRun (without "Gateway") when referring to the service. **Per-turn spend cap**: diff --git a/README.md b/README.md index 0ac8f44..7f2022f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Fund the wallet. Set a budget. Walk away — and come back to a book. > Franklin Trading is a fork of [Franklin](https://github.com/BlockRunAI/Franklin) — the > general-purpose Autonomous Economic Agent — specialized as a wallet-native trading > agent. It inherits Franklin's economic substrate (x402 micropayments, USDC settlement, -> learned-weight model router across 55+ models, removable-by-design harness components) +> learned-weight model router across 66 models, removable-by-design harness components) > and adds a multi-role persona debate, a unified Backtest → Paper → Live strategy > lifecycle, multi-venue on-chain execution, and four moat layers nobody else ships. diff --git a/brand-numbers.json b/brand-numbers.json new file mode 100644 index 0000000..312505d --- /dev/null +++ b/brand-numbers.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://blockrun.ai/brand/numbers.schema.json", + "version": 1, + "models": { + "chatVisible": 66, + "totalVisible": 86, + "free": 8, + "freeWithheld": 17, + "image": 8, + "video": 5, + "music": 1, + "speech": 5, + "soundfx": 1, + "withFallback": 44, + "withFallbackAllEntries": 75 + }, + "clawrouter": { + "dimensions": 15, + "tiers": 4, + "profiles": 4, + "aliases": 202 + }, + "mcp": { + "tools": 19 + }, + "chains": { + "rpc": 40 + }, + "savings": { + "baselineModel": "anthropic/claude-opus-5", + "ecoVsBaselinePct": 98, + "autoVsBaselinePct": 87 + } +} diff --git a/docs/adr/0002-blockrun-gateway-as-aggregator.md b/docs/adr/0002-blockrun-gateway-as-aggregator.md index 32734b3..31cb9d1 100644 --- a/docs/adr/0002-blockrun-gateway-as-aggregator.md +++ b/docs/adr/0002-blockrun-gateway-as-aggregator.md @@ -2,7 +2,7 @@ **Status:** accepted -Franklin makes every LLM and paid-tool call through a single counterparty — the BlockRun Gateway — instead of integrating directly with each model provider, image/video/audio service, search API, and market-data feed. The gateway aggregates 55+ models and a small set of paid APIs and accepts x402 uniformly. +Franklin makes every LLM and paid-tool call through a single counterparty — the BlockRun Gateway — instead of integrating directly with each model provider, image/video/audio service, search API, and market-data feed. The gateway aggregates 66 models and a small set of paid APIs and accepts x402 uniformly. ## Considered options diff --git a/docs/anatomy-of-an-economic-agent.md b/docs/anatomy-of-an-economic-agent.md index abc3ea4..5e88843 100644 --- a/docs/anatomy-of-an-economic-agent.md +++ b/docs/anatomy-of-an-economic-agent.md @@ -326,7 +326,7 @@ seamlessly. │ signed USDC (HTTP 402) ▼ BlockRun Gateway - 55+ models / paid APIs + 66 models / paid APIs │ ▼ User Wallet diff --git a/docs/why-ai-agents-need-a-wallet.md b/docs/why-ai-agents-need-a-wallet.md index cb61e47..c90ff09 100644 --- a/docs/why-ai-agents-need-a-wallet.md +++ b/docs/why-ai-agents-need-a-wallet.md @@ -154,7 +154,7 @@ cheap models because cheapness is now something the agent can agents both learn in real time. **3. Single-vendor failure becomes a one-command swap.** A router -with 55+ models across every major provider can route around any +with 66 models across every major provider can route around any single vendor's bad release. The wallet doesn't know or care which model answered — it only pays for the answer that arrived. @@ -175,7 +175,7 @@ filled it. Franklin is the reference implementation of the Economic Agent. It's an AI agent CLI that holds USDC on Base or Solana, routes requests -across 55+ models, and settles every paid action in real time via +across 66 models, and settles every paid action in real time via the [x402](https://x402.org) HTTP-402 micropayment protocol. It is Apache-2.0, written in TypeScript, and ships as one npm package. diff --git a/scripts/sync-brand-numbers.mjs b/scripts/sync-brand-numbers.mjs new file mode 100644 index 0000000..c3717f4 --- /dev/null +++ b/scripts/sync-brand-numbers.mjs @@ -0,0 +1,270 @@ +#!/usr/bin/env node +/** + * Sync marketing numbers from BlockRun's canonical brand artifact. + * + * This file is copied byte-for-byte into every public repo as + * scripts/sync-brand-numbers.mjs. It is a copy rather than an npm package on + * purpose: a package would mean 37 dependency bumps, and several consuming + * repos have no package.json at all. Zero dependencies, plain Node. + * + * node scripts/sync-brand-numbers.mjs rewrite markers in place + * node scripts/sync-brand-numbers.mjs --check exit 1 on drift, write nothing + * node scripts/sync-brand-numbers.mjs --refresh re-fetch the artifact first + * + * --check NEVER touches the network. PR CI must be deterministic and offline: + * if it fetched, a deploy in progress would fail every repo in the org at once. + * Freshness is the fan-out job's problem, not the pull request's. + * + * Markers look like: 66 + * and wrap the WHOLE token, so a badge URL, its alt text and the prose number + * can all regenerate from one key. + */ +import { existsSync, lstatSync, readFileSync, writeFileSync, readdirSync } from "node:fs"; +import { join, relative, extname } from "node:path"; + +const ROOT = process.cwd(); +const SNAPSHOT = join(ROOT, "brand-numbers.json"); +// ORIGIN is tried first because it IS the truth — the mirror can only ever be +// as fresh as the last time someone refreshed it. The mirror exists so a repo +// can still sync while blockrun.ai is down, not to front the origin. +// +// The mirror is awesome-blockrun's own brand-numbers.json: that repo consumes +// the artifact like every other, and its snapshot doubles as the org's copy. +// One file, one role per repo, nothing to keep in step by hand. +const ORIGIN = "https://blockrun.ai/brand/numbers.json"; +const MIRROR = + "https://raw.githubusercontent.com/BlockRunAI/awesome-blockrun/main/brand-numbers.json"; + +const argv = new Set(process.argv.slice(2)); +const check = argv.has("--check"); +const refresh = argv.has("--refresh"); + +const SKIP_DIRS = new Set([ + "node_modules", ".git", "dist", "build", "out", ".next", "coverage", + "vendor", "target", "__pycache__", ".venv", "venv", +]); +// .txt is here for llms.txt, which is a first-class marketing surface: it is +// what agents read to find out what BlockRun serves. Scanning other .txt files +// costs a read and changes nothing — only files with markers are ever written. +const TEXT_EXT = new Set([".md", ".mdx", ".txt"]); + +/* ── 1. numbers ──────────────────────────────────────────────────────────── */ + +async function loadNumbers() { + if (!refresh) { + try { + return JSON.parse(readFileSync(SNAPSHOT, "utf8")); + } catch { + fail( + `no brand-numbers.json in ${ROOT}\n` + + ` run with --refresh once to seed it from ${ORIGIN}`, + ); + } + } + for (const url of [ORIGIN, MIRROR]) { + try { + const res = await fetch(url, { signal: AbortSignal.timeout(10_000) }); + if (!res.ok) continue; + const json = await res.json(); + writeFileSync(SNAPSHOT, `${JSON.stringify(json, null, 2)}\n`); + return json; + } catch { + /* try the next source */ + } + } + fail(`could not refresh from ${MIRROR} or ${ORIGIN}`); +} + +/** Flatten nested numbers into dotted keys, ignoring $comment / rationale prose. */ +function flatten(obj, prefix = "") { + return Object.entries(obj).flatMap(([k, v]) => { + if (k.startsWith("$")) return []; + const key = `${prefix}${k}`; + if (v && typeof v === "object" && !Array.isArray(v)) return flatten(v, `${key}.`); + if (v === null) return []; + return [[key, v]]; + }); +} + +/* ── 2. renderers ────────────────────────────────────────────────────────── */ + +/** + * How a key becomes text. Default is the bare value. + * + * A marker may carry an `@modifier` — `` — which + * selects a renderer without changing which number is looked up. The modifier + * is what makes a key reusable: the same mcp.tools appears as a shields badge + * at the top of a README and as a bare "19 tools" in a table two screens down, + * and one marker still keeps the badge URL, its alt text and the label in step. + * + * Renderers are registered under the FULL marker name so a badge's label is + * written out rather than guessed from the key. + */ +const badge = (label) => (n) => + `${n} ${label}`; + +const RENDER = { + "mcp.tools@badge": badge("tools"), + "models.totalVisible@badge": badge("models"), + "models.chatVisible@badge": badge("models"), +}; +const render = (marker, value) => (RENDER[marker] ?? String)(value); + +/** `mcp.tools@badge` looks up `mcp.tools`. Unmodified markers are unaffected. */ +const keyOf = (marker) => marker.split("@")[0]; + +/* ── 3. marker rewriting ─────────────────────────────────────────────────── */ + +const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +const OPEN_ANY = //g; +const CLOSE_ANY = //g; + +/** Byte ranges of fenced code blocks — markers inside them are documentation. */ +function fencedRanges(text) { + const ranges = []; + const fence = /^(\s*)(`{3,}|~{3,})[^\n]*$/gm; + let open = null; + for (let m; (m = fence.exec(text)); ) { + if (open === null) open = m.index; + else { + ranges.push([open, m.index + m[0].length]); + open = null; + } + } + return ranges; +} + +function syncFile(file, numbers, problems) { + const before = readFileSync(file, "utf8"); + const rel = relative(ROOT, file); + const fenced = fencedRanges(before); + const inFence = (i) => fenced.some(([a, b]) => i >= a && i < b); + const known = new Map(numbers); + const used = new Set(); + + // Markers actually present, so a file is only ever rewritten for what it uses + // and an @modifier is carried through to the renderer verbatim. + const markers = new Set(); + // A marker naming a key that does not exist is an error, never a silent + // no-op: a typo'd marker would otherwise sit there looking synced forever. + for (const [re, shown] of [ + [OPEN_ANY, (n) => ``], + [CLOSE_ANY, (n) => ``], + ]) { + for (const m of before.matchAll(re)) { + if (inFence(m.index)) continue; + markers.add(m[1]); + if (!known.has(keyOf(m[1]))) problems.push(`${rel}: unknown key ${shown(m[1])}`); + } + } + + let after = before; + for (const marker of markers) { + const key = keyOf(marker); + if (!known.has(key)) continue; + const value = known.get(key); + const pair = new RegExp( + `()([\\s\\S]*?)()`, + "g", + ); + after = after.replace(pair, (whole, open, inner, close, offset) => { + if (inFence(offset)) return whole; + // Nesting means the closing tag of an inner marker would be consumed by + // the outer one. Refuse rather than produce mangled output. + if (/`, "g"))] + .filter((m) => !inFence(m.index)).length; + const closes = [...before.matchAll(new RegExp(``, "g"))] + .filter((m) => !inFence(m.index)).length; + if (opens !== closes) problems.push(`${rel}: unbalanced marker br:${marker} (${opens} open, ${closes} close)`); + } + + return { before, after, changed: before !== after, used }; +} + +/* ── 4. walk ─────────────────────────────────────────────────────────────── */ + +function* walk(dir) { + for (const name of readdirSync(dir)) { + if (SKIP_DIRS.has(name)) continue; + const p = join(dir, name); + // lstat, not stat: a symlinked directory is reached by its real path or not + // at all. blockrun's docs/ -> awesome-blockrun/docs is exactly the case that + // matters — following it would edit a submodule's files behind the skip + // below, and a link pointing at an ancestor would recurse forever. + const s = lstatSync(p); + if (s.isSymbolicLink()) continue; + if (s.isDirectory()) { + // A nested repo is a submodule or vendored checkout: it carries its own + // brand-numbers.json and syncs itself. Rewriting its markers from THIS + // repo's snapshot would dirty a submodule nobody asked us to touch, and + // would report drift that belongs to another repo's CI. + if (existsSync(join(p, ".git"))) continue; + yield* walk(p); + } else if (TEXT_EXT.has(extname(name))) yield p; + } +} + +function fail(msg) { + console.error(`brand-numbers: ${msg}`); + process.exit(1); +} + +/* ── 5. run ──────────────────────────────────────────────────────────────── */ + +const raw = await loadNumbers(); +const numbers = flatten(raw); +const problems = []; +const drifted = []; +const everUsed = new Set(); + +for (const file of walk(ROOT)) { + const { before, after, changed, used } = syncFile(file, numbers, problems); + used.forEach((k) => everUsed.add(k)); + if (!changed) continue; + drifted.push({ file: relative(ROOT, file), before, after }); + if (!check) writeFileSync(file, after); +} + +if (problems.length) { + for (const p of problems) console.error(` ${p}`); + fail(`${problems.length} marker problem(s)`); +} + +if (check) { + if (drifted.length === 0) { + console.log(`brand-numbers: up to date (${everUsed.size} keys in use)`); + process.exit(0); + } + console.error("brand-numbers: these files disagree with brand-numbers.json\n"); + for (const { file, before, after } of drifted) { + const b = before.split("\n"); + const a = after.split("\n"); + for (let i = 0; i < Math.max(b.length, a.length); i++) { + if (b[i] !== a[i]) { + console.error(` ${file}:${i + 1}`); + console.error(` - ${(b[i] ?? "").trim()}`); + console.error(` + ${(a[i] ?? "").trim()}`); + } + } + } + console.error( + "\n fix with: node scripts/sync-brand-numbers.mjs && git commit -am 'chore: sync brand numbers'", + ); + process.exit(1); +} + +console.log( + drifted.length + ? `brand-numbers: updated ${drifted.length} file(s)` + : `brand-numbers: already up to date (${everUsed.size} keys in use)`, +);