@blockrun/franklin 3.13.0 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -213,11 +213,11 @@ You run on the BlockRun AI Gateway. When the user asks you to "test the BlockRun
213
213
  - Use the **\`JupiterQuote\` and \`JupiterSwap\` built-in tools** — they call Jupiter's Ultra API directly from this process. The user is the first-party caller of Jupiter; we are not a gateway proxy here. A 20 bps platform fee is collected on-chain as part of the swap (Jupiter Referral Program — official integrator mechanism, not a hidden cost).
214
214
  - Do NOT try to call \`/v1/jupiter/...\` on the BlockRun gateway — there is no such endpoint (Jupiter ToU forbids the gateway-proxy model).
215
215
 
216
- **Base DEX swap (0x V2 Permit2)**
217
- - Use the **\`Base0xQuote\` and \`Base0xSwap\` built-in tools** for swaps on Base (chain id 8453). Mirrors Jupiter's local-call posture: 0x's API is hit directly, the user signs the Permit2 EIP-712 with their Base keypair, the tx settles on Base RPC. 0x's official affiliate program embeds 20 bps in the sell-token automatically (BlockRun affiliate, on-chain).
216
+ **Base DEX swap (0x V2 Permit2 via BlockRun gateway)**
217
+ - Use the **\`Base0xQuote\` and \`Base0xSwap\` built-in tools** for swaps on Base (chain id 8453). Tools route through BlockRun gateway \`/v1/zerox/{price,quote}\` (server-side 0x key, x402-paid). User pays $0.001 USDC per quote/swap call to the gateway; on-chain affiliate (20 bps in the sell-token) flows automatically to BlockRun treasury at swap settlement. **No 0x signup needed** — BlockRun manages the upstream key.
218
218
  - Symbol shortcuts pre-mapped: ETH (native), WETH, USDC, USDT, CBBTC, CBETH, AERO, DAI. Raw \`0x...\` addresses pass through.
219
- - **Each Franklin user supplies their OWN \`ZERO_EX_API_KEY\`** (free, no credit card, 10 req/s — sign up at https://dashboard.0x.org). The affiliate cut routes to BlockRun via the swap query params regardless of whose API key is making the call. If the swap tool errors with the env-var message, repeat the URL to the user — do not try to set the env yourself or invent a key.
220
219
  - For native ETH → token: no Permit2 approval needed (native value path). For ERC-20 → token: first-time-per-token Permit2 approval auto-runs before the swap (one-time gas cost; future swaps of the same sell-token reuse it).
220
+ - The user signs Permit2 typed data locally with their Base keypair; the signed transaction is submitted to a Base RPC (default public mainnet-beta) — BlockRun never custodies keys.
221
221
 
222
222
  **Sandbox (POST, x402-paid)**
223
223
  - \`/v1/modal/{...path}\` — Modal GPU sandbox passthrough (create/exec/etc.).
@@ -9,6 +9,10 @@ export interface AppConfig {
9
9
  'auto-compact'?: string;
10
10
  'session-save'?: string;
11
11
  'debug'?: string;
12
+ /** 0x V2 Swap API key for Base swaps. Free at https://dashboard.0x.org. Each user supplies their own; the on-chain affiliate fee routes to BlockRun regardless. */
13
+ 'zerox-api-key'?: string;
14
+ /** Optional Base RPC URL override (Alchemy, QuickNode public, etc.). Defaults to https://mainnet.base.org. */
15
+ 'base-rpc-url'?: string;
12
16
  }
13
17
  export declare function loadConfig(): AppConfig;
14
18
  export declare function configCommand(action: string, keyOrUndefined?: string, value?: string): void;
@@ -15,6 +15,8 @@ const VALID_KEYS = [
15
15
  'auto-compact',
16
16
  'session-save',
17
17
  'debug',
18
+ 'zerox-api-key',
19
+ 'base-rpc-url',
18
20
  ];
19
21
  export function loadConfig() {
20
22
  try {
@@ -25,21 +25,31 @@ import { createWalletClient, http, publicActions, concat, numberToHex, size, par
25
25
  import { privateKeyToAccount } from 'viem/accounts';
26
26
  import { base } from 'viem/chains';
27
27
  import { getOrCreateWallet } from '@blockrun/llm';
28
+ import { loadConfig } from '../commands/config.js';
29
+ import { loadChain, API_URLS, VERSION } from '../config.js';
28
30
  // ─── BlockRun affiliate identity on Base ─────────────────────────────────
29
31
  // Reuses the existing BlockRun ops wallet that already receives x402
30
32
  // settlements on Base. Every swap routed through these tools deposits 20
31
33
  // bps of the sell-token amount into this address at settlement.
32
34
  const BLOCKRUN_BASE_AFFILIATE = '0xe9030014F5DAe217d0A152f02A043567b16c1aBf';
33
35
  const BLOCKRUN_AFFILIATE_FEE_BPS = 20; // 0.2% — matches Jupiter Ultra path.
34
- // ─── 0x API endpoints ─────────────────────────────────────────────────────
35
- const ZEROX_BASE = 'https://api.0x.org/swap/permit2';
36
- const ZEROX_VERSION = 'v2';
37
- const ZEROX_TIMEOUT_MS = 20_000;
36
+ // ─── BlockRun gateway path for 0x ────────────────────────────────────────
37
+ // As of v3.14.0 we route through the BlockRun gateway (server-side 0x key),
38
+ // not directly to api.0x.org. User pays $0.001 via x402 per gateway call;
39
+ // affiliate 20 bps is force-set server-side and lands on-chain in the same
40
+ // BlockRun treasury that already collects x402 settlements.
41
+ const ZEROX_GATEWAY_PATH = '/v1/zerox';
42
+ const ZEROX_TIMEOUT_MS = 30_000;
38
43
  // ─── Default Base RPC ────────────────────────────────────────────────────
39
- // Public Base mainnet endpoint. Override via BASE_RPC_URL env (Alchemy,
40
- // QuickNode public, etc.). The user-facing call is the swap submission;
41
- // quote fetches are off-chain.
44
+ // Public Base mainnet endpoint. Override via BASE_RPC_URL env or
45
+ // `franklin config set base-rpc-url <url>` (Alchemy, QuickNode public, etc.).
46
+ // The user-facing call is the swap submission; quote fetches are off-chain.
42
47
  const DEFAULT_BASE_RPC = 'https://mainnet.base.org';
48
+ function resolveBaseRpcUrl() {
49
+ return (process.env.BASE_RPC_URL ||
50
+ loadConfig()['base-rpc-url'] ||
51
+ DEFAULT_BASE_RPC);
52
+ }
43
53
  // ─── Session safety: cumulative live-swap counter ─────────────────────────
44
54
  const DEFAULT_LIVE_SWAP_CAP = 10;
45
55
  const liveSwapCap = (() => {
@@ -130,59 +140,54 @@ async function loadEvmWallet() {
130
140
  return { account, address: raw.address };
131
141
  }
132
142
  function makeClient(account) {
133
- const rpcUrl = process.env.BASE_RPC_URL || DEFAULT_BASE_RPC;
134
143
  return createWalletClient({
135
144
  account,
136
145
  chain: base,
137
- transport: http(rpcUrl),
146
+ transport: http(resolveBaseRpcUrl()),
138
147
  }).extend(publicActions);
139
148
  }
140
- function zeroxHeaders() {
141
- const apiKey = process.env.ZERO_EX_API_KEY;
142
- if (!apiKey) {
143
- throw new Error("Base swaps need a 0x API key. Each Franklin user sets their own (free, no credit card): " +
144
- "1) Sign up at https://dashboard.0x.org · " +
145
- "2) Copy the API key from the Demo App or your own app · " +
146
- "3) Run `ZERO_EX_API_KEY=zx_... franklin` (or add it to ~/.zshrc / ~/.bashrc). " +
147
- "BlockRun does NOT need a 0x account — the affiliate fee routes to BlockRun's wallet regardless of whose key is making the call.");
148
- }
149
- return {
150
- 'Content-Type': 'application/json',
151
- '0x-api-key': apiKey,
152
- '0x-version': ZEROX_VERSION,
149
+ // ─── 0x calls via BlockRun gateway (free public passthrough) ─────────────
150
+ async function gatewayGet(path, params, ctx) {
151
+ const chain = loadChain();
152
+ const apiUrl = API_URLS[chain];
153
+ const endpoint = `${apiUrl}${ZEROX_GATEWAY_PATH}/${path}?${params.toString()}`;
154
+ const headers = {
155
+ Accept: 'application/json',
156
+ 'User-Agent': `franklin/${VERSION}`,
153
157
  };
154
- }
155
- // ─── 0x API calls ────────────────────────────────────────────────────────
156
- async function zeroxFetch(path, params) {
157
- const url = `${ZEROX_BASE}/${path}?${params.toString()}`;
158
158
  const controller = new AbortController();
159
159
  const timer = setTimeout(() => controller.abort(), ZEROX_TIMEOUT_MS);
160
+ const onAbort = () => controller.abort();
161
+ ctx.abortSignal.addEventListener('abort', onAbort, { once: true });
160
162
  try {
161
- const res = await fetch(url, { headers: zeroxHeaders(), signal: controller.signal });
162
- if (!res.ok) {
163
- const text = await res.text();
164
- throw new Error(`0x ${path} returned ${res.status}: ${text.slice(0, 300)}`);
163
+ const response = await fetch(endpoint, {
164
+ method: 'GET',
165
+ headers,
166
+ signal: controller.signal,
167
+ });
168
+ if (!response.ok) {
169
+ const text = await response.text().catch(() => '');
170
+ throw new Error(`BlockRun gateway /v1/zerox/${path} returned ${response.status}: ${text.slice(0, 300)}`);
165
171
  }
166
- return (await res.json());
172
+ return (await response.json());
167
173
  }
168
174
  finally {
169
175
  clearTimeout(timer);
176
+ ctx.abortSignal.removeEventListener('abort', onAbort);
170
177
  }
171
178
  }
172
179
  function buildSwapParams(args) {
173
- const p = new URLSearchParams({
180
+ // Affiliate params (swapFeeRecipient/Bps/Token) are NOT set here —
181
+ // the BlockRun gateway forces them server-side, ensuring every
182
+ // gateway-routed swap pays affiliate to BlockRun treasury regardless
183
+ // of what the agent passes. See blockrun/src/lib/zerox.ts:proxyToZerox.
184
+ return new URLSearchParams({
174
185
  chainId: String(base.id),
175
186
  sellToken: args.sellTokenAddr,
176
187
  buyToken: args.buyTokenAddr,
177
188
  sellAmount: args.sellAmountAtomic,
178
189
  taker: args.taker,
179
190
  });
180
- if (args.feeRecipient && args.feeBps && args.feeBps > 0 && args.feeToken) {
181
- p.set('swapFeeRecipient', args.feeRecipient);
182
- p.set('swapFeeBps', String(args.feeBps));
183
- p.set('swapFeeToken', args.feeToken);
184
- }
185
- return p;
186
191
  }
187
192
  // ─── Formatting ──────────────────────────────────────────────────────────
188
193
  function formatQuoteText(q) {
@@ -210,7 +215,7 @@ function formatQuoteText(q) {
210
215
  return lines.join('\n');
211
216
  }
212
217
  // ─── Quote (read-only) ───────────────────────────────────────────────────
213
- async function executeBase0xQuote(input) {
218
+ async function executeBase0xQuote(input, ctx) {
214
219
  let walletAddress;
215
220
  try {
216
221
  const wallet = await loadEvmWallet();
@@ -231,14 +236,9 @@ async function executeBase0xQuote(input) {
231
236
  buyTokenAddr,
232
237
  sellAmountAtomic: sellAmount,
233
238
  taker: walletAddress,
234
- feeRecipient: BLOCKRUN_BASE_AFFILIATE,
235
- feeBps: BLOCKRUN_AFFILIATE_FEE_BPS,
236
- // Take the fee in the sell token so it's deterministic and doesn't
237
- // depend on the output token having an existing recipient ATA / balance.
238
- feeToken: sellTokenAddr,
239
239
  });
240
240
  try {
241
- const price = await zeroxFetch('price', params);
241
+ const price = await gatewayGet('price', params, ctx);
242
242
  if (!price.liquidityAvailable && price.liquidityAvailable !== undefined) {
243
243
  return {
244
244
  output: `0x reports no liquidity for ${symbolFor(sellTokenAddr)} → ${symbolFor(buyTokenAddr)} on Base.`,
@@ -248,7 +248,7 @@ async function executeBase0xQuote(input) {
248
248
  return { output: formatQuoteText(price) };
249
249
  }
250
250
  catch (err) {
251
- return { output: `0x /price failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
251
+ return { output: `BlockRun gateway 0x /price failed: ${err instanceof Error ? err.message : String(err)}`, isError: true };
252
252
  }
253
253
  }
254
254
  // ─── Swap (full execute) ─────────────────────────────────────────────────
@@ -281,18 +281,16 @@ async function executeBase0xSwap(input, ctx) {
281
281
  buyTokenAddr,
282
282
  sellAmountAtomic: sellAmount,
283
283
  taker: wallet.address,
284
- feeRecipient: BLOCKRUN_BASE_AFFILIATE,
285
- feeBps: BLOCKRUN_AFFILIATE_FEE_BPS,
286
- feeToken: sellTokenAddr,
287
284
  });
288
- // Step 1 — fetch the firm quote (returns tx + permit2.eip712 to sign).
285
+ // Step 1 — fetch the firm quote via BlockRun gateway (x402-paid).
286
+ // Gateway forces affiliate params server-side; user pays $0.001 USDC.
289
287
  let quote;
290
288
  try {
291
- quote = await zeroxFetch('quote', params);
289
+ quote = await gatewayGet('quote', params, ctx);
292
290
  }
293
291
  catch (err) {
294
292
  return {
295
- output: `0x /quote failed: ${err instanceof Error ? err.message : String(err)}`,
293
+ output: `BlockRun gateway 0x /quote failed: ${err instanceof Error ? err.message : String(err)}`,
296
294
  isError: true,
297
295
  };
298
296
  }
@@ -444,15 +442,15 @@ export const base0xQuoteCapability = {
444
442
  properties: COMMON_INPUT_PROPERTIES,
445
443
  },
446
444
  },
447
- execute: async (input) => {
448
- return executeBase0xQuote(input);
445
+ execute: async (input, ctx) => {
446
+ return executeBase0xQuote(input, ctx);
449
447
  },
450
448
  concurrent: true,
451
449
  };
452
450
  export const base0xSwapCapability = {
453
451
  spec: {
454
452
  name: 'Base0xSwap',
455
- description: "Execute a Base DEX swap via 0x V2 (Permit2). Quotes the order, asks the user to confirm, signs locally with the Franklin Base wallet, and submits via Base RPC. A 20 bps affiliate fee in the sell-token is collected on-chain by 0x as part of the swap (BlockRun affiliate program — official 0x integrator mechanism). Returns the BaseScan transaction link. Requires ZERO_EX_API_KEY env var (free at dashboard.0x.org).",
453
+ description: "Execute a Base DEX swap via 0x V2 (Permit2). Quotes through BlockRun gateway (x402-paid, server-side 0x key — no user setup needed), asks the user to confirm, signs locally with the Franklin Base wallet, and submits via Base RPC. A 20 bps affiliate fee in the sell-token is collected on-chain by 0x as part of the swap (BlockRun affiliate program — official 0x integrator mechanism). Returns the BaseScan transaction link.",
456
454
  input_schema: {
457
455
  type: 'object',
458
456
  required: ['sell_token', 'buy_token', 'sell_amount'],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blockrun/franklin",
3
- "version": "3.13.0",
3
+ "version": "3.14.1",
4
4
  "description": "Franklin — The AI agent with a wallet. Spends USDC autonomously to get real work done. Pay per action, no subscriptions.",
5
5
  "type": "module",
6
6
  "exports": {