Skip to content
Open
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
64 changes: 49 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@

# Franklin Trading

**The AI trading agent with a wallet.**
**The AI trading agent with account API and wallet support.**

Researches, debates, paper-trades against real prices, and settles every paid call in USDC.
Researches, debates, and paper-trades against real prices with account API or x402 billing.
Risk limits live in code, not in the prompt. Every fill has a receipt.
Fund the wallet. Set a budget. Walk away — and come back to a book.
Set a budget. Connect a transaction wallet for live trades. Walk away — and come back to a book.

[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org/)
Expand All @@ -17,7 +17,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,
> agent. It inherits Franklin's economic substrate (account API access, x402 micropayments,
> the shared [Router Core](https://github.com/BlockRunAI/router-core) engine across
> <!-- br:models.chatVisible -->76<!-- /br:models.chatVisible --> models, removable-by-design harness components)
> and adds a deterministic fee-aware risk engine, a wallet-bound trade journal, a
Expand All @@ -26,11 +26,11 @@ Fund the wallet. Set a budget. Walk away — and come back to a book.
## What works today

This section is the honest one. Everything in it ships in `@blockrun/franklin-trading` 0.3.0
and is covered by the local test suite (386 tests, no network).
and is covered by the local test suite (392 tests, no network).

| Capability | Status | Where |
|---|---|---|
| USDC wallet on Base or Solana, x402 pay-per-call to every model and paid API | ✅ shipped | `src/wallet/`, `@blockrun/llm` |
| Account API key or USDC wallet on Solana / Base for every model and paid API | ✅ shipped | `src/payments/`, `src/wallet/`, `@blockrun/llm` |
| Auto model routing on the shared Router Core engine, <!-- br:models.chatVisible -->76<!-- /br:models.chatVisible --> models, dead-model kill-switch | ✅ shipped | `src/router/` |
| Paper trading against **live** CoinGecko marks (real P&L, simulated fills) | ✅ shipped | `src/trading/live-exchange.ts` |
| Deterministic risk engine: cash **including exchange fee**, per-position cap, total exposure cap, sell integrity | ✅ shipped | `src/trading/risk.ts` |
Expand Down Expand Up @@ -59,6 +59,30 @@ persistent memory, TradingAgents-style hierarchical persona debate, Hummingbot-s
execution rigor — wrapped in Franklin's wallet-native economic substrate, with the one
thing none of them do: **the money is real from day one**, so the guardrails had to be too.

## Account API key

Register at [user.blockrun.ai](https://user.blockrun.ai), create an
[API key](https://user.blockrun.ai/dashboard/keys), and add
[credits](https://user.blockrun.ai/dashboard/credits).

```bash
export BLOCKRUN_API_KEY="brk_live_..."
franklin-trading
```

The account endpoint defaults to `https://api.blockrun.ai`; set
`BLOCKRUN_API_BASE_URL` only when using another trusted BlockRun deployment.
API mode covers the agent, subagents, local proxy, model catalog, Exa,
prediction markets, DeFiLlama, RPC and BlockRun market data. A 401 points to
the key dashboard; a 402 points to account credit top-up. Franklin Trading
does not fall back to a wallet payment after either response.

The API key pays for BlockRun services. It cannot sign an exchange order or
an on-chain transaction. Paper trading needs no transaction wallet; live
trading still requires a separate Solana or Base wallet. `setup`, `balance`
and the Wallet tool continue to manage and report that transaction wallet.
Never put either credential in source control.

## Risk lives outside the model

[Conviction #5](docs/CONVICTIONS.md): the LLM is never the last line of defense. Every
Expand Down Expand Up @@ -93,20 +117,30 @@ validated on load (an `Infinity` balance would otherwise disarm every cap).
```bash
npm install -g @blockrun/franklin-trading

# 1. Run — free out of the box (nvidia/nemotron-nano-9b-v2, no wallet needed)
# Option A: use account credits for models, research and market data
export BLOCKRUN_API_KEY="brk_live_..."
franklin-trading

# 2. Create a USDC wallet on Base (or solana) to unlock every paid model + API
franklin-trading setup base
# Add a separate transaction wallet only when you are ready for live trades.
# Solana is the default; Base remains available explicitly.
franklin-trading setup solana
# franklin-trading setup base

# Option B: unset the key and use x402 wallet billing for BlockRun calls
unset BLOCKRUN_API_KEY
franklin-trading setup solana

# 3. Fund it with $5+ USDC — print the address with:
# Print the active transaction wallet and its USDC balance
franklin-trading balance

# 4. Start with a budget — Franklin Trading stops when the cap is hit
# Franklin Trading stops when the local session estimate reaches the cap
franklin-trading --max-spend 5
```

Zero signup, zero API keys, zero card. The wallet is the identity.
Account API usage is recorded in the
[account dashboard](https://user.blockrun.ai/dashboard). In API mode, local
cost totals and `--max-spend` are estimates; the dashboard ledger is
authoritative. Unset `BLOCKRUN_API_KEY` to return to x402 wallet billing.

## A 60-second tour

Expand Down Expand Up @@ -206,7 +240,7 @@ call in USDC. No free alias ever falls back to a paid model.
Execution: LiveExchange (paper, live marks) today · Hyperliquid · Jupiter · 0x · Polymarket (M4–M5)
Economic substrate (inherited): USDC wallet on Base + Solana, x402 micropayments
Economic substrate: account API · x402 USDC on Solana + Base · separate trade signing
```

See [`PHILOSOPHY.md`](PHILOSOPHY.md) for the design principles,
Expand Down Expand Up @@ -324,8 +358,8 @@ franklin-trading run btc-funding-basis --mode live # real on-chain orders
```bash
npm install
npm run build # tsc + copy bundled skills
npm test # 386 local tests, no network, no wallet
npm run test:e2e # hits real models — needs a funded wallet
npm test # local tests, no network or funded wallet
npm run test:e2e # real models — needs BLOCKRUN_API_KEY or a funded x402 wallet
```

Upstream sync: model catalog, router and pricing changes land in
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,9 @@
"build": "tsc && node scripts/copy-plugin-assets.mjs",
"dev": "tsc --watch",
"start": "node dist/index.js",
"test": "npm run build && node --test --test-reporter=spec test/local.mjs test/skills.local.mjs",
"test": "npm run build && node --test --test-reporter=spec test/local.mjs test/skills.local.mjs test/api-key.local.mjs",
"test:e2e": "npm run build && node --test --test-reporter=spec test/e2e.mjs",
"test:api-key:e2e": "npm run build && node --test --test-reporter=spec test/api-key.e2e.mjs",
"test:strategies": "npm run build && node --test --test-reporter=spec test/strategies.mjs",
"test:free-models": "npm run build && node --test --test-reporter=spec test/free-model-matrix.mjs",
"test:all": "npm run test && npm run test:strategies && npm run test:e2e",
Expand Down
2 changes: 2 additions & 0 deletions src/agent/commands.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { accountMode, ACCOUNT_PORTAL } from '../payments/account.js';
/**
* Slash command registry for Franklin.
* Extracted from loop.ts for maintainability.
Expand Down Expand Up @@ -933,6 +934,7 @@ export async function handleSlashCommand(
} catch { balance = '(unavailable)'; }
}
ctx.onEvent({ kind: 'text_delta', text:
(accountMode() ? `Account API billing: ${ACCOUNT_PORTAL}/dashboard\nTransaction wallet:\n` : '') +
`**Wallet**\n` +
` Chain: ${chain}\n` +
` Address: ${address}\n` +
Expand Down
15 changes: 9 additions & 6 deletions src/agent/context.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { accountMode, ACCOUNT_PORTAL } from '../payments/account.js';
/**
* Context Manager for Franklin
* Assembles system instructions, reads project config, injects environment info.
Expand Down Expand Up @@ -181,17 +182,19 @@ Do NOT check access before acting. Do NOT explain what you tried. Just deliver,
}

function getWalletKnowledgeSection(): string {
if (accountMode()) return `# BlockRun account billing
Model, media, search and data requests use the configured API key. No payment wallet is needed for those calls. Account balance and usage: ${ACCOUNT_PORTAL}/dashboard; top up: ${ACCOUNT_PORTAL}/dashboard/credits. Never inspect or print BLOCKRUN_API_KEY. Actual on-chain trades and transactions still require a separately configured transaction wallet. Local model cost totals are estimates, not the account ledger.`;
return `# Wallet Storage (answer "where is my wallet" directly — no searching)
Franklin Trading stores wallet keys in ~/.blockrun/. When the user asks about wallet location, answer from this map — do not grep or scan.

- Base / EVM wallet (the primary wallet shown in Franklin's startup banner):
Private key file: ~/.blockrun/.session
Format: 66-char hex string starting with 0x (file name intentionally looks like a session token for obscurity)
Address: derivable from the key; also available via getWalletAddress() from @blockrun/llm
- Solana wallet:
Private key file: ~/.blockrun/.solana-session
Format: bare base58 secret key (file name mirrors the Base wallet's obscurity convention; mode 600)
Address: derivable; available via getOrCreateSolanaWallet() from @blockrun/llm
- Base / EVM wallet:
Private key file: ~/.blockrun/.session
Format: 66-char hex string starting with 0x (file name intentionally looks like a session token for obscurity)
Address: derivable from the key; also available via getWalletAddress() from @blockrun/llm
- Chain selection: ~/.blockrun/payment-chain ("base" or "solana"). Legacy file ~/.blockrun/.chain may also exist on installs that haven't migrated; canonical is payment-chain.
- Spending data:
- ~/.blockrun/franklin-stats.json — rolling totals + per-model breakdown (what \`franklin stats\` reads).
Expand All @@ -200,7 +203,7 @@ Franklin Trading stores wallet keys in ~/.blockrun/. When the user asks about wa
- Use \`franklin stats\` / \`franklin content list\` instead of parsing files when the user asks "how much did I spend".
- Programmatic access: import { getWalletAddress, getOrCreateWallet, getOrCreateSolanaWallet } from '@blockrun/llm'

When the user asks about "my wallet" without qualifier, default to Base (it's the primary chain shown at launch). Only mention Solana if the chain file says solana or the user explicitly asks.
When the user asks about "my wallet", use the saved active chain. New users default to Solana; preserve existing Base selections.

## Funding the wallet ("how do I deposit / recharge / fund / top up", in any language)

Expand All @@ -221,8 +224,8 @@ function getBlockRunApiSection(): string {
You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun API", "check all endpoints", or call the gateway directly, use ONLY the paths below. **Never invent, pluralize, or singularize an endpoint** — \`/v1/image/generate\` (singular) is wrong, \`/v1/images/generations\` (plural) is correct. If a path you have in mind isn't in this list, fetch the canonical discovery endpoints before calling it.

**Base URLs**
- Base chain: \`https://blockrun.ai/api\` (alias: \`https://api.blockrun.ai\`)
- Solana chain: \`https://sol.blockrun.ai/api\`
- Base chain: \`https://blockrun.ai/api\` (alias: \`https://api.blockrun.ai\`)

**Discovery (always free, GET) — fetch these BEFORE guessing a path**
- \`GET /openapi.json\` (or \`/.well-known/openapi.json\`) — full OpenAPI 3.1 contract, every route + request schema
Expand Down
2 changes: 2 additions & 0 deletions src/agent/error-classifier.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { ACCOUNT_PORTAL } from '../payments/account.js';
/**
* Classify model/runtime errors so recovery and UX can be more consistent.
*
Expand Down Expand Up @@ -52,6 +53,7 @@ function includesAny(text: string, patterns: string[]): boolean {

export function classifyAgentError(message: string): AgentErrorInfo {
const err = message.toLowerCase();
if (err.includes("account credits exhausted")) return { category: "payment", label: "Payment", isTransient: false, maxRetries: 0, suggestion: `Top up at ${ACCOUNT_PORTAL}/dashboard/credits.` };

// Extract Retry-After hint that streaming-client appended (see llm.ts
// 429 path). Surfaces on the AgentErrorInfo so the loop can honor the
Expand Down
11 changes: 6 additions & 5 deletions src/agent/intent-prefetch.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { gatewayFetch as fetch, accountMode } from '../payments/account.js';
/**
* Proactive prefetch for live-world questions.
*
Expand Down Expand Up @@ -175,7 +176,7 @@ async function exaAnswerTry(query: string, client: ModelClient): Promise<string
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query }),
});
if (res.status === 402) {
if (res.status === 402 && !accountMode()) {
const payHdr = await extractPaymentReq(res);
if (!payHdr) return null;
const { getOrCreateWallet, getOrCreateSolanaWallet, createPaymentPayload, createSolanaPaymentPayload,
Expand Down Expand Up @@ -216,12 +217,12 @@ async function exaAnswerTry(query: string, client: ModelClient): Promise<string
method: 'POST', headers, body: JSON.stringify({ query }),
});
if (!res2.ok) return null;
const body = await res2.json() as { data?: { answer?: string } };
return (body.data?.answer || '').slice(0, 600).trim() || null;
const body = await res2.json() as { answer?: string; data?: { answer?: string } };
return (body.data?.answer || body.answer || '').slice(0, 600).trim() || null;
}
if (!res.ok) return null;
const body = await res.json() as { data?: { answer?: string } };
return (body.data?.answer || '').slice(0, 600).trim() || null;
const body = await res.json() as { answer?: string; data?: { answer?: string } };
return (body.data?.answer || body.answer || '').slice(0, 600).trim() || null;
} catch {
return null;
}
Expand Down
5 changes: 3 additions & 2 deletions src/agent/llm.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { gatewayFetch as fetch, accountMode } from '../payments/account.js';
/**
* LLM Client for Franklin
* Calls BlockRun API directly with x402 payment handling and streaming.
Expand Down Expand Up @@ -644,7 +645,7 @@ export class ModelClient {
);

// Handle x402 payment
if (response.status === 402) {
if (response.status === 402 && !accountMode()) {
if (this.debug) console.error('[franklin] Payment required — signing...');
const paymentHeader = await this.signPayment(response, request.model);
if (!paymentHeader) {
Expand Down Expand Up @@ -723,7 +724,7 @@ export class ModelClient {
createModelTimeoutError('request', request.model, requestTimeoutMs),
requestTimeoutMs,
);
if (response.status === 402) {
if (response.status === 402 && !accountMode()) {
const paymentHeader = await this.signPayment(response, request.model);
if (!paymentHeader) {
yield { kind: 'error', payload: { message: 'Payment signing failed' } };
Expand Down
12 changes: 9 additions & 3 deletions src/commands/balance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { accountMode, ACCOUNT_PORTAL, validateAccountConfig } from '../payments/account.js';
import chalk from 'chalk';
import { setupAgentWallet, setupAgentSolanaWallet } from '@blockrun/llm';
import { loadChain } from '../config.js';

export async function balanceCommand() {
if (accountMode()) {
validateAccountConfig();
console.log(`Account API credits and usage: ${ACCOUNT_PORTAL}/dashboard`);
console.log(chalk.dim('Transaction wallet:'));
}
const chain = loadChain();

try {
Expand All @@ -12,7 +18,7 @@ export async function balanceCommand() {
const balance = await client.getBalance();

console.log(`Chain: ${chalk.magenta('solana')}`);
console.log(`Wallet: ${chalk.cyan(address)}`);
console.log(`Transaction Wallet: ${chalk.cyan(address)}`);
console.log(
`USDC Balance: ${chalk.green(`$${balance.toFixed(2)}`)}`
);
Expand All @@ -28,7 +34,7 @@ export async function balanceCommand() {
const balance = await client.getBalance();

console.log(`Chain: ${chalk.magenta('base')}`);
console.log(`Wallet: ${chalk.cyan(address)}`);
console.log(`Transaction Wallet: ${chalk.cyan(address)}`);
console.log(
`USDC Balance: ${chalk.green(`$${balance.toFixed(2)}`)}`
);
Expand All @@ -42,7 +48,7 @@ export async function balanceCommand() {
} catch (err) {
const msg = err instanceof Error ? err.message : '';
if (msg.includes('ENOENT') || msg.includes('wallet') || msg.includes('key')) {
console.log(chalk.red('No wallet found. Run `franklin setup` first.'));
console.log(chalk.red('No transaction wallet found. Run `franklin-trading setup` before live trading.'));
} else {
console.log(chalk.red(`Error checking balance: ${msg || 'unknown error'}`));
}
Expand Down
Loading
Loading