Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
24 changes: 24 additions & 0 deletions .github/workflows/brand-numbers.yml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to blockrun-llm-go will be documented in this file.

## 0.19.1

- **perf(solana): drop both RPC round-trips from the x402 payment critical
path.** Mirrors the TS SDK fast path. USDC mint info (token program +
decimals) is hardcoded — SPL Token fixes `decimals` in `InitializeMint` and
ships no instruction to change it — so `getAccountInfo` is gone outright for
USDC (non-USDC mints still resolve over RPC). `getLatestBlockhash` is cached
per RPC endpoint with a 10s TTL (bounded to 8 endpoints). Unlike the TS SDK,
no duplicate-transaction guard is needed when a blockhash is reused: every
transaction carries a random 16-byte memo nonce, so two payments can never
be byte-identical. Measured on paid gpt-4o-mini calls via `sol.blockrun.ai`:
median 2.16s → 1.72s, mean 3.57s → 1.74s, worst case 9.68s → 2.31s (the old
tail was slow serial RPC).

## 0.19.0

- **Solana (SVM) x402 payments.** Every client can now pay USDC on Solana via
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,12 @@ co-signs (gasless) and settles. Constructors: `NewLLMClientSolana`,

| Feature | Description |
|---------|-------------|
| **Chat & Completion** | OpenAI-compatible chat with 40+ models |
| **Chat & Completion** | OpenAI-compatible chat with <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible --> models |
| **Anthropic Client** | Native Anthropic Messages API with automatic x402 payments |
| **Smart Routing** | Auto-selects the best model for your prompt |
| **Streaming** | SSE streaming for real-time responses |
| **Tool Calling** | OpenAI-compatible function/tool calling |
| **Multi-chain RPC** | JSON-RPC 2.0 to 40+ chains, $0.002/call |
| **Multi-chain RPC** | JSON-RPC 2.0 to <!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains, $0.002/call |
| **Web Search** | Search web, X/Twitter, and news |
| **Prediction Markets** | Polymarket, Kalshi data access |
| **Image Generation** | DALL-E 3, GPT Image 1/2, Nano Banana, Flux, CogView-4, Grok Imagine |
Expand Down Expand Up @@ -423,7 +423,7 @@ ie, lu, cn, ca`.
## Multi-chain RPC

`RPCClient` wraps `POST /v1/rpc/{network}` — standard JSON-RPC 2.0 access to
40+ chains through one endpoint (Ethereum, Base, Solana, Polygon, BSC,
<!-- br:chains.rpc -->40<!-- /br:chains.rpc --> chains through one endpoint (Ethereum, Base, Solana, Polygon, BSC,
Arbitrum, Optimism, Avalanche, Bitcoin, Sui, and more; powered by Tatum's RPC
gateway). No API key, no per-chain endpoints: flat **$0.002 per call** in
USDC; a JSON-RPC batch charges per element.
Expand Down Expand Up @@ -919,7 +919,7 @@ for _, w := range wallets {
| **ElevenLabs** | Flash v2.5, Turbo v2.5, Multilingual v2, v3 (TTS $0.05–0.10/1k chars), Sound Effects ($0.05/gen) | — | — |
| **Moonshot** | Kimi K2.6 (256K, vision + reasoning) | $0.95 | $4.00 |
| **Moonshot** | Kimi K2.5 (262K context, legacy) | $0.60 | $3.00 |
| **NVIDIA** | DeepSeek V4 Pro/Flash, Nemotron Nano Omni (vision), Qwen3, Llama 4, GLM-4.7, Mistral (9 models) | **FREE** | **FREE** |
| **NVIDIA** | DeepSeek V4 Pro/Flash, Nemotron Nano Omni (vision), Qwen3, Llama 4, GLM-4.7, Mistral (<!-- br:models.free -->8<!-- /br:models.free --> models) | **FREE** | **FREE** |

Use `client.ListModels(ctx)` for the full list with current pricing.

Expand Down Expand Up @@ -963,7 +963,7 @@ if err != nil {
## FAQ

**What is blockrun-llm-go?**
A Go SDK for pay-per-request access to 40+ LLMs, multi-chain RPC, web search, prediction markets, and image generation. Uses x402 micropayments — no API keys, no subscriptions.
A Go SDK for pay-per-request access to <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible --> LLMs, multi-chain RPC, web search, prediction markets, and image generation. Uses x402 micropayments — no API keys, no subscriptions.

**How much does it cost?**
Pay only for what you use. 9 NVIDIA-hosted models are completely free (DeepSeek V4 Pro/Flash, Nemotron Nano Omni vision, Qwen3, Llama 4, GLM-4.7, Mistral). $5 USDC gets you thousands of paid-model requests.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.19.0
0.19.1
34 changes: 34 additions & 0 deletions brand-numbers.json
Original file line number Diff line number Diff line change
@@ -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
}
}
270 changes: 270 additions & 0 deletions scripts/sync-brand-numbers.mjs
Original file line number Diff line number Diff line change
@@ -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: <!-- br:models.chatVisible -->66<!-- /br:models.chatVisible -->
* 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` — `<!-- br:mcp.tools@badge -->` — 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) =>
`<img src="https://img.shields.io/badge/${label}-${n}-5B9BF6?style=flat-square&labelColor=0B0A0F" alt="${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 = /<!--\s*br:([A-Za-z0-9_.@]+)\s*-->/g;
const CLOSE_ANY = /<!--\s*\/br:([A-Za-z0-9_.@]+)\s*-->/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) => `<!-- br:${n} -->`],
[CLOSE_ANY, (n) => `<!-- /br:${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*br:${esc(marker)}\\s*-->)([\\s\\S]*?)(<!--\\s*/br:${esc(marker)}\\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 (/<!--\s*\/?br:/.test(inner)) {
problems.push(`${rel}: nested marker inside br:${marker}`);
return whole;
}
used.add(marker);
return open + render(marker, value) + close;
});

// An opening tag with no partner silently swallows the rest of the file on
// a naive regex, so catch it explicitly.
const opens = [...before.matchAll(new RegExp(`<!--\\s*br:${esc(marker)}\\s*-->`, "g"))]
.filter((m) => !inFence(m.index)).length;
const closes = [...before.matchAll(new RegExp(`<!--\\s*/br:${esc(marker)}\\s*-->`, "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)`,
);
Loading
Loading