From 3854b3afb0f61d77a331750e9362e50886af2585 Mon Sep 17 00:00:00 2001 From: Andrew Barnes Date: Mon, 18 May 2026 20:56:49 -0400 Subject: [PATCH] Add Satoshi API fee agent template --- README.md | 1 + typescript/satoshi-api-fee-agent/.env.example | 3 + typescript/satoshi-api-fee-agent/README.md | 81 ++++++++ typescript/satoshi-api-fee-agent/index.ts | 178 ++++++++++++++++++ typescript/satoshi-api-fee-agent/package.json | 18 ++ .../satoshi-api-fee-agent/tsconfig.json | 14 ++ 6 files changed, 295 insertions(+) create mode 100644 typescript/satoshi-api-fee-agent/.env.example create mode 100644 typescript/satoshi-api-fee-agent/README.md create mode 100644 typescript/satoshi-api-fee-agent/index.ts create mode 100644 typescript/satoshi-api-fee-agent/package.json create mode 100644 typescript/satoshi-api-fee-agent/tsconfig.json diff --git a/README.md b/README.md index 35457c74..dcdeeb53 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ Ready-to-use automation templates for Stagehand and Browserbase. Each template h | proxies | [TS](typescript/proxies) | [PY](python/proxies) | - | Demonstrate different proxy configurations with Browserbase sessions | | proxies-weather | [TS](typescript/proxies-weather) | [PY](python/proxies-weather) | - | Geolocation proxies fetching location-specific weather data from multiple cities | | puppeteer | [TS](typescript/puppeteer) | - | - | Raw Puppeteer usage with Browserbase | +| satoshi-api-fee-agent | [TS](typescript/satoshi-api-fee-agent) | - | - | Give browser agents live Bitcoin fee intelligence before payment or wallet flows | | sec-filing-research | [TS](typescript/sec-filing-research) | [PY](python/sec-filing-research) | - | Search SEC EDGAR for a company and extract recent filing metadata | | selenium | [TS](typescript/selenium) | [PY](python/selenium) | - | Raw Selenium usage with Browserbase | | smart-fetch-scraper | [TS](typescript/smart-fetch-scraper) | [PY](python/smart-fetch-scraper) | - | Scrape a webpage using the fastest method available -- Fetch API first, full browser session as fallback | diff --git a/typescript/satoshi-api-fee-agent/.env.example b/typescript/satoshi-api-fee-agent/.env.example new file mode 100644 index 00000000..5ba3cb14 --- /dev/null +++ b/typescript/satoshi-api-fee-agent/.env.example @@ -0,0 +1,3 @@ +BROWSERBASE_API_KEY=your_browserbase_api_key +SATOSHI_API_URL=https://bitcoinsapi.com +SATOSHI_API_KEY= diff --git a/typescript/satoshi-api-fee-agent/README.md b/typescript/satoshi-api-fee-agent/README.md new file mode 100644 index 00000000..4e292936 --- /dev/null +++ b/typescript/satoshi-api-fee-agent/README.md @@ -0,0 +1,81 @@ +# Satoshi API Fee Agent + +## AT A GLANCE + +- Goal: give a Browserbase Stagehand agent live Bitcoin fee intelligence before + it acts on a payment, wallet, exchange, or checkout page. +- Uses Satoshi API's free `GET /api/v1/fees/recommended` endpoint for the + first no-token fee decision. +- Opens a Browserbase cloud browser, visits a live Bitcoin fee page, and + extracts visible page context to compare with the API response. +- Documents the optional x402 paid path, starting with `GET /api/v1/fees/now`, + for accountless premium Bitcoin data. + +## GLOSSARY + +- Satoshi API: a self-hostable Bitcoin REST API and hosted service for + structured Bitcoin data. + Docs -> https://bitcoinsapi.com +- fee recommendation: current sat/vB guidance for next-block and lower-urgency + confirmation targets. + Docs -> https://bitcoinsapi.com/docs +- x402: HTTP `402 Payment Required` flow for accountless pay-per-call API + access. + Docs -> https://bitcoinsapi.com/x402/start + +## QUICKSTART + +1. cd typescript/satoshi-api-fee-agent +2. pnpm install +3. cp .env.example .env +4. Add your Browserbase API key to .env +5. pnpm start + +## EXPECTED OUTPUT + +- Fetches live Bitcoin fee guidance from Satoshi API +- Starts a Browserbase Stagehand session +- Opens `https://mempool.space/` for live browser context +- Extracts visible fee page context +- Prints the Satoshi API recommendation, Browserbase session link, page title, + and extracted page summary +- Closes the browser session cleanly + +## COMMON PITFALLS + +- Missing Browserbase credentials: verify `.env` contains `BROWSERBASE_API_KEY` +- Satoshi API rate limits: add `SATOSHI_API_KEY` for higher limits, or self-host + Satoshi API +- Paid endpoint confusion: use `/api/v1/fees/recommended` for the free + quickstart; `/api/v1/fees/now` is the x402 paid first-call route +- Page extraction drift: keep the Satoshi API decision separate from selectors; + Browserbase handles browser context while Satoshi API handles the Bitcoin fee + decision + +## USE CASES + +- Fee-aware payment agents: decide whether to send now or wait before clicking + through a payment flow +- Wallet and checkout QA: compare UI fee suggestions against live mempool + guidance +- Exchange monitoring: attach fee context to Browserbase session replays +- x402-native workflows: combine Browserbase browser sessions with paid Bitcoin + data calls on demand + +## NEXT STEPS + +- Swap `https://mempool.space/` for your wallet, checkout, or exchange URL +- Persist the Satoshi API response next to the Browserbase session replay URL +- Call `GET /api/v1/tx/{txid}/status` after a Browserbase agent finds or submits + a transaction ID +- Try the x402 paid route at `GET /api/v1/fees/now` when you need accountless + premium analysis + +## HELPFUL RESOURCES + +Satoshi API: https://bitcoinsapi.com +Satoshi API source: https://github.com/Bortlesboat/bitcoin-api +Browserbase Docs: https://docs.browserbase.com +Stagehand Docs: https://docs.stagehand.dev/v3/first-steps/introduction +Browserbase x402 Docs: https://docs.browserbase.com/integrations/x402/introduction +Templates: https://www.browserbase.com/templates diff --git a/typescript/satoshi-api-fee-agent/index.ts b/typescript/satoshi-api-fee-agent/index.ts new file mode 100644 index 00000000..572f0041 --- /dev/null +++ b/typescript/satoshi-api-fee-agent/index.ts @@ -0,0 +1,178 @@ +// Satoshi API Fee Agent - See README.md for full documentation + +import "dotenv/config"; +import { Stagehand } from "@browserbasehq/stagehand"; +import { z } from "zod"; + +const FeePageSchema = z.object({ + visibleFastFee: z + .string() + .describe("Fast or next-block fee visible on the current page, if present."), + visibleEconomyFee: z + .string() + .describe("Lower-priority or economy fee visible on the current page, if present."), + congestionSummary: z + .string() + .describe("Short summary of visible mempool or fee congestion context."), +}); + +type JsonObject = Record; + +interface SatoshiFeePayload { + data: JsonObject; + meta?: JsonObject; +} + +function getObject(value: unknown): JsonObject | undefined { + return value && typeof value === "object" && !Array.isArray(value) + ? (value as JsonObject) + : undefined; +} + +function getString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 ? value : undefined; +} + +function getNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) ? value : undefined; +} + +function getEstimate(data: JsonObject, keys: string[]): number | undefined { + const estimates = getObject(data.estimates); + for (const key of keys) { + const estimate = estimates ? getNumber(estimates[key]) : undefined; + if (estimate !== undefined) { + return estimate; + } + + const topLevel = getNumber(data[key]); + if (topLevel !== undefined) { + return topLevel; + } + } + + return undefined; +} + +function summarizeSatoshiRecommendation(data: JsonObject): string { + const recommendation = + getString(data.recommendation) ?? + getString(data.summary) ?? + getString(data.message) ?? + "Satoshi API returned live Bitcoin fee context for this browser agent."; + + const nextBlockFee = getEstimate(data, [ + "1", + "high", + "fastestFee", + "fastest_fee", + "nextBlockFee", + "next_block_fee_sat_vb", + ]); + const sixBlockFee = getEstimate(data, ["6", "medium", "halfHourFee"]); + const dayFee = getEstimate(data, ["144", "low", "economyFee"]); + + const feeParts = [ + nextBlockFee === undefined ? null : `next block ${nextBlockFee} sat/vB`, + sixBlockFee === undefined ? null : `~6 blocks ${sixBlockFee} sat/vB`, + dayFee === undefined ? null : `~1 day ${dayFee} sat/vB`, + ].filter((part): part is string => part !== null); + + return feeParts.length > 0 ? `${recommendation} (${feeParts.join(", ")})` : recommendation; +} + +async function fetchSatoshiFeeRecommendation(): Promise { + const baseUrl = process.env.SATOSHI_API_URL ?? "https://bitcoinsapi.com"; + const headers: Record = { + Accept: "application/json", + }; + + if (process.env.SATOSHI_API_KEY) { + headers["X-API-Key"] = process.env.SATOSHI_API_KEY; + } + + const response = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/fees/recommended`, { + headers, + }); + + if (!response.ok) { + throw new Error(`Satoshi API returned HTTP ${response.status}: ${await response.text()}`); + } + + const payload = (await response.json()) as unknown; + const envelope = getObject(payload); + const data = envelope ? getObject(envelope.data) : undefined; + + if (!data) { + throw new Error("Satoshi API response did not include a data object."); + } + + return { + data, + meta: envelope ? getObject(envelope.meta) : undefined, + }; +} + +async function main() { + const satoshiFeePayload = await fetchSatoshiFeeRecommendation(); + const satoshiRecommendation = summarizeSatoshiRecommendation(satoshiFeePayload.data); + + const stagehand = new Stagehand({ + env: "BROWSERBASE", + model: "google/gemini-3-flash-preview", + verbose: 1, + browserbaseSessionCreateParams: { + browserSettings: { + viewport: { + width: 1280, + height: 720, + }, + }, + }, + }); + + try { + await stagehand.init(); + + const page = stagehand.context.pages()[0]; + if (!page) { + throw new Error("Browserbase session did not create an initial page."); + } + + console.log( + `Live View Link: https://browserbase.com/sessions/${stagehand.browserbaseSessionId}`, + ); + console.log("Satoshi API fee recommendation:"); + console.log(satoshiRecommendation); + + await page.goto("https://mempool.space/", { + waitUntil: "domcontentloaded", + timeoutMs: 60000, + }); + + const pageContext = await stagehand.extract( + `Extract the visible Bitcoin fee context from this page. + Return the fast or next-block fee, a lower-priority or economy fee, and a short congestion summary. + If a value is not visible, say "not visible".`, + FeePageSchema, + ); + + console.log("\nBrowserbase session:"); + console.log(`Page title: ${await page.title()}`); + console.log("Visible page context:"); + console.log(JSON.stringify(pageContext, null, 2)); + } finally { + await stagehand.close(); + console.log("Session closed successfully"); + } +} + +main().catch((error) => { + console.error("Error running Satoshi API fee agent:", error); + console.error("\nCommon issues:"); + console.error(" - Check .env file has BROWSERBASE_API_KEY"); + console.error(" - Add SATOSHI_API_KEY for higher Satoshi API limits"); + console.error(" - Use /api/v1/fees/recommended for the free quickstart"); + console.error("Docs: https://docs.stagehand.dev/v3/first-steps/introduction"); + process.exit(1); +}); diff --git a/typescript/satoshi-api-fee-agent/package.json b/typescript/satoshi-api-fee-agent/package.json new file mode 100644 index 00000000..245eacc6 --- /dev/null +++ b/typescript/satoshi-api-fee-agent/package.json @@ -0,0 +1,18 @@ +{ + "name": "satoshi-api-fee-agent-template", + "type": "module", + "scripts": { + "build": "tsc", + "start": "tsx index.ts" + }, + "dependencies": { + "@browserbasehq/stagehand": "latest", + "dotenv": "^16.4.7", + "zod": "latest" + }, + "devDependencies": { + "@types/node": "^25.0.0", + "tsx": "^4.19.2", + "typescript": "^5.0.0" + } +} diff --git a/typescript/satoshi-api-fee-agent/tsconfig.json b/typescript/satoshi-api-fee-agent/tsconfig.json new file mode 100644 index 00000000..a14301b3 --- /dev/null +++ b/typescript/satoshi-api-fee-agent/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": ["index.ts"] +}