From e19e214356405efd40c4587448c40c2275455661 Mon Sep 17 00:00:00 2001 From: tmm Date: Thu, 12 Mar 2026 14:26:00 -0400 Subject: [PATCH 01/40] chore: tweaks (#1) --- vocs.config.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/vocs.config.ts b/vocs.config.ts index 9ebdd607..3a578035 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -40,9 +40,11 @@ export default defineConfig({ path === '/' ? `${baseUrl}/og-docs.png` : `${baseUrl}/api/og?title=%title&description=%description`, + // TODO: Change back to file paths (`/lockup-light.svg`, `/lockup-dark.svg`) once password protection is removed logoUrl: { - light: '/lockup-light.svg', - dark: '/lockup-dark.svg', + light: + 'data:image/svg+xml,%3Csvg%20width%3D%22184%22%20height%3D%2241%22%20viewBox%3D%220%200%20184%2041%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%0A%3Cpath%20d%3D%22M13.6424%2040.3635H2.80251L12.8492%209.60026H0L2.80251%200.58344H38.6006L35.7981%209.60026H23.6362L13.6424%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M53.9809%2040.3635H28.2824L41.1846%200.58344H66.7773L64.3449%208.16818H49.4863L46.7896%2016.7076H61.1723L58.7399%2024.1863H44.3043L41.6076%2032.7788H56.3604L53.9809%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M65.6123%2040.3635H56.9933L69.9483%200.58344H84.331L83.8551%2022.0647L97.8676%200.58344H113.625L100.723%2040.3635H89.936L98.5021%2013.6313H98.3435L80.7353%2040.3635H74.3371L74.6015%2013.3131H74.4957L65.6123%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M125.758%207.95602L121.581%2020.7917H122.744C125.388%2020.7917%20127.592%2020.1729%20129.354%2018.9353C131.117%2017.6624%20132.262%2015.859%20132.791%2013.5252C133.249%2011.5097%20133.003%2010.0776%20132.051%209.22898C131.099%208.38034%20129.513%207.95602%20127.292%207.95602H125.758ZM115.289%2040.3635H104.449L117.351%200.58344H130.517C133.549%200.58344%20136.158%201.07848%20138.343%202.06856C140.564%203.02328%20142.186%204.40233%20143.208%206.20569C144.266%207.97369%20144.618%2010.0423%20144.266%2012.4114C143.807%2015.5231%20142.609%2018.2635%20140.67%2020.6326C138.731%2023.0017%20136.211%2024.8405%20133.108%2026.1488C130.042%2027.4217%20126.604%2028.0582%20122.797%2028.0582H119.255L115.289%2040.3635Z%22%20fill%3D%22black%22/%3E%0A%3Cpath%20d%3D%22M170.103%2037.8176C166.507%2039.9392%20162.682%2041%20158.628%2041H158.523C154.927%2041%20151.895%2040.2044%20149.428%2038.6132C146.995%2036.9866%20145.25%2034.7943%20144.193%2032.0362C143.171%2029.2781%20142.924%2026.2549%20143.453%2022.9664C144.122%2018.8292%20145.656%2015.0103%20148.053%2011.5097C150.45%208.00906%20153.446%205.21561%20157.042%203.12937C160.638%201.04312%20164.48%200%20168.569%200H168.675C172.412%200%20175.496%200.795602%20177.929%202.38681C180.396%203.97801%20182.106%206.15265%20183.058%208.91074C184.045%2011.6335%20184.256%2014.6921%20183.692%2018.0867C183.023%2022.0824%20181.489%2025.8482%20179.092%2029.3842C176.695%2032.8849%20173.699%2035.696%20170.103%2037.8176ZM155.138%2030.9754C156.09%2032.7788%20157.747%2033.6805%20160.109%2033.6805H160.215C162.154%2033.6805%20163.951%2032.9556%20165.608%2031.5058C167.3%2030.0207%20168.728%2028.0405%20169.891%2025.5653C171.09%2023.0901%20171.971%2020.332%20172.535%2017.2911C173.064%2014.3208%20172.852%2011.934%20171.901%2010.1307C170.949%208.29194%20169.31%207.37257%20166.983%207.37257H166.877C165.079%207.37257%20163.335%208.11514%20161.642%209.60026C159.986%2011.0854%20158.54%2013.0832%20157.306%2015.5938C156.073%2018.1044%20155.174%2020.8271%20154.61%2023.762C154.046%2026.7322%20154.222%2029.1367%20155.138%2030.9754Z%22%20fill%3D%22black%22/%3E%0A%3C/svg%3E', + dark: 'data:image/svg+xml,%3Csvg%20width%3D%22184%22%20height%3D%2241%22%20viewBox%3D%220%200%20184%2041%22%20fill%3D%22none%22%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%3E%0A%3Cpath%20d%3D%22M13.6424%2040.3635H2.80251L12.8492%209.60026H0L2.80251%200.58344H38.6006L35.7981%209.60026H23.6362L13.6424%2040.3635Z%22%20fill%3D%22white%22/%3E%0A%3Cpath%20d%3D%22M53.9809%2040.3635H28.2824L41.1846%200.58344H66.7773L64.3449%208.16818H49.4863L46.7896%2016.7076H61.1723L58.7399%2024.1863H44.3043L41.6076%2032.7788H56.3604L53.9809%2040.3635Z%22%20fill%3D%22white%22/%3E%0A%3Cpath%20d%3D%22M65.6123%2040.3635H56.9933L69.9483%200.58344H84.331L83.8551%2022.0647L97.8676%200.58344H113.625L100.723%2040.3635H89.936L98.5021%2013.6313H98.3435L80.7353%2040.3635H74.3371L74.6015%2013.3131H74.4957L65.6123%2040.3635Z%22%20fill%3D%22white%22/%3E%0A%3Cpath%20d%3D%22M125.758%207.95602L121.581%2020.7917H122.744C125.388%2020.7917%20127.592%2020.1729%20129.354%2018.9353C131.117%2017.6624%20132.262%2015.859%20132.791%2013.5252C133.249%2011.5097%20133.003%2010.0776%20132.051%209.22898C131.099%208.38034%20129.513%207.95602%20127.292%207.95602H125.758ZM115.289%2040.3635H104.449L117.351%200.58344H130.517C133.549%200.58344%20136.158%201.07848%20138.343%202.06856C140.564%203.02328%20142.186%204.40233%20143.208%206.20569C144.266%207.97369%20144.618%2010.0423%20144.266%2012.4114C143.807%2015.5231%20142.609%2018.2635%20140.67%2020.6326C138.731%2023.0017%20136.211%2024.8405%20133.108%2026.1488C130.042%2027.4217%20126.604%2028.0582%20122.797%2028.0582H119.255L115.289%2040.3635Z%22%20fill%3D%22white%22/%3E%0A%3Cpath%20d%3D%22M170.103%2037.8176C166.507%2039.9392%20162.682%2041%20158.628%2041H158.523C154.927%2041%20151.895%2040.2044%20149.428%2038.6132C146.995%2036.9866%20145.25%2034.7943%20144.193%2032.0362C143.171%2029.2781%20142.924%2026.2549%20143.453%2022.9664C144.122%2018.8292%20145.656%2015.0103%20148.053%2011.5097C150.45%208.00906%20153.446%205.21561%20157.042%203.12937C160.638%201.04312%20164.48%200%20168.569%200H168.675C172.412%200%20175.496%200.795602%20177.929%202.38681C180.396%203.97801%20182.106%206.15265%20183.058%208.91074C184.045%2011.6335%20184.256%2014.6921%20183.692%2018.0867C183.023%2022.0824%20181.489%2025.8482%20179.092%2029.3842C176.695%2032.8849%20173.699%2035.696%20170.103%2037.8176ZM155.138%2030.9754C156.09%2032.7788%20157.747%2033.6805%20160.109%2033.6805H160.215C162.154%2033.6805%20163.951%2032.9556%20165.608%2031.5058C167.3%2030.0207%20168.728%2028.0405%20169.891%2025.5653C171.09%2023.0901%20171.971%2020.332%20172.535%2017.2911C173.064%2014.3208%20172.852%2011.934%20171.901%2010.1307C170.949%208.29194%20169.31%207.37257%20166.983%207.37257H166.877C165.079%207.37257%20163.335%208.11514%20161.642%209.60026C159.986%2011.0854%20158.54%2013.0832%20157.306%2015.5938C156.073%2018.1044%20155.174%2020.8271%20154.61%2023.762C154.046%2026.7322%20154.222%2029.1367%20155.138%2030.9754Z%22%20fill%3D%22white%22/%3E%0A%3C/svg%3E', }, iconUrl: { light: '/icon-light.png', From 11e8e5c35f3a163df94abd0a30eb1ab9f8417086 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Thu, 12 Mar 2026 12:10:55 -0700 Subject: [PATCH 02/40] docs: add Machine Payments (MPP) section (#2) * docs: add Machine Payments (MPP) section Add 6 new pages under guide/machine-payments: - Overview: protocol intro, payment flow diagram, intents, use cases - Client quickstart: fetch polyfill, Wagmi, per-request accounts, manual handling - Server quickstart: framework middleware, manual mode, push/pull, fee sponsorship - Accept one-time payments: charge intent guide with multi-framework examples - Accept pay-as-you-go payments: session intent with payment channel diagram - Accept streamed payments: SSE per-token billing with session vouchers Also adds MermaidDiagram component (from mpp repo) for animated sequence diagrams and sidebar entry under 'Start Building on Tempo'. * fix: use mpp.dev instead of mpp.sh for all links * fix: resolve tsgo build errors and biome warnings in MermaidDiagram * fix(ci): increase Node.js heap to 4GB for vite build OOM --- .github/workflows/verify.yml | 1 + src/components/MermaidDiagram.tsx | 1091 +++++++++++++++++ src/pages/guide/machine-payments/client.mdx | 202 +++ src/pages/guide/machine-payments/index.mdx | 104 ++ .../machine-payments/one-time-payments.mdx | 178 +++ .../guide/machine-payments/pay-as-you-go.mdx | 236 ++++ src/pages/guide/machine-payments/server.mdx | 180 +++ .../machine-payments/streamed-payments.mdx | 281 +++++ vocs.config.ts | 30 + 9 files changed, 2303 insertions(+) create mode 100644 src/components/MermaidDiagram.tsx create mode 100644 src/pages/guide/machine-payments/client.mdx create mode 100644 src/pages/guide/machine-payments/index.mdx create mode 100644 src/pages/guide/machine-payments/one-time-payments.mdx create mode 100644 src/pages/guide/machine-payments/pay-as-you-go.mdx create mode 100644 src/pages/guide/machine-payments/server.mdx create mode 100644 src/pages/guide/machine-payments/streamed-payments.mdx diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index bb2a0522..8b2ddaee 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -36,6 +36,7 @@ jobs: run: pnpm run build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NODE_OPTIONS: --max-old-space-size=4096 e2e: name: E2E Tests (${{ matrix.shard }}/${{ strategy.job-total }}) diff --git a/src/components/MermaidDiagram.tsx b/src/components/MermaidDiagram.tsx new file mode 100644 index 00000000..b5783219 --- /dev/null +++ b/src/components/MermaidDiagram.tsx @@ -0,0 +1,1091 @@ +"use client"; + +import { useCallback, useEffect, useRef, useState } from "react"; + +// --------------------------------------------------------------------------- +// Layout constants +// --------------------------------------------------------------------------- + +export const LAYOUT = { + padding: 20, + actorGap: 260, + actorGap2: 360, + actorBoxH: 36, + actorPadX: 24, + headerGap: 72, + + rowHeight: 72, + blockPadX: 12, + blockPadTop: 28, + blockPadBottom: 10, + labelLineGap: 22, + arrowSize: 8, + badgeR: 10, + noteBoxPadX: 16, + noteBoxPadY: 8, + noteExtraMargin: 28, + fontFamily: + '"Geist Pixel Square", "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace', + actorFontSize: 14, + actorFontWeight: 600, + labelFontSize: 14, + labelFontWeight: 400, + noteFontSize: 13, + noteFontWeight: 500, + badgeFontSize: 10, + blockLabelFontSize: 11, + blockLabelFontWeight: 600, + messageStroke: 1.2, + lifelineStroke: 0.75, +}; + +export interface ThemeColors { + text: string; + textMuted: string; + line: string; + lifeline: string; + arrow: string; + successArrow: string; + errorCode: string; + actorFill: string; + actorStroke: string; + blockStroke: string; + blockHeaderBg: string; + badgeBg: string; + badgeText: string; +} + +export const THEMES: Record<"light" | "dark", ThemeColors> = { + light: { + text: "#27272a", + textMuted: "#3f3f46", + line: "#a1a1aa", + lifeline: "#d4d4d8", + arrow: "#0166ff", + successArrow: "#16a34a", + errorCode: "#dc2626", + actorFill: "#ffffff", + actorStroke: "#e4e4e7", + blockStroke: "#e4e4e7", + blockHeaderBg: "#f4f4f5", + badgeBg: "#e4e4e7", + badgeText: "#52525b", + }, + dark: { + text: "#e4e4e7", + textMuted: "#e4e4e7", + line: "#71717a", + lifeline: "#3f3f46", + arrow: "#60a5fa", + successArrow: "#4ade80", + errorCode: "#f87171", + actorFill: "#27272a", + actorStroke: "#3f3f46", + blockStroke: "#3f3f46", + blockHeaderBg: "#27272a", + badgeBg: "#3f3f46", + badgeText: "#a1a1aa", + }, +}; + +// --------------------------------------------------------------------------- +// Parser +// --------------------------------------------------------------------------- + +export interface Participant { + id: string; + label: string; +} + +export type Step = + | { + type: "message"; + from: string; + to: string; + label: string; + num: string | null; + dashed: boolean; + } + | { type: "note"; over: string; text: string; num: string | null } + | { type: "loop-start"; label: string } + | { type: "loop-end" }; + +export interface ParsedDiagram { + participants: Participant[]; + steps: Step[]; +} + +export function extractNum(text: string): { num: string | null; rest: string } { + const m = text.match(/^\((\d+)\)\s*(.+)$/); + return m ? { num: m[1], rest: m[2] } : { num: null, rest: text }; +} + +export function parse(source: string): ParsedDiagram { + const lines = source + .split("\n") + .map((l) => l.trim()) + .filter((l) => l && !l.startsWith("%%")); + const participants: Participant[] = []; + const steps: Step[] = []; + const seen = new Set(); + const ensure = (id: string) => { + if (!seen.has(id)) { + seen.add(id); + participants.push({ id, label: id }); + } + }; + + for (const line of lines) { + if (line === "sequenceDiagram") continue; + const mPartAs = line.match(/^participant\s+(\S+)\s+as\s+(.+)$/i); + if (mPartAs) { + seen.add(mPartAs[1]); + participants.push({ id: mPartAs[1], label: mPartAs[2].trim() }); + continue; + } + const mPart = line.match(/^participant\s+(\S+)$/i); + if (mPart) { + ensure(mPart[1]); + continue; + } + const mNote = line.match(/^Note\s+over\s+(\S+?)\s*:\s*(.+)$/i); + if (mNote) { + ensure(mNote[1]); + const e = extractNum(mNote[2].trim()); + steps.push({ type: "note", over: mNote[1], text: e.rest, num: e.num }); + continue; + } + const mLoop = line.match(/^loop\s+(.+)$/i); + if (mLoop) { + steps.push({ type: "loop-start", label: mLoop[1].trim() }); + continue; + } + if (/^end$/i.test(line)) { + steps.push({ type: "loop-end" }); + continue; + } + const mMsg = line.match(/^(\S+?)(--?>>)(\S+?)\s*:\s*(.+)$/); + if (mMsg) { + ensure(mMsg[1]); + ensure(mMsg[3]); + const e = extractNum(mMsg[4].trim()); + steps.push({ + type: "message", + from: mMsg[1], + to: mMsg[3], + label: e.rest, + num: e.num, + dashed: mMsg[2] === "-->>", + }); + } + } + return { participants, steps }; +} + +// --------------------------------------------------------------------------- +// Layout +// --------------------------------------------------------------------------- + +export interface LMsg { + x1: number; + x2: number; + y: number; + label: string; + num: string | null; + labelX: number; + labelY: number; + dashed: boolean; + si: number; + isLast: boolean; +} +export interface LNote { + text: string; + num: string | null; + x: number; + y: number; + boxX: number; + boxY: number; + boxW: number; + boxH: number; + lines: string[]; + si: number; +} +export interface LActor { + cx: number; + boxX: number; + boxY: number; + boxW: number; + boxH: number; + label: string; +} +export interface LBlock { + label: string; + x: number; + y: number; + w: number; + h: number; +} +export interface LLifeline { + x: number; + y1: number; + y2: number; +} +export interface Layout { + w: number; + h: number; + actors: LActor[]; + lifelines: LLifeline[]; + messages: LMsg[]; + notes: LNote[]; + blocks: LBlock[]; + msgCount: number; +} + +export function doLayout(p: ParsedDiagram): Layout { + const L = LAYOUT; + const n = p.participants.length; + const gap = n === 2 ? L.actorGap2 : L.actorGap; + + const aw = p.participants.map( + (a) => estW(a.label, L.actorFontSize) + L.actorPadX * 2, + ); + const cx: number[] = []; + let xc = L.padding + aw[0] / 2; + for (let i = 0; i < n; i++) { + if (i > 0) xc += Math.max(gap, (aw[i - 1] + aw[i]) / 2 + 60); + cx.push(xc); + } + + const idx = new Map(); + for (let i = 0; i < n; i++) idx.set(p.participants[i].id, i); + + const bY = L.padding; + const actors: LActor[] = p.participants.map((a, i) => ({ + cx: cx[i], + boxX: cx[i] - aw[i] / 2, + boxY: bY, + boxW: aw[i], + boxH: L.actorBoxH, + label: a.label, + })); + + let y = bY + L.actorBoxH + L.headerGap; + const messages: LMsg[] = []; + const notes: LNote[] = []; + const blocks: LBlock[] = []; + const bStack: { label: string; x: number; y: number }[] = []; + const rightEdge = cx[n - 1] + aw[n - 1] / 2; + const leftEdge = cx[0] - aw[0] / 2; + const midX = (cx[0] + cx[n - 1]) / 2; + + // Count total messages to identify the last one + let totalMsgs = 0; + for (const s of p.steps) { + if (s.type === "message") totalMsgs++; + } + let msgIdx = 0; + + for (let si = 0; si < p.steps.length; si++) { + const s = p.steps[si]; + if (s.type === "message") { + const fi = idx.get(s.from) ?? 0; + const ti = idx.get(s.to) ?? 0; + msgIdx++; + messages.push({ + x1: cx[fi], + x2: cx[ti], + y, + label: s.label, + num: s.num, + labelX: (cx[fi] + cx[ti]) / 2, + labelY: y - L.labelLineGap, + dashed: s.dashed, + si, + isLast: msgIdx === totalMsgs, + }); + y += L.rowHeight; + } else if (s.type === "note") { + const maxNW = (rightEdge - leftEdge) * 0.8; + const wrapped = wrapText(s.text, maxNW, L.noteFontSize); + const lineH = L.noteFontSize + 4; + const boxW = + Math.max(...wrapped.map((t) => estW(t, L.noteFontSize))) + + L.noteBoxPadX * 2; + const boxH = wrapped.length * lineH + L.noteBoxPadY * 2; + const boxX = midX - boxW / 2; + const boxY = y - boxH / 2; + notes.push({ + text: s.text, + num: s.num, + x: midX, + y, + boxX, + boxY, + boxW: boxW + (s.num ? L.badgeR * 2 + 6 : 0), + boxH, + lines: wrapped, + si, + }); + y += L.rowHeight + L.noteExtraMargin; + } else if (s.type === "loop-start") { + bStack.push({ + label: s.label, + x: leftEdge - L.blockPadX, + y: y - L.blockPadTop / 2, + }); + y += L.blockPadTop; + } else if (s.type === "loop-end") { + const blk = bStack.pop(); + if (blk) { + const bw = rightEdge + L.blockPadX - blk.x; + blocks.push({ + label: blk.label, + x: blk.x, + y: blk.y, + w: bw, + h: y - blk.y + L.blockPadBottom, + }); + y += L.blockPadBottom; + } + } + } + + const llBot = y - L.rowHeight / 2; + const lifelines: LLifeline[] = cx.map((lx) => ({ + x: lx, + y1: bY + L.actorBoxH, + y2: llBot, + })); + const totalW = rightEdge + L.padding; + const totalH = llBot + L.padding; + + return { + w: totalW, + h: totalH, + actors, + lifelines, + messages, + notes, + blocks, + msgCount: totalMsgs, + }; +} + +export function estW(text: string, fontSize: number): number { + return text.length * fontSize * 0.6; +} + +export function wrapText( + text: string, + maxW: number, + fontSize: number, +): string[] { + const words = text.split(/\s+/); + const lines: string[] = []; + let cur = ""; + for (const word of words) { + const test = cur ? `${cur} ${word}` : word; + if (estW(test, fontSize) > maxW && cur) { + lines.push(cur); + cur = word; + } else { + cur = test; + } + } + if (cur) lines.push(cur); + return lines.length > 0 ? lines : [text]; +} + +// --------------------------------------------------------------------------- +// SVG renderer +// --------------------------------------------------------------------------- + +export function render(lo: Layout, th: ThemeColors): string { + const L = LAYOUT; + const o: string[] = []; + const sz = L.arrowSize; + const br = L.badgeR; + + o.push( + '', + ); + o.push(``); + + // Gradient for last (success) message line — use userSpaceOnUse to avoid + // zero-height bounding box issues on horizontal elements. + const lastMsg = lo.messages.find((m) => m.isLast); + if (lastMsg) { + o.push( + '', + ); + } + + // Lifelines + for (const ll of lo.lifelines) { + o.push( + '', + ); + } + + // Blocks + for (const b of lo.blocks) { + const tw = estW(b.label, L.blockLabelFontSize) + 20; + o.push( + '', + ); + o.push( + '', + ); + o.push( + '' + + esc(b.label) + + "", + ); + } + + // Actors + for (const a of lo.actors) { + o.push( + '', + ); + o.push( + '' + + esc(a.label) + + "", + ); + } + + // Messages + for (const m of lo.messages) { + const da = m.dashed ? ' stroke-dasharray="6 4"' : ""; + const goingRight = m.x2 > m.x1; + const lineEndX = goingRight ? m.x2 - sz : m.x2 + sz; + const lineStroke = m.isLast ? "url(#grad-success)" : th.line; + // Solid arrows (->>): filled triangle; dashed arrows (-->>): outline triangle + const arrowFill = m.isLast + ? m.dashed + ? th.actorFill + : th.successArrow + : m.dashed + ? th.actorFill + : th.line; + const arrowStroke = m.isLast ? th.successArrow : th.line; + + // Line + o.push( + '", + ); + + // Arrow + const tipX = m.x2; + const baseX = goingRight ? tipX - sz : tipX + sz; + o.push( + '', + ); + + // Compute label text width to place badge to its left + const labelW = estW(m.label, L.labelFontSize); + const totalLabelW = labelW + (m.num ? br * 2 + 6 : 0); + const groupLeft = m.labelX - totalLabelW / 2; + + // Badge (subtle bg color, not blue) + if (m.num) { + const bcx = groupLeft + br; + const bcy = m.labelY; + o.push( + '', + ); + o.push( + '' + + m.num + + "", + ); + } + + // Label text (to the right of badge) + const textX = m.num ? groupLeft + br * 2 + 6 + labelW / 2 : m.labelX; + o.push( + '' + + highlightLabel(m.label, th) + + "", + ); + } + + // Notes — rounded box with wrapped text, no italic + for (const nt of lo.notes) { + const lineH = L.noteFontSize + 4; + // Recenter box now that boxW includes badge space + const centeredBoxX = nt.x - nt.boxW / 2; + const textStartY = nt.boxY + L.noteBoxPadY + L.noteFontSize; + + o.push( + '', + ); + + if (nt.num) { + const bx = centeredBoxX + L.noteBoxPadX + br; + const by = nt.boxY + nt.boxH / 2; + o.push( + '', + ); + o.push( + '' + + nt.num + + "", + ); + } + + const textX = nt.num + ? centeredBoxX + L.noteBoxPadX + br * 2 + 6 + : centeredBoxX + L.noteBoxPadX; + for (let li = 0; li < nt.lines.length; li++) { + o.push( + '' + + esc(nt.lines[li]) + + "", + ); + } + } + + o.push(""); + return o.join("\n"); +} + +// Syntax highlight HTTP codes and methods in labels +export function highlightLabel(label: string, th: ThemeColors): string { + // Tokenize: split label into segments with optional color overrides + const re = + /(GET|POST|PUT|DELETE|PATCH|\b[45]\d{2}\b|\b2\d{2}\s*OK\b|\b2\d{2}\b)/g; + let lastIdx = 0; + let result = ""; + let match: RegExpExecArray | null = re.exec(label); + while (match !== null) { + // Text before match + if (match.index > lastIdx) { + result += esc(label.slice(lastIdx, match.index)); + } + const tok = match[0]; + let color = th.textMuted; + if (/^(GET|POST|PUT|DELETE|PATCH)$/.test(tok)) color = th.arrow; + else if (/^[45]\d{2}$/.test(tok)) color = th.errorCode; + else if (/^2\d{2}/.test(tok)) color = th.successArrow; + result += `${esc(tok)}`; + lastIdx = match.index + tok.length; + match = re.exec(label); + } + // Remaining text + if (lastIdx < label.length) { + result += esc(label.slice(lastIdx)); + } + return result; +} + +export function esc(s: string): string { + return s + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """); +} + +// --------------------------------------------------------------------------- +// Animation +// --------------------------------------------------------------------------- + +export interface AnimationHandle { + skipToEnd: () => void; +} + +export function showAllItems(svg: SVGSVGElement) { + svg.style.opacity = "1"; + for (const el of svg.querySelectorAll( + "[data-step],[data-step-arrow],[data-step-label],[data-step-note]", + )) { + el.style.transition = "none"; + el.style.opacity = "1"; + el.style.strokeDashoffset = "0"; + } +} + +export function animate( + svg: SVGSVGElement, + onComplete: () => void, + onStart: () => void, +): AnimationHandle { + type Item = { + si: number; + draw?: SVGElement; + arrow?: SVGElement; + fade: SVGElement[]; + isNote: boolean; + }; + const map = new Map(); + const get = (i: number, isNote = false) => { + if (!map.has(i)) map.set(i, { si: i, fade: [], isNote }); + return map.get(i)!; + }; + + svg.querySelectorAll("[data-step]").forEach((el) => { + get(+(el.dataset.step ?? 0)).draw = el; + }); + svg.querySelectorAll("[data-step-arrow]").forEach((el) => { + get(+(el.dataset.stepArrow ?? 0)).arrow = el; + }); + svg.querySelectorAll("[data-step-label]").forEach((el) => { + get(+(el.dataset.stepLabel ?? 0)).fade.push(el); + }); + svg.querySelectorAll("[data-step-note]").forEach((el) => { + const item = get(+(el.dataset.stepNote ?? 0), true); + item.isNote = true; + item.fade.push(el); + }); + + const timeline = Array.from(map.values()).sort((a, b) => a.si - b.si); + let skipped = false; + const timers: ReturnType[] = []; + + const handle: AnimationHandle = { + skipToEnd() { + if (skipped) return; + skipped = true; + for (const t of timers) clearTimeout(t); + showAllItems(svg); + onComplete(); + }, + }; + + if (!timeline.length) { + svg.style.opacity = "1"; + onComplete(); + return handle; + } + + svg.style.opacity = "1"; + for (const item of timeline) { + if (item.draw) { + const len = lineLen(item.draw); + item.draw.style.strokeDasharray = `${len}`; + item.draw.style.strokeDashoffset = `${len}`; + item.draw.style.opacity = "0"; + } + if (item.arrow) item.arrow.style.opacity = "0"; + for (const el of item.fade) el.style.opacity = "0"; + } + + const obs = new IntersectionObserver( + ([e]) => { + if (!e.isIntersecting) return; + obs.disconnect(); + onStart(); + let lastDelay = 0; + let cumDelay = 800; + for (let i = 0; i < timeline.length; i++) { + const item = timeline[i]; + const delay = cumDelay; + if (delay > lastDelay) lastDelay = delay; + cumDelay += item.isNote ? 2000 : 1200; + + const drawEl = item.draw; + const arrowEl = item.arrow; + timers.push( + setTimeout(() => { + if (skipped) return; + if (drawEl) { + drawEl.style.transition = + "opacity 0.3s ease, stroke-dashoffset 1.2s ease-out"; + drawEl.style.opacity = "1"; + drawEl.style.strokeDashoffset = "0"; + } + for (const el of item.fade) { + el.style.transition = drawEl + ? "opacity 0.6s ease" + : "opacity 0.8s ease"; + el.style.opacity = "1"; + } + if (arrowEl) { + timers.push( + setTimeout(() => { + if (skipped) return; + arrowEl.style.transition = "opacity 0.3s ease"; + arrowEl.style.opacity = "1"; + }, 1000), + ); + } + }, delay), + ); + } + timers.push( + setTimeout(() => { + if (!skipped) onComplete(); + }, lastDelay + 1800), + ); + }, + { threshold: 0.15 }, + ); + obs.observe(svg); + + return handle; +} + +export function lineLen(el: SVGElement): number { + const x1 = +(el.getAttribute("x1") || 0); + const x2 = +(el.getAttribute("x2") || 0); + const y1 = +(el.getAttribute("y1") || 0); + const y2 = +(el.getAttribute("y2") || 0); + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); +} + +// --------------------------------------------------------------------------- +// React component +// --------------------------------------------------------------------------- + +export function MermaidDiagram({ chart }: { chart: string }) { + const wrapperRef = useRef(null); + const svgRef = useRef(null); + const animRef = useRef(null); + const [isDark, setIsDark] = useState(false); + const [phase, setPhase] = useState<"idle" | "playing" | "done">("idle"); + + useEffect(() => { + const check = () => + setIsDark( + document.documentElement.style.colorScheme === "dark" || + document.documentElement.classList.contains("dark"), + ); + check(); + const obs = new MutationObserver(check); + obs.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class", "style"], + }); + return () => obs.disconnect(); + }, []); + + const renderDiagram = useCallback(() => { + const el = svgRef.current; + if (!el || !el.isConnected) return; + setPhase("idle"); + try { + const parsed = parse(chart); + const lo = doLayout(parsed); + const th = isDark ? THEMES.dark : THEMES.light; + el.innerHTML = render(lo, th); + const svg = el.querySelector("svg"); + if (!svg) return; + svg.style.maxWidth = "100%"; + svg.style.height = "auto"; + svg.style.display = "block"; + svg.style.margin = "0 auto"; + animRef.current = animate( + svg, + () => setPhase("done"), + () => setPhase("playing"), + ); + } catch (err) { + console.error("MermaidDiagram:", err); + } + }, [chart, isDark]); + + useEffect(() => { + const el = svgRef.current; + if (!el) return; + let dead = false; + const raf = requestAnimationFrame(() => { + if (!dead) renderDiagram(); + }); + return () => { + dead = true; + cancelAnimationFrame(raf); + el.innerHTML = ""; + }; + }, [renderDiagram]); + + const th = isDark ? THEMES.dark : THEMES.light; + + const btnStyle: React.CSSProperties = { + position: "absolute", + top: 12, + right: 12, + width: 28, + height: 28, + borderRadius: "50%", + border: `1px solid ${th.actorStroke}`, + background: th.actorFill, + color: th.textMuted, + display: "flex", + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + padding: 0, + opacity: 0.7, + transition: "opacity 0.2s", + }; + + return ( +
+
+ {phase === "playing" && ( + + )} + {phase === "done" && ( + + )} +
+ ); +} diff --git a/src/pages/guide/machine-payments/client.mdx b/src/pages/guide/machine-payments/client.mdx new file mode 100644 index 00000000..4ae816d8 --- /dev/null +++ b/src/pages/guide/machine-payments/client.mdx @@ -0,0 +1,202 @@ +--- +title: Client Quickstart +description: Set up an MPP client on Tempo. Polyfill fetch to automatically pay for 402 responses with TIP-20 stablecoins. +--- + +import { Card, Cards } from 'vocs' + +# Client quickstart + +Polyfill `fetch` to handle `402` responses. Your existing code works unchanged — payments happen in the background. + +::::steps + +### Install dependencies + +:::code-group +```bash [npm] +npm install mppx viem +``` +```bash [pnpm] +pnpm add mppx viem +``` +```bash [bun] +bun add mppx viem +``` +::: + +### Define an account + +```ts +import { privateKeyToAccount } from 'viem/accounts' + +const account = privateKeyToAccount('0xabc…123') +``` + +:::tip +With Tempo, you can also use [Passkey or WebCrypto accounts](https://viem.sh/tempo/accounts). +::: + +### Create payment handler + +Call `Mppx.create` at startup. This polyfills `fetch` to automatically handle `402` payment challenges. + +```ts +import { privateKeyToAccount } from 'viem/accounts' +import { Mppx, tempo } from 'mppx/client' + +const account = privateKeyToAccount('0xabc…123') + +Mppx.create({ + methods: [tempo({ account })], +}) +``` + +:::tip +If you want to avoid polyfilling, use the bound `fetch` instead. + +```ts +const mppx = Mppx.create({ + polyfill: false, + methods: [tempo({ account })] +}) + +const response = await mppx.fetch('https://api.example.com/resource') +``` +::: + +### Request protected resources + +Use `fetch`. Payment happens when a server returns `402`. + +```ts +const response = await fetch('https://api.example.com/resource') +``` + +:::: + +## Learn more + +### Wagmi + +You can inject a [Wagmi](https://wagmi.sh) connector into Mppx by passing the `getConnectorClient` function. + +:::code-group + +```ts [example.ts] +import { Mppx, tempo } from 'mppx/client' +import { getConnectorClient } from 'wagmi/actions' +import { config } from './config' + +Mppx.create({ + methods: [tempo({ + getClient: (parameters) => getConnectorClient(config, parameters), + })], +}) +``` + +```ts [config.ts] +import { createConfig, http } from 'wagmi' +import { webAuthn } from 'wagmi/tempo' +import { tempoModerato } from 'viem/chains' + +export const config = createConfig({ + connectors: [webAuthn()], + chains: [tempoModerato], + transports: { + [tempoModerato.id]: http(), + }, +}) +``` + +::: + +### Per-request accounts + +Pass accounts on individual requests instead of at setup: + +```ts +import { privateKeyToAccount } from 'viem/accounts' +import { Mppx, tempo } from 'mppx/client' + +const mppx = Mppx.create({ + polyfill: false, + methods: [tempo()] +}) + +const response = await mppx.fetch('https://api.example.com/resource', { + context: { + account: privateKeyToAccount('0xabc…123'), + } +}) +``` + +### Manual payment handling + +Use `Mppx.create` for full control over the payment flow: + +- Present payment UI before paying +- Implement custom retry logic +- Handle credentials manually + +```ts +import { Mppx, tempo } from 'mppx/client' +import { privateKeyToAccount } from 'viem/accounts' + +const mppx = Mppx.create({ + polyfill: false, + methods: [tempo()], +}) + +const response = await fetch('https://api.example.com/resource') + +if (response.status === 402) { + const credential = await mppx.createCredential(response, { + account: privateKeyToAccount('0x...'), + }) + + const paidResponse = await fetch('https://api.example.com/resource', { + headers: { Authorization: credential }, + }) +} +``` + +### Payment receipts + +On success, the server returns a `Payment-Receipt` header: + +```ts +import { Receipt } from 'mppx' + +const response = await fetch('https://api.example.com/resource') + +const receipt = Receipt.fromResponse(response) + +console.log(receipt.status) +// success +console.log(receipt.reference) +// 0xtx789abc... +``` + +## Next steps + + + + + + diff --git a/src/pages/guide/machine-payments/index.mdx b/src/pages/guide/machine-payments/index.mdx new file mode 100644 index 00000000..f1b34c6a --- /dev/null +++ b/src/pages/guide/machine-payments/index.mdx @@ -0,0 +1,104 @@ +--- +title: Machine Payments +description: Charge for APIs, MCP tools, and digital content using the Machine Payments Protocol (MPP) on Tempo with TIP-20 stablecoins. +--- + +import { Card, Cards } from 'vocs' +import { MermaidDiagram } from '../../../components/MermaidDiagram' + +# Make Machine Payments + +The [Machine Payments Protocol](https://mpp.dev) (MPP) adds inline payments to any HTTP endpoint. Clients — apps, agents, or humans — pay as part of their request, and the server verifies payment before returning the response. + +## Payment flow + +A client requests a paid resource, the server responds with `402` and a `Challenge` describing the price. The client pays, retries with a `Credential` transaction, and the server returns the resource with a `Receipt`. + +>Server: (1) GET /resource + Server-->>Client: (2) 402 Payment Required + Challenge + Note over Client: (3) Client fulfills payment + Client->>Server: (4) GET /resource + Credential + Note over Server: (5) Server verifies payment + Server-->>Client: (6) 200 OK + Receipt +`} /> + +1. **Request** — Any HTTP method (`GET`, `POST`, etc.) +2. **Challenge** — `402` with `WWW-Authenticate: Payment` header describing amount, currency, and recipient +3. **Pay** — Client signs a transaction or fulfills payment off-chain +4. **Retry** — Client re-sends with `Authorization: Payment` header containing the Credential +5. **Deliver** — Server verifies, returns `200` with `Payment-Receipt` header + +## Why Tempo + +Tempo's transaction model is explicitly designed for inline payments using MPP: + +- **~500ms finality** — Deterministic confirmation fast enough for synchronous request/response flows +- **Sub-cent fees** — Low enough for micropayments and per-request billing +- **Fee sponsorship** — Servers can cover gas on behalf of clients so they only need stablecoins +- **2D and expiring nonces** — Parallel nonce lanes prevent payment transactions from blocking other account activity +- **High throughput** — Supports the on-chain settlement volume that payment channels generate at scale + +## Payment intents + +Two [intents](https://mpp.dev/protocol#payment-intents) are available on Tempo: + +| | **Charge** | **Session** | +|---|---|---| +| **Pattern** | One-time payment per request | Continuous pay-as-you-go | +| **Latency** | ~500ms (on-chain confirmation) | Near-zero (off-chain vouchers) | +| **Best for** | Single API calls, content access, one-off purchases | LLM APIs, metered services, usage-based billing | +| **On-chain cost** | Per request | Amortized across many requests | + +## Use cases + +- **Paid APIs** — Charge per request without API keys, billing accounts, or signup flows. +- **MCP tools** — Monetize tool calls served through the Model Context Protocol. Agents pay per call without OAuth or account setup. +- **Digital content** — Charge per access for articles, data feeds, or media without subscription paywalls. + +## Get started + + + + + + + + +## SDKs and tools + +| SDK | Package | Install | +|-----|---------|---------| +| TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx viem` | +| Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install pympp` | +| Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp-rs` | + +See the [full SDK documentation](https://mpp.dev/sdk) for API reference and advanced usage. + +## Learn more + +- [MPP documentation](https://mpp.dev) — Full protocol docs, SDK reference, and guides +- [IETF specs](https://paymentauth.org/) — Normative protocol specification +- [Protocol overview](https://mpp.dev/protocol) — Challenges, Credentials, Receipts, and transports diff --git a/src/pages/guide/machine-payments/one-time-payments.mdx b/src/pages/guide/machine-payments/one-time-payments.mdx new file mode 100644 index 00000000..3b19af3f --- /dev/null +++ b/src/pages/guide/machine-payments/one-time-payments.mdx @@ -0,0 +1,178 @@ +--- +title: Accept One-Time Payments +description: Charge per request on Tempo using the mppx charge intent. Each request triggers a TIP-20 transfer that settles in ~500ms. +--- + +import { Card, Cards } from 'vocs' + +# Accept one-time payments + +Build a payment-gated API that charges $0.01 per request using `mppx`. The server returns a random photo from [Picsum](https://picsum.photos) behind a paywall. + +## Server setup + +::::steps + +### Install dependencies + +:::code-group +```bash [npm] +npm install mppx viem +``` +```bash [pnpm] +pnpm add mppx viem +``` +```bash [bun] +bun add mppx viem +``` +::: + +### Set up `Mppx` instance + +Set up an `Mppx` instance with the `tempo` method. + +- `recipient` is the address where you receive payments. +- `currency` is the token address for payments (in this case, `pathUSD`). + +```ts +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) +``` + +### Add a payment-gated route + +Add payment verification using `mppx.charge` as route middleware. The handler only runs after payment is verified. + +:::code-group + +```ts [Next.js] +import { Mppx, tempo } from 'mppx/nextjs' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +export const GET = + mppx.charge({ amount: '0.01', description: 'Random stock photo' }) + (async () => { + const res = await fetch('https://picsum.photos/1024/1024') + return Response.json({ url: res.url }) + }) +``` + +```ts [Hono] +import { Hono } from 'hono' +import { Mppx, tempo } from 'mppx/hono' + +const app = new Hono() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/api/photo', + mppx.charge({ amount: '0.01', description: 'Random stock photo' }), + async (c) => { + const res = await fetch('https://picsum.photos/1024/1024') + return c.json({ url: res.url }) + }, +) +``` + +```ts [Express] +import express from 'express' +import { Mppx, tempo } from 'mppx/express' + +const app = express() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/api/photo', + mppx.charge({ amount: '0.01', description: 'Random stock photo' }), + async (req, res) => { + const response = await fetch('https://picsum.photos/1024/1024') + res.json({ url: response.url }) + }, +) +``` + +```ts [Fetch API] +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +Bun.serve({ + async fetch(request) { + const result = await mppx.charge({ + amount: '0.01', + description: 'Random stock photo', + })(request) + + if (result.status === 402) return result.challenge + + const res = await fetch('https://picsum.photos/1024/1024') + return result.withReceipt(Response.json({ url: res.url })) + }, +}) +``` + +::: + +### Test the endpoint + +```bash +# Create account funded with testnet tokens +$ npx mppx account create + +# Make a paid request +$ npx mppx http://localhost:3000/api/photo +``` + +:::: + +## Next steps + + + + + + diff --git a/src/pages/guide/machine-payments/pay-as-you-go.mdx b/src/pages/guide/machine-payments/pay-as-you-go.mdx new file mode 100644 index 00000000..c3845507 --- /dev/null +++ b/src/pages/guide/machine-payments/pay-as-you-go.mdx @@ -0,0 +1,236 @@ +--- +title: Accept Pay-As-You-Go Payments +description: Session-based billing on Tempo with MPP payment channels. Clients deposit funds, sign off-chain vouchers, and pay per request without on-chain latency. +--- + +import { Card, Cards } from 'vocs' +import { MermaidDiagram } from '../../../components/MermaidDiagram' + +# Accept pay-as-you-go payments + +Build a payment-gated photo gallery API that charges $0.01 per photo using `mppx` sessions. The server returns random photos from [Picsum](https://picsum.photos) behind a paywall. + +:::info +Unlike [one-time payments](/guide/machine-payments/one-time-payments), sessions open a payment channel once and use off-chain vouchers for each subsequent request — vouchers are processed in pure CPU-bound signature checks, not bottlenecked by blockchain throughput. +::: + +## How sessions work + +>Tempo: (1) Deposit tokens + Tempo-->>Client: Channel created + Client->>Server: (2) Open credential + Note over Server: Verify on-chain deposit + Server-->>Client: 200 OK (session established) + loop Per request + Client->>Server: (3) Request + voucher + Note over Server: ecrecover only + Server-->>Client: 200 OK + Receipt + end + Note over Server: (4) Periodic settlement + Server->>Tempo: settle(channelId, voucher) + Client->>Server: (5) Close + Server->>Tempo: close(channelId, voucher) + Tempo-->>Client: Refund remaining deposit +`} /> + +1. **Open** — Client deposits funds into an on-chain escrow contract, creating a payment channel +2. **Session** — Client signs EIP-712 vouchers with increasing cumulative amounts as service is consumed +3. **Top up** — If the channel runs low, the client deposits additional tokens without closing the channel +4. **Close** — Either party closes the channel, settling the final balance on-chain and refunding unused deposit + +## Server setup + +::::steps + +### Install dependencies + +:::code-group +```bash [npm] +npm install mppx viem +``` +```bash [pnpm] +pnpm add mppx viem +``` +```bash [bun] +bun add mppx viem +``` +::: + +### Set up `Mppx` instance + +Set up an `Mppx` instance with the `tempo` method. + +- `recipient` is the address where you receive payments. +- `currency` is the token address for payments (in this case, `pathUSD`). + +```ts +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) +``` + +### Add a session-gated route + +Add payment verification using `mppx.session` as route middleware. The handler only runs after payment is verified. + +:::code-group + +```ts [Next.js] +import { Mppx, tempo } from 'mppx/nextjs' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +export const GET = + mppx.session({ amount: '0.01', unitType: 'photo' }) + (async () => { + const res = await fetch('https://picsum.photos/200/200') + return Response.json({ url: res.url }) + }) +``` + +```ts [Hono] +import { Hono } from 'hono' +import { Mppx, tempo } from 'mppx/hono' + +const app = new Hono() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/api/sessions/photo', + mppx.session({ amount: '0.01', unitType: 'photo' }), + async (c) => { + const res = await fetch('https://picsum.photos/200/200') + return c.json({ url: res.url }) + }, +) +``` + +```ts [Express] +import express from 'express' +import { Mppx, tempo } from 'mppx/express' + +const app = express() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/api/sessions/photo', + mppx.session({ amount: '0.01', unitType: 'photo' }), + async (req, res) => { + const response = await fetch('https://picsum.photos/200/200') + res.json({ url: response.url }) + }, +) +``` + +```ts [Fetch API] +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +Bun.serve({ + async fetch(request) { + const result = await mppx.session({ + amount: '0.01', + unitType: 'photo', + })(request) + + if (result.status === 402) return result.challenge + + const res = await fetch('https://picsum.photos/200/200') + return result.withReceipt(Response.json({ url: res.url })) + }, +}) +``` + +::: + +### Test the endpoint + +```bash +# Create account funded with testnet tokens +$ npx mppx account create + +# Make a paid request +$ npx mppx http://localhost:3000/api/sessions/photo +``` + +:::: + +## Client setup + +When using sessions from a client, set `maxDeposit` to enable automatic channel management. This is the maximum amount of tokens the client locks into the payment channel's escrow contract. Any unspent deposit is refunded when the channel closes. + +```ts +import { Mppx, tempo } from 'mppx/client' +import { privateKeyToAccount } from 'viem/accounts' + +const mppx = Mppx.create({ + methods: [tempo({ + account: privateKeyToAccount('0x...'), + maxDeposit: '1', // Lock up to 1 pathUSD per channel + })], +}) + +// Each fetch automatically manages the session lifecycle: +// 1st request: opens channel on-chain, sends initial voucher +// 2nd+ requests: sends off-chain vouchers (no on-chain tx) +const res = await fetch('http://localhost:3000/api/sessions/photo') +``` + +- **`maxDeposit: '1'`** — Locks up to 1 pathUSD into the payment channel. At $0.01/photo, this covers up to 100 requests before the channel runs out. +- The client handles the full session lifecycle automatically: channel open, voucher signing, and retry after `402` responses. +- If the server sets `suggestedDeposit`, the client uses `min(suggestedDeposit, maxDeposit)`. + +## Next steps + + + + + + diff --git a/src/pages/guide/machine-payments/server.mdx b/src/pages/guide/machine-payments/server.mdx new file mode 100644 index 00000000..0271c362 --- /dev/null +++ b/src/pages/guide/machine-payments/server.mdx @@ -0,0 +1,180 @@ +--- +title: Server Quickstart +description: Add payment gating to any HTTP endpoint on Tempo with mppx middleware for Next.js, Hono, Express, and the Fetch API. +--- + +import { Card, Cards } from 'vocs' + +# Server quickstart + +Plug MPP into any server framework to accept payments for protected resources. Use `mppx` middleware for your framework, or call `mppx/server` directly with the Fetch API. + +## Framework middleware + +Use the framework-specific middleware from `mppx` to integrate payment into your server. Each middleware handles the `402` challenge/credential flow and attaches receipts automatically. + +:::code-group + +```ts [Next.js] +import { Mppx, tempo } from 'mppx/nextjs' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +export const GET = + mppx.charge({ amount: '0.1' }) + (() => Response.json({ data: '...' })) +``` + +```ts [Hono] +import { Hono } from 'hono' +import { Mppx, tempo } from 'mppx/hono' + +const app = new Hono() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/resource', + mppx.charge({ amount: '0.1' }), + (c) => c.json({ data: '...' }), +) +``` + +```ts [Express] +import express from 'express' +import { Mppx, tempo } from 'mppx/express' + +const app = express() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +app.get( + '/resource', + mppx.charge({ amount: '0.1' }), + (req, res) => res.json({ data: '...' })) +``` + +::: + +:::tip +You can override `currency` and `recipient` per call if different routes need different payment configurations. + +```ts +mppx.charge({ + amount: '0.1', + currency: '0x…', + recipient: '0x…', +}) +``` +::: + +## Manual mode + +If you prefer full control over the payment flow, use `mppx/server` directly with the Fetch API. + +```ts +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) + +export async function handler(request: Request) { + const response = await mppx.charge({ amount: '0.1' })(request) + + // Payment required: send 402 response with challenge + if (response.status === 402) return response.challenge + + // Payment verified: attach receipt and return resource + return response.withReceipt(Response.json({ data: '...' })) +} +``` + +:::info[Currency and recipient values] +`currency` is the TIP-20 token contract address — [`0x20c0…`](https://explore.tempo.xyz/address/0x20c0000000000000000000000000000000000000?live=false) is PathUSD on Tempo. `recipient` is the address that receives payment. See the [Tempo payment method](https://mpp.dev/payment-methods/tempo) for supported tokens. +::: + +## Push & pull modes + +Tempo charges support two transaction submission modes, determined by the client: + +- **`pull` mode (default)**: the client signs the transaction and sends the serialized transaction to the server. The server broadcasts it and verifies on-chain. This enables the server to sponsor gas fees via a `feePayer`. +- **`push` mode**: the client builds, signs, and broadcasts the transaction itself (for example, via a browser wallet). It sends the transaction hash to the server, which verifies the payment by fetching the receipt. + +Your server handles both modes automatically — no configuration required. The server inspects the credential payload type (`transaction` for pull, `hash` for push) and verifies accordingly. + +### Fee sponsorship + +To sponsor gas fees for pull-mode clients, pass a `feePayer` account to `tempo()`: + +```ts +import { Mppx, tempo } from 'mppx/server' +import { privateKeyToAccount } from 'viem/accounts' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + feePayer: privateKeyToAccount('0x…'), + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + })], +}) +``` + +When a pull-mode client submits a signed transaction, the server co-signs with the fee payer account before broadcasting. Push-mode clients pay their own gas, so `feePayer` is ignored for those requests. + +## Testing your server + +After your server is running, test it with the `mppx` CLI: + +```bash +# Create an account funded with testnet tokens +$ npx mppx account create + +# Make a paid request +$ npx mppx /resource +``` + +:::tip +Use `npx mppx --inspect` to debug your server's Challenge response without making any payments. +::: + +## Next steps + + + + + + diff --git a/src/pages/guide/machine-payments/streamed-payments.mdx b/src/pages/guide/machine-payments/streamed-payments.mdx new file mode 100644 index 00000000..7bb141a8 --- /dev/null +++ b/src/pages/guide/machine-payments/streamed-payments.mdx @@ -0,0 +1,281 @@ +--- +title: Accept Streamed Payments +description: Per-token billing over Server-Sent Events on Tempo. Stream content word-by-word and charge per unit using MPP sessions with SSE. +--- + +import { Card, Cards } from 'vocs' + +# Accept streamed payments + +Build a payment-gated API that streams content word-by-word and charges $0.001 per word using `mppx` sessions with Server-Sent Events (SSE). + +:::info +Streamed payments extend [pay-as-you-go sessions](/guide/machine-payments/pay-as-you-go) with SSE. The server charges per token as content streams — if the channel balance runs out mid-stream, the client automatically sends a new voucher and the stream resumes. +::: + +## Server setup + +::::steps + +### Install dependencies + +:::code-group +```bash [npm] +npm install mppx viem +``` +```bash [pnpm] +pnpm add mppx viem +``` +```bash [bun] +bun add mppx viem +``` +::: + +### Set up `Mppx` instance with streaming + +Set up an `Mppx` instance with `sse: true` to enable SSE support on the session method. + +```ts +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + sse: true, + })], +}) +``` + +### Add a streaming route + +The handler returns an async generator — each yielded value becomes one SSE event and is charged one tick ($0.001). If the channel balance runs out mid-stream, the server emits `event: payment-need-voucher` and pauses until the client sends a new voucher. + +:::code-group + +```ts [Next.js] +import { Mppx, tempo } from 'mppx/nextjs' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + sse: true, + })], +}) + +const poem = { + title: 'The Road Not Taken', + author: 'Robert Frost', + lines: [ + 'Two roads diverged in a yellow wood,', + 'And sorry I could not travel both', + 'And be one traveler, long I stood', + 'And looked down one as far as I could', + 'To where it bent in the undergrowth;', + ], +} + +export const GET = + mppx.session({ amount: '0.001', unitType: 'word' }) + (async () => { + const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) + return async function* (stream) { + yield JSON.stringify({ title: poem.title, author: poem.author }) + for (const word of words) { + await stream.charge() + yield word + } + } + }) +``` + +```ts [Hono] +import { Hono } from 'hono' +import { Mppx, tempo } from 'mppx/hono' + +const app = new Hono() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + sse: true, + })], +}) + +const poem = { + title: 'The Road Not Taken', + author: 'Robert Frost', + lines: [ + 'Two roads diverged in a yellow wood,', + 'And sorry I could not travel both', + 'And be one traveler, long I stood', + 'And looked down one as far as I could', + 'To where it bent in the undergrowth;', + ], +} + +app.get( + '/api/sessions/poem', + mppx.session({ amount: '0.001', unitType: 'word' }), + async (c) => { + const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) + return async function* (stream) { + yield JSON.stringify({ title: poem.title, author: poem.author }) + for (const word of words) { + await stream.charge() + yield word + } + } + }, +) +``` + +```ts [Express] +import express from 'express' +import { Mppx, tempo } from 'mppx/express' + +const app = express() + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + sse: true, + })], +}) + +const poem = { + title: 'The Road Not Taken', + author: 'Robert Frost', + lines: [ + 'Two roads diverged in a yellow wood,', + 'And sorry I could not travel both', + 'And be one traveler, long I stood', + 'And looked down one as far as I could', + 'To where it bent in the undergrowth;', + ], +} + +app.get( + '/api/sessions/poem', + mppx.session({ amount: '0.001', unitType: 'word' }), + async (req, res) => { + const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) + return async function* (stream) { + yield JSON.stringify({ title: poem.title, author: poem.author }) + for (const word of words) { + await stream.charge() + yield word + } + } + }, +) +``` + +```ts [Fetch API] +import { Mppx, tempo } from 'mppx/server' + +const mppx = Mppx.create({ + methods: [tempo({ + currency: '0x20c0000000000000000000000000000000000000', + recipient: '0xa726a1CD723409074DF9108A2187cfA19899aCF8', + sse: true, + })], +}) + +const poem = { + title: 'The Road Not Taken', + author: 'Robert Frost', + lines: [ + 'Two roads diverged in a yellow wood,', + 'And sorry I could not travel both', + 'And be one traveler, long I stood', + 'And looked down one as far as I could', + 'To where it bent in the undergrowth;', + ], +} + +Bun.serve({ + async fetch(request) { + const result = await mppx.session({ + amount: '0.001', + unitType: 'word', + })(request) + + if (result.status === 402) return result.challenge + + const words = poem.lines.flatMap((line) => [...line.split(' '), '\\n']) + return result.withReceipt(async function* (stream) { + yield JSON.stringify({ title: poem.title, author: poem.author }) + for (const word of words) { + await stream.charge() + yield word + } + }) + }, +}) +``` + +::: + +### Test the endpoint + +```bash +# Create account funded with testnet tokens +$ npx mppx account create + +# Stream a paid poem +$ npx mppx http://localhost:3000/api/sessions/poem +``` + +:::: + +## Client setup + +Use `tempo.session()` from `mppx/client` to create a session manager. The `.sse()` method connects to the SSE endpoint and handles voucher renewal automatically — if the server requests a new voucher mid-stream, the client signs and sends one without interrupting the stream. + +```ts +import { tempo } from 'mppx/client' +import { privateKeyToAccount } from 'viem/accounts' + +const session = tempo.session({ + account: privateKeyToAccount('0x...'), + maxDeposit: '1', // Lock up to 1 pathUSD per channel +}) + +// .sse() returns an async iterable of SSE data payloads +const stream = await session.sse('http://localhost:3000/api/sessions/poem') + +for await (const word of stream) { + process.stdout.write(word + ' ') +} +``` + +- **`tempo.session()`** — Creates a session manager that handles the full channel lifecycle: open, voucher signing, and close. +- **`.sse()`** — Connects to an SSE endpoint. Automatically sends new vouchers when the server emits `payment-need-voucher` events. +- **`maxDeposit: '1'`** — Locks up to 1 pathUSD. At $0.001/word, this covers ~1,000 words before the channel needs a top-up. + +## Next steps + + + + + + diff --git a/vocs.config.ts b/vocs.config.ts index 3a578035..f14685eb 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -245,6 +245,36 @@ export default defineConfig({ }, ], }, + { + text: 'Make Machine Payments', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/guide/machine-payments', + }, + { + text: 'Client quickstart', + link: '/guide/machine-payments/client', + }, + { + text: 'Server quickstart', + link: '/guide/machine-payments/server', + }, + { + text: 'Accept one-time payments', + link: '/guide/machine-payments/one-time-payments', + }, + { + text: 'Accept pay-as-you-go payments', + link: '/guide/machine-payments/pay-as-you-go', + }, + { + text: 'Accept streamed payments', + link: '/guide/machine-payments/streamed-payments', + }, + ], + }, ], }, { From 8a942a05c7a6fd697e433f7eed4400aafd18755b Mon Sep 17 00:00:00 2001 From: onbjerg Date: Fri, 13 Mar 2026 18:30:01 +0100 Subject: [PATCH 03/40] docs: use stepped layout for Foundry install (#7) --- src/pages/sdk/foundry/index.mdx | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index 885e99df..6c5a9a9e 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -12,13 +12,13 @@ For general information about Foundry, see the [Foundry documentation](https://g ## Get started with Foundry -### Install using `foundryup` - Tempo's Foundry fork is installed through the standard upstream `foundryup` using the `-n tempo` flag, no separate installer is required. -Getting started is very easy: +::::steps + +## Install `foundryup` -Install regular `foundryup`: +If you don't have `foundryup` installed yet: ```bash curl -L https://foundry.paradigm.xyz | bash @@ -30,21 +30,22 @@ Or if you already have `foundryup` installed: foundryup --update ``` -Next, run: +## Install Tempo's Foundry fork ```bash foundryup -n tempo ``` -It will automatically install the latest `nightly` release of all precompiled binaries: [`forge`](https://getfoundry.sh/forge/overview#forge), [`cast`](https://getfoundry.sh/cast/overview#cast), [`anvil`](https://getfoundry.sh/anvil/overview#anvil), and [`chisel`](https://getfoundry.sh/chisel/overview#chisel). +This will automatically install the latest `nightly` release of all precompiled binaries: [`forge`](https://getfoundry.sh/forge/overview#forge), [`cast`](https://getfoundry.sh/cast/overview#cast), [`anvil`](https://getfoundry.sh/anvil/overview#anvil), and [`chisel`](https://getfoundry.sh/chisel/overview#chisel). +:::tip To install a specific version, replace `` with the desired release tag: - ```bash foundryup -n tempo -i ``` +::: -### Verify Installation +## Verify Installation ```bash forge -V @@ -56,7 +57,7 @@ You should see version information include `-tempo`, indicating you are using th # forge -tempo ( ) ``` -### Create a new Foundry project +## Create a new Foundry project Initialize a new project with Tempo support: @@ -66,6 +67,8 @@ forge init -n tempo my-project && cd my-project Each new project is configured for Tempo out of the box, with [`tempo-std`](https://github.com/tempoxyz/tempo-std), the Tempo standard library installed, containing helpers for Tempo's protocol-level features. +:::: + ## Use Foundry for your workflows All standard Foundry commands are supported out of the box. From 610b31f5067b48f165229482054fd461708c90d7 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Fri, 13 Mar 2026 18:30:03 +0100 Subject: [PATCH 04/40] docs: add Python SDK page (#4) --- src/pages/sdk/python/index.mdx | 217 +++++++++++++++++++++++++++++++++ vocs.config.ts | 4 + 2 files changed, 221 insertions(+) create mode 100644 src/pages/sdk/python/index.mdx diff --git a/src/pages/sdk/python/index.mdx b/src/pages/sdk/python/index.mdx new file mode 100644 index 00000000..2203d2d8 --- /dev/null +++ b/src/pages/sdk/python/index.mdx @@ -0,0 +1,217 @@ +--- +description: Build blockchain apps with the Tempo Python SDK. Send transactions, batch calls, and handle fee sponsorship using web3.py. +--- + +# Python + +Tempo distributes a Python SDK as a [web3.py](https://web3py.readthedocs.io/) extension. The SDK adds native support for Tempo transactions, including call batching, fee sponsorship, and access key management. + +The Tempo Python SDK can be used to perform common operations with the chain, such as: sending Tempo transactions, batching multiple calls, fee sponsorship, and more. + +::::steps + +## Install + +To install the Tempo Python SDK: + +```bash [pip] +pip install pytempo +``` + +:::tip +The SDK requires Python 3.9 or higher and web3.py 7.0+. +::: + +## Create a Client + +To interact with Tempo, create a web3.py client connected to a Tempo node: + +```python [main.py] +from web3 import Web3 + +w3 = Web3(Web3.HTTPProvider("https://rpc.testnet.tempo.xyz")) # [!code hl] + +block_number = w3.eth.block_number +print(f"Connected to Tempo at block {block_number}") +``` + +## Send a Transaction + +Build and send a transaction using the `TempoTransaction` class: + +```python [main.py] +import os +from web3 import Web3 +from pytempo import Call, TempoTransaction # [!code hl] + +w3 = Web3(Web3.HTTPProvider("https://rpc.testnet.tempo.xyz")) +private_key = os.environ["PRIVATE_KEY"] +account = w3.eth.account.from_key(private_key) + +# [!code hl:12] +tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + Call.create(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + ), +) + +signed_tx = tx.sign(private_key) # [!code hl] +tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) # [!code hl] +receipt = w3.eth.wait_for_transaction_receipt(tx_hash) +print(f"Transaction hash: {tx_hash.hex()}") +``` + +:::: + +## Examples + +### Token Transfer + +Send a TIP-20 token transfer using web3.py's contract interface: + +```python [transfer.py] +erc20_abi = '[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]}]' +token = w3.eth.contract(address="0x20c0000000000000000000000000000000000001", abi=erc20_abi) + +recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +amount = 100_000_000 # 100 tokens (6 decimals) + +transfer_data = token.encode_abi("transfer", args=[recipient, amount]) + +tx = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + Call.create(to=token.address, data=transfer_data), + ), +) +``` + +### Pay Fees in a Stablecoin + +Use a TIP-20 token to pay for transaction fees instead of the native token: + +```python [fee_token.py] +tx = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce=w3.eth.get_transaction_count(account.address), + fee_token="0x20c0000000000000000000000000000000000001", # AlphaUSD // [!code hl] + calls=( + Call.create(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), + ), +) +``` + +### Batch Multiple Calls + +Execute multiple operations atomically in a single transaction: + +```python [batch.py] +tx = TempoTransaction.create( + chain_id=42429, + gas_limit=300_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + Call.create(to=addr1, data=transfer1_data), # [!code hl] + Call.create(to=addr2, data=transfer2_data), # [!code hl] + Call.create(to=addr3, data=contract_call_data), # [!code hl] + ), +) + +signed_tx = tx.sign(private_key) +tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) +``` + +### Parallel Transactions (2D Nonces) + +Send multiple transactions concurrently using different nonce keys: + +```python [parallel.py] +tx1 = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce_key=1, # Sequence A // [!code hl] + nonce=0, + calls=(Call.create(to=recipient1, data=data1),), +) + +tx2 = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce_key=2, # Sequence B (parallel) // [!code hl] + nonce=0, + calls=(Call.create(to=recipient2, data=data2),), +) + +# Sign and send both in parallel +signed_tx1 = tx1.sign(private_key) +signed_tx2 = tx2.sign(private_key) +w3.eth.send_raw_transaction(signed_tx1.encode()) +w3.eth.send_raw_transaction(signed_tx2.encode()) +``` + +### Fee Sponsorship + +Have another account pay for transaction fees: + +```python [fee_payer.py] +# User creates and signs a transaction marked for fee sponsorship +tx = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + awaiting_fee_payer=True, # [!code hl] + calls=(Call.create(to=recipient, data=data),), +) + +signed_by_user = tx.sign(user_private_key) +final_tx = signed_by_user.sign(fee_payer_private_key, for_fee_payer=True) # [!code hl] +w3.eth.send_raw_transaction(final_tx.encode()) +``` + +### Transaction Validity Window + +Set a time window during which the transaction is valid: + +```python [validity.py] +import time + +now = int(time.time()) + +tx = TempoTransaction.create( + chain_id=42429, + gas_limit=100_000, + max_fee_per_gas=10_000_000_000, + max_priority_fee_per_gas=1_000_000_000, + nonce=nonce, + valid_after=now, # [!code hl] + valid_before=now + 3600, # 1 hour from now // [!code hl] + calls=(Call.create(to=recipient, data=data),), +) +``` + +## Next Steps + +After setting up the Python SDK, you can: + +- Follow a guide on how to [use accounts](/guide/use-accounts), [make payments](/guide/payments), [issue stablecoins](/guide/issuance), [exchange stablecoins](/guide/stablecoin-dex), and [more](/). +- View the [source on GitHub](https://github.com/tempoxyz/pytempo) +- View the [package on PyPI](https://pypi.org/project/pytempo/) diff --git a/vocs.config.ts b/vocs.config.ts index f14685eb..58a6a219 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -543,6 +543,10 @@ export default defineConfig({ text: 'Foundry', link: '/sdk/foundry', }, + { + text: 'Python', + link: '/sdk/python', + }, { text: 'Rust', link: '/sdk/rust', From 6456e53d64a59e7138d5b39273682ba3bbafea00 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Fri, 13 Mar 2026 18:33:24 +0100 Subject: [PATCH 05/40] docs: pin tempo-alloy (#5) --- src/pages/sdk/rust/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/sdk/rust/index.mdx b/src/pages/sdk/rust/index.mdx index 64cfcc69..689b162e 100644 --- a/src/pages/sdk/rust/index.mdx +++ b/src/pages/sdk/rust/index.mdx @@ -16,7 +16,7 @@ To install the Tempo extension, you will need to install [Alloy](https://alloy.r ```bash [cargo] cargo add alloy tokio -cargo add tempo-alloy --git https://github.com/tempoxyz/tempo +cargo add tempo-alloy --git https://github.com/tempoxyz/tempo --tag v1.4.2 ``` :::tip From 1796cb339fcf6697e87fc0f812573657dfcc84b8 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Fri, 13 Mar 2026 18:33:33 +0100 Subject: [PATCH 06/40] docs: tempo-go v0.3.0 (#6) --- src/pages/sdk/go/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/sdk/go/index.mdx b/src/pages/sdk/go/index.mdx index 5986104c..a0623cf9 100644 --- a/src/pages/sdk/go/index.mdx +++ b/src/pages/sdk/go/index.mdx @@ -16,7 +16,7 @@ The Tempo Go SDK can be used to perform common operations with the chain, such a To install the Tempo Go SDK: ```bash [go] -go get github.com/tempoxyz/tempo-go@v0.1.0 +go get github.com/tempoxyz/tempo-go@v0.3.0 ``` :::tip From 037c8ede91e5ce5421b2e2a7de5be53951811b25 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 15 Mar 2026 19:06:48 +0100 Subject: [PATCH 07/40] docs: add Python to SDKs Support banner (#9) --- src/pages/guide/tempo-transaction/index.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/guide/tempo-transaction/index.mdx b/src/pages/guide/tempo-transaction/index.mdx index 12103b2d..69a6b0ba 100644 --- a/src/pages/guide/tempo-transaction/index.mdx +++ b/src/pages/guide/tempo-transaction/index.mdx @@ -10,7 +10,7 @@ import TempoTxProperties from '../../../snippets/tempo-tx-properties.mdx' Tempo Transactions are a new [EIP-2718](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-2718.md) transaction type, exclusively available on Tempo. :::note[SDKs Support] -Transaction [SDKs](#integration-guides) are available for TypeScript, Rust, Go, and Foundry. +Transaction [SDKs](#integration-guides) are available for TypeScript, Rust, Go, Python, and Foundry. ::: If you're integrating with Tempo, we **strongly recommend** using Tempo Transactions, and not regular Ethereum transactions. Learn more about the benefits below, or follow the guide on issuance [here](/guide/issuance). From fa95d5c039df642b56097e0ce6baba6d4719a351 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 15 Mar 2026 19:06:55 +0100 Subject: [PATCH 08/40] docs: add cast tabs to tempo tx examples (#10) --- src/snippets/tempo-tx-properties.mdx | 148 +++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index a03dac51..91166acb 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -65,6 +65,18 @@ user's preferred fee token and the validator's preferred token. + + + ```bash + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.fee-token 0x20c0000000000000000000000000000000000001 # [!code hl] + ``` + + + ```tsx @@ -158,6 +170,32 @@ over the transaction with a special "fee payer envelope" to commit to paying fee + + + ```bash + # 1. Get the fee payer signature hash + $ FEE_PAYER_HASH=$(cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $SENDER_KEY \ + --tempo.print-sponsor-hash) # [!code hl] + + # 2. Sponsor signs the hash + $ SPONSOR_SIG=$(cast wallet sign \ + --private-key $SPONSOR_KEY \ + "$FEE_PAYER_HASH" \ + --no-hash) # [!code hl] + + # 3. Send with sponsor signature + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $SENDER_KEY \ + --tempo.sponsor-signature "$SPONSOR_SIG" # [!code hl] + ``` + + + ```tsx @@ -309,6 +347,19 @@ parameter. + + + ```bash + $ cast batch-send \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" \ + --call "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef::setNumber(uint256):500" \ + --call "0xcafebabecafebabecafebabecafebabecafebabe::increment()" + ``` + + + ```tsx @@ -408,6 +459,26 @@ transactions thereafter can be signed by the access key. + + + ```bash + # 1. Authorize the access key via Account Keychain precompile + $ cast send 0xAAAAAAAA00000000000000000000000000000000 \ + 'authorizeKey(address,uint8,uint64,bool,(address,uint256)[])' \ + $ACCESS_KEY_ADDR 0 1893456000 false "[]" \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $ROOT_PRIVATE_KEY # [!code hl] + + # 2. Send using the access key + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef \ + --rpc-url $TEMPO_RPC_URL \ + --tempo.root-account $ROOT_ADDRESS \ + --tempo.access-key $ACCESS_KEY_PRIVATE_KEY # [!code hl] + ``` + + + ```tsx @@ -518,6 +589,31 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. + + + ```bash + # Send three transactions concurrently using different nonce keys + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 1 # [!code hl] + + $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ + --data 0xcafebabe0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 2 # [!code hl] + + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --async --nonce 0 --tempo.nonce-key 3 # [!code hl] + ``` + + + ```tsx @@ -621,6 +717,19 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo + + + ```bash + $ VALID_BEFORE=$(($(date +%s) + 20)) + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.expiring-nonce --tempo.valid-before $VALID_BEFORE # [!code hl] + ``` + + + ```tsx @@ -724,6 +833,30 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** + + + ```bash + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 1 # [!code hl] + + $ cast send 0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef \ + --data 0xcafebabe0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 2 # [!code hl] + + $ cast send 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --nonce 0 --tempo.nonce-key 3 # [!code hl] + ``` + + + ```tsx @@ -811,6 +944,21 @@ the transaction can be included in a block. + + + ```bash + $ VALID_AFTER=$(date -d '2026-01-01' +%s) + $ VALID_BEFORE=$(date -d '2026-01-02' +%s) + $ cast mktx 0xcafebabecafebabecafebabecafebabecafebabe \ + --data 0xdeadbeef0000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.valid-after $VALID_AFTER \ + --tempo.valid-before $VALID_BEFORE # [!code hl] + ``` + + + ```tsx From b7b50277811852327f385f29def71c5e32a3ac0f Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 15 Mar 2026 19:46:13 +0100 Subject: [PATCH 09/40] docs: add Rust tabs to tempo tx examples (#11) --- src/snippets/rust-provider.rs | 15 + src/snippets/tempo-tx-properties.mdx | 407 ++++++++++++++++++++++++++- 2 files changed, 414 insertions(+), 8 deletions(-) create mode 100644 src/snippets/rust-provider.rs diff --git a/src/snippets/rust-provider.rs b/src/snippets/rust-provider.rs new file mode 100644 index 00000000..b27a081c --- /dev/null +++ b/src/snippets/rust-provider.rs @@ -0,0 +1,15 @@ +// [!region setup] +use alloy::providers::ProviderBuilder; +use tempo_alloy::TempoNetwork; + +pub async fn get_provider() -> Result< + impl alloy::providers::Provider, + Box, +> { + let provider = ProviderBuilder::new_with_network::() + .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) + .await?; + + Ok(provider) +} +// [!endregion setup] diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 91166acb..573fd9ee 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -65,6 +65,43 @@ user's preferred fee token and the validator's preferred token. + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_fee_token(alpha_usd) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")), + ) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -170,6 +207,55 @@ over the transaction with a special "fee payer envelope" to commit to paying fee + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::signers::{SignerSync, local::PrivateKeySigner}; + use tempo_alloy::primitives::transaction::tempo_transaction::Call; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let mut tx = TempoTransactionRequest { + calls: vec![Call { + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), + value: U256::ZERO, + input: bytes!("deadbeef"), + }], + ..Default::default() + }; + + // Fill gas, nonce, etc. from the network + let filled = provider.fill(tx).await?; + let mut tempo_tx = filled.build_aa()?; + + // Fee payer signs the transaction // [!code hl] + let fee_payer = PrivateKeySigner::from_str("0x...")?; // [!code hl] + let sender_addr = provider.default_signer_address(); // [!code hl] + let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); // [!code hl] + tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] + + let pending = provider.send_transaction(tempo_tx).await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -347,6 +433,57 @@ parameter. + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::primitives::transaction::Call; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let pending = provider + .send_transaction(TempoTransactionRequest { + calls: vec![ // [!code hl] + Call { // [!code hl] + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + Call { // [!code hl] + to: address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("cafebabe0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + Call { // [!code hl] + to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), // [!code hl] + value: U256::ZERO, // [!code hl] + input: bytes!("deadbeef0000000000000000000000000000000001"), // [!code hl] + }, // [!code hl] + ], // [!code hl] + ..Default::default() + }) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -459,6 +596,94 @@ transactions thereafter can be signed by the access key. + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; + use alloy::signers::{SignerSync, local::PrivateKeySigner}; + use tempo_alloy::primitives::transaction::key_authorization::{ + KeyAuthorization, SignedKeyAuthorization, + }; + use tempo_alloy::primitives::transaction::tt_signature::{ + KeychainSignature, PrimitiveSignature, SignatureType, TempoSignature, + }; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let root = PrivateKeySigner::from_str("0x...")?; + let access_key = PrivateKeySigner::random(); + + // Sign key authorization with root account // [!code hl] + let authorization = KeyAuthorization { // [!code hl] + chain_id: 4217, // [!code hl] + key_type: SignatureType::Secp256k1, // [!code hl] + key_id: access_key.address(), // [!code hl] + expiry: None, // [!code hl] + limits: None, // [!code hl] + }; // [!code hl] + let sig = root.sign_hash_sync(&authorization.signature_hash())?; // [!code hl] + let key_authorization = SignedKeyAuthorization { // [!code hl] + authorization, // [!code hl] + signature: sig.into(), // [!code hl] + }; // [!code hl] + + // Attach key authorization to a transaction signed by the root key. // [!code hl] + // This registers the access key on-chain via the Account Keychain. // [!code hl] + provider + .send_transaction( + TempoTransactionRequest { + key_authorization: Some(key_authorization), // [!code hl] + ..Default::default() + } + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")), + ) + .await? + .get_receipt() + .await?; + + // Sign a subsequent transaction with the access key // [!code hl] + let tx = TempoTransactionRequest::default() + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef")); + + let filled = provider.fill(tx).await?; + let tempo_tx = filled.build_aa()?; + + // Access key signs a domain-separated hash bound to the root account // [!code hl] + let inner_hash = // [!code hl] + KeychainSignature::signing_hash(tempo_tx.signature_hash(), root.address()); // [!code hl] + let inner_sig = access_key.sign_hash_sync(&inner_hash)?; // [!code hl] + let signature = TempoSignature::Keychain(KeychainSignature::new( // [!code hl] + root.address(), // [!code hl] + PrimitiveSignature::Secp256k1(inner_sig), // [!code hl] + )); // [!code hl] + + let envelope = tempo_tx.into_signed(signature); // [!code hl] + let pending = provider // [!code hl] + .send_raw_transaction(envelope.encoded_2718().as_ref()) // [!code hl] + .await?; // [!code hl] + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -589,6 +814,55 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + // Send three transactions concurrently using different nonce keys + let (r1, r2, r3) = tokio::try_join!( + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(1)) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(2)) // [!code hl] + .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + .with_input(bytes!("cafebabe0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(3)) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + )?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -705,16 +979,43 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo - ```rust - let pending = provider - .send_transaction(TempoTransactionRequest { - nonce_key: Some(U256::MAX), // [!code focus] - valid_before: Some(SystemTime::now().duration_since(UNIX_EPOCH).as_secs() + 30), // [!code focus] - ..Default::default(), - }) - .await?; + :::code-group + + ```rust [example.rs] + use std::time::{SystemTime, UNIX_EPOCH}; + + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let valid_before = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + 30; + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::MAX) // [!code focus] + .with_valid_before(valid_before) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ) + .await?; + + Ok(()) + } ``` + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + @@ -833,6 +1134,54 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let (r1, r2, r3) = tokio::try_join!( + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(1)) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(2)) // [!code focus] + .with_to(address!("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef")) + .with_input(bytes!("cafebabe0000000000000000000000000000000001")), + ), + provider.send_transaction( + TempoTransactionRequest::default() + .with_nonce_key(U256::from(3)) // [!code focus] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ), + )?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash @@ -944,6 +1293,48 @@ the transaction can be included in a block. + + + :::code-group + + ```rust [example.rs] + use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; + use tempo_alloy::rpc::TempoTransactionRequest; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + // 2026-01-01 00:00:00 UTC + let valid_after = 1_767_225_600; + // 2026-01-02 00:00:00 UTC + let valid_before = 1_767_312_000; + + let pending = provider + .send_transaction( + TempoTransactionRequest::default() + .with_valid_after(valid_after) // [!code hl] + .with_valid_before(valid_before) // [!code hl] + .with_to(address!("0xcafebabecafebabecafebabecafebabecafebabe")) + .with_input(bytes!("deadbeef0000000000000000000000000000000001")), + ) + .await?; + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + ```bash From 4fcc070ea4def54c626b39ed24acfbc27b14922d Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 15 Mar 2026 20:18:51 +0100 Subject: [PATCH 10/40] docs: pytempo examples (#12) --- src/snippets/tempo-tx-properties.mdx | 366 +++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 573fd9ee..e221c115 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -102,6 +102,47 @@ user's preferred fee token and the validator's preferred token. + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + alpha_usd = "0x20c0000000000000000000000000000000000001" + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + fee_token=alpha_usd, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -256,6 +297,51 @@ over the transaction with a special "fee payer envelope" to commit to paying fee + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + fee_payer_key = "0x..." + + # Sender signs with awaiting_fee_payer flag + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + awaiting_fee_payer=True, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + sender_signed = tx.sign(account.key.hex()) + + # Fee payer co-signs the transaction // [!code hl] + fully_signed = sender_signed.sign(fee_payer_key, for_fee_payer=True) # [!code hl] + + tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -484,6 +570,52 @@ parameter. + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=600_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( # [!code hl] + Call.create( # [!code hl] + to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] + data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + Call.create( # [!code hl] + to="0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", # [!code hl] + data="0xcafebabe0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + Call.create( # [!code hl] + to="0xcafebabecafebabecafebabecafebabecafebabe", # [!code hl] + data="0xdeadbeef0000000000000000000000000000000001", # [!code hl] + ), # [!code hl] + ), # [!code hl] + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -684,6 +816,70 @@ transactions thereafter can be signed by the access key. + + + :::code-group + + ```python [example.py] + import time + + from eth_account import Account as EthAccount + from pytempo import ( + Call, KeyAuthorization, SignatureType, TempoTransaction, + sign_tx_access_key, + ) + from provider import w3, account + + access_key = EthAccount.create() + + # Sign key authorization with root account // [!code hl] + auth = KeyAuthorization( # [!code hl] + chain_id=w3.eth.chain_id, # [!code hl] + key_type=SignatureType.SECP256K1, # [!code hl] + key_id=access_key.address, # [!code hl] + expiry=int(time.time()) + 3600, # [!code hl] + limits=None, # [!code hl] + ) # [!code hl] + signed_auth = auth.sign(account.key.hex()) # [!code hl] + + # Attach key authorization to a transaction signed by the access key. // [!code hl] + # This registers the access key on-chain via the Account Keychain. // [!code hl] + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=600_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=201, + key_authorization=signed_auth.rlp_encode(), # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef", + ), + ), + ) + + signed_tx = sign_tx_access_key( # [!code hl] + tx, # [!code hl] + access_key_private_key=access_key.key.hex(), # [!code hl] + root_account=account.address, # [!code hl] + ) # [!code hl] + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -863,6 +1059,45 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + # Send three transactions concurrently using different nonce keys + for nonce_key, to, data in [ + (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), + (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + ]: + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=nonce_key, # [!code hl] + calls=(Call.create(to=to, data=data),), + ) + signed_tx = tx.sign(account.key.hex()) + w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -1018,6 +1253,51 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo + + + :::code-group + + ```python [example.py] + import time + + from pytempo import Call, TempoTransaction + from provider import w3, account + + # maxUint256: signals an expiring nonce + MAX_UINT256 = 2**256 - 1 + valid_before = int(time.time()) + 20 + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce_key=MAX_UINT256, # [!code focus] + valid_before=valid_before, # [!code focus] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef0000000000000000000000000000000001", + ), + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -1182,6 +1462,44 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** + + + :::code-group + + ```python [example.py] + from pytempo import Call, TempoTransaction + from provider import w3, account + + for nonce_key, to, data in [ + (1, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + (2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "0xcafebabe0000000000000000000000000000000001"), + (3, "0xcafebabecafebabecafebabecafebabecafebabe", "0xdeadbeef0000000000000000000000000000000001"), + ]: + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=0, + nonce_key=nonce_key, # [!code focus] + calls=(Call.create(to=to, data=data),), + ) + signed_tx = tx.sign(account.key.hex()) + w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash @@ -1335,6 +1653,54 @@ the transaction can be included in a block. + + + :::code-group + + ```python [example.py] + from datetime import datetime, timezone + + from pytempo import Call, TempoTransaction + from provider import w3, account + + # 2026-01-01 00:00:00 UTC + valid_after = int(datetime(2026, 1, 1, tzinfo=timezone.utc).timestamp()) + # 2026-01-02 00:00:00 UTC + valid_before = int(datetime(2026, 1, 2, tzinfo=timezone.utc).timestamp()) + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=300_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + valid_after=valid_after, # [!code hl] + valid_before=valid_before, # [!code hl] + calls=( + Call.create( + to="0xcafebabecafebabecafebabecafebabecafebabe", + data="0xdeadbeef0000000000000000000000000000000001", + ), + ), + ) + + # Sign now, submit to the network for later execution + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + ```bash From b54ba32fee8e11fc7811c24c6295e565afc4108a Mon Sep 17 00:00:00 2001 From: onbjerg Date: Sun, 15 Mar 2026 21:00:56 +0100 Subject: [PATCH 11/40] docs: add Go tabs to tempo tx examples (#13) --- src/snippets/go-provider.go | 13 + src/snippets/tempo-tx-properties.mdx | 546 +++++++++++++++++++++++++++ 2 files changed, 559 insertions(+) create mode 100644 src/snippets/go-provider.go diff --git a/src/snippets/go-provider.go b/src/snippets/go-provider.go new file mode 100644 index 00000000..95bb5496 --- /dev/null +++ b/src/snippets/go-provider.go @@ -0,0 +1,13 @@ +// [!region setup] +package main + +import ( + "os" + + "github.com/tempoxyz/tempo-go/pkg/client" +) + +func newClient() *client.Client { + return client.New(os.Getenv("TEMPO_RPC_URL")) +} +// [!endregion setup] diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index e221c115..0dee9dbf 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -143,6 +143,59 @@ user's preferred fee token and the validator's preferred token. + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetFeeToken(transaction.AlphaUSDAddress). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -342,6 +395,67 @@ over the transaction with a special "fee payer envelope" to commit to paying fee + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + senderSgn, _ := signer.NewSigner("0x...") + sponsorSgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) + + // Sender builds and signs a sponsored transaction + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetSponsored(true). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef"), + ). + Build() + + _ = transaction.SignTransaction(tx, senderSgn) + + // Fee payer co-signs the transaction // [!code hl] + tx.FeeToken = transaction.AlphaUSDAddress // [!code hl] + tx.AwaitingFeePayer = false // [!code hl] + _ = transaction.AddFeePayerSignature(tx, sponsorSgn) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -616,6 +730,68 @@ parameter. + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(600_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("cafebabe0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), // [!code hl] + ). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -880,6 +1056,115 @@ transactions thereafter can be signed by the access key. + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "encoding/hex" + "log" + "math/big" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/tempoxyz/tempo-go/pkg/keychain" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + rootSgn, _ := signer.NewSigner("0x...") + accessKeyPriv, _ := crypto.GenerateKey() + accessKey := signer.NewSignerFromKey(accessKeyPriv) + c := newClient() + ctx := context.Background() + + chainID := big.NewInt(transaction.ChainIdModerato) + gasPrice := big.NewInt(25_000_000_000) + keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) + + // Authorize the access key via Account Keychain precompile // [!code hl] + parsed, _ := abi.JSON(strings.NewReader(`[{ + "name": "authorizeKey", + "type": "function", + "inputs": [ + {"name": "keyId", "type": "address"}, + {"name": "sigType", "type": "uint8"}, + {"name": "expiry", "type": "uint64"}, + {"name": "enforceLimits", "type": "bool"}, + {"name": "limits", "type": "tuple[]", "components": [ + {"name": "token", "type": "address"}, + {"name": "amount", "type": "uint256"} + ]} + ] + }]`)) + type TokenLimit struct { + Token common.Address + Amount *big.Int + } + calldata, _ := parsed.Pack("authorizeKey", + accessKey.Address(), + uint8(0), + uint64(1893456000), + false, + []TokenLimit{}, + ) + + nonce, _ := c.GetTransactionCount(ctx, rootSgn.Address().Hex()) + authTx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: nonce, + GasTipCap: gasPrice, + GasFeeCap: gasPrice, + Gas: 600_000, + To: &keychainAddr, + Data: calldata, + }) + signedAuthTx, _ := types.SignTx( + authTx, types.NewLondonSigner(chainID), rootSgn.PrivateKey(), + ) + txBytes, _ := signedAuthTx.MarshalBinary() + authHash, _ := c.SendRawTransaction(ctx, "0x"+hex.EncodeToString(txBytes)) + log.Printf("Authorized access key: %s", authHash) + + // Sign a transaction with the access key // [!code hl] + tx := transaction.NewBuilder(chainID). // [!code hl] + SetNonce(0). // [!code hl] + SetNonceKey(big.NewInt(300)). // [!code hl] + SetGas(500_000). // [!code hl] + SetMaxFeePerGas(gasPrice). // [!code hl] + SetMaxPriorityFeePerGas(gasPrice). // [!code hl] + AddCall( // [!code hl] + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), // [!code hl] + big.NewInt(0), // [!code hl] + common.Hex2Bytes("deadbeef"), // [!code hl] + ). // [!code hl] + Build() // [!code hl] + + _ = keychain.SignWithAccessKey(tx, accessKey, rootSgn.Address()) // [!code hl] + + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -1098,6 +1383,78 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // Send three transactions concurrently using different nonce keys + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -1298,6 +1655,63 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + // maxUint256: signals an expiring nonce + maxUint256, _ := new(big.Int).SetString("ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 16) + + validBefore := uint64(time.Now().Unix()) + 20 + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetNonceKey(maxUint256). // [!code focus] + SetValidBefore(validBefore). // [!code focus] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -1500,6 +1914,77 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + type txParams struct { + nonceKey int64 + to string + data string + } + params := []txParams{ + {1, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + {2, "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", "cafebabe0000000000000000000000000000000001"}, + {3, "0xcafebabecafebabecafebabecafebabecafebabe", "deadbeef0000000000000000000000000000000001"}, + } + + var wg sync.WaitGroup + for _, p := range params { + wg.Add(1) + go func(p txParams) { + defer wg.Done() + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(0). + SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( + common.HexToAddress(p.to), + big.NewInt(0), + common.Hex2Bytes(p.data), + ). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Nonce key %d tx: %s", p.nonceKey, txHash) + }(p) + } + wg.Wait() + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash @@ -1701,6 +2186,67 @@ the transaction can be included in a block. + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + // 2026-01-01 00:00:00 UTC + validAfter := uint64(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC).Unix()) + // 2026-01-02 00:00:00 UTC + validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(300_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetValidAfter(validAfter). // [!code hl] + SetValidBefore(validBefore). // [!code hl] + AddCall( + common.HexToAddress("0xcafebabecafebabecafebabecafebabecafebabe"), + big.NewInt(0), + common.Hex2Bytes("deadbeef0000000000000000000000000000000001"), + ). + Build() + + // Sign now, submit to the network for later execution + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ```bash From 7f5107b0c0c7fca60ee16fa9d26288b00c157058 Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Sun, 15 Mar 2026 14:37:02 -0700 Subject: [PATCH 12/40] docs: add 'How sessions work' diagram to streamed payments page (#14) --- .../machine-payments/streamed-payments.mdx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/pages/guide/machine-payments/streamed-payments.mdx b/src/pages/guide/machine-payments/streamed-payments.mdx index 7bb141a8..98f9e5db 100644 --- a/src/pages/guide/machine-payments/streamed-payments.mdx +++ b/src/pages/guide/machine-payments/streamed-payments.mdx @@ -4,6 +4,7 @@ description: Per-token billing over Server-Sent Events on Tempo. Stream content --- import { Card, Cards } from 'vocs' +import { MermaidDiagram } from '../../../components/MermaidDiagram' # Accept streamed payments @@ -13,6 +14,38 @@ Build a payment-gated API that streams content word-by-word and charges $0.001 p Streamed payments extend [pay-as-you-go sessions](/guide/machine-payments/pay-as-you-go) with SSE. The server charges per token as content streams — if the channel balance runs out mid-stream, the client automatically sends a new voucher and the stream resumes. ::: +## How sessions work + +>Tempo: (1) Deposit tokens + Tempo-->>Client: Channel created + Client->>Server: (2) Open credential + Note over Server: Verify on-chain deposit + Server-->>Client: 200 OK (SSE stream begins) + loop Per token streamed + Server-->>Client: (3) SSE data event + charge + Note over Server: ecrecover only + end + alt Channel balance low + Server-->>Client: (4) payment-need-voucher event + Client->>Server: New voucher + Note over Server: Resume streaming + end + Note over Server: (5) Periodic settlement + Server->>Tempo: settle(channelId, voucher) + Client->>Server: (6) Close + Server->>Tempo: close(channelId, voucher) + Tempo-->>Client: Refund remaining deposit +`} /> + +1. **Open** — Client deposits funds into an on-chain escrow contract, creating a payment channel +2. **Stream** — Server streams SSE events, calling `stream.charge()` per token to increment the voucher amount +3. **Top up** — If the channel runs low mid-stream, the server emits a `payment-need-voucher` event and the client automatically signs a new voucher +4. **Close** — Either party closes the channel, settling the final balance on-chain and refunding unused deposit + ## Server setup ::::steps From 11d245764d1c2652746a2b2b2d51a6286ec73076 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Mon, 16 Mar 2026 11:23:57 +0100 Subject: [PATCH 13/40] docs: add more sdks to "Accept a payment" (#16) --- src/pages/guide/payments/accept-a-payment.mdx | 648 ++++++++++++++---- 1 file changed, 507 insertions(+), 141 deletions(-) diff --git a/src/pages/guide/payments/accept-a-payment.mdx b/src/pages/guide/payments/accept-a-payment.mdx index cdd24714..dd363330 100644 --- a/src/pages/guide/payments/accept-a-payment.mdx +++ b/src/pages/guide/payments/accept-a-payment.mdx @@ -5,6 +5,7 @@ description: Accept stablecoin payments in your application. Verify transactions import * as Demo from '../../../components/guides/Demo.tsx' import * as Step from '../../../components/guides/steps' import * as Token from '../../../components/guides/tokens' +import { Tabs, Tab } from 'vocs' # Accept a Payment @@ -27,170 +28,535 @@ Check if a payment has been received by querying the token balance or listening ### Check Balance -:::code-group + + -```ts [TypeScript] -import { client } from './viem.config' + :::code-group -const balance = await client.token.getBalance({ - token: '0x20c0000000000000000000000000000000000001', // AlphaUSD - address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', -}) + ```ts [example.ts] + import { client } from './viem.config' -console.log('Balance:', balance) -``` + const balance = await client.token.getBalance({ + token: '0x20c0000000000000000000000000000000000001', // AlphaUSD + address: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', + }) -```rust [Rust] -use alloy::{primitives::address, providers::ProviderBuilder}; -use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; + console.log('Balance:', balance) + ``` -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; + ```ts [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` - let balance = ITIP20::new( // [!code focus] - address!("0x20c0000000000000000000000000000000000001"), // Alpha USD // [!code focus] - &provider, // [!code focus] - ) // [!code focus] - .balanceOf(address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb")) // [!code focus] - .call() // [!code focus] - .await?; // [!code focus] + ::: - println!("Balance: {balance:?}"); // [!code focus] + - Ok(()) -} -``` + + + :::code-group + + ```rust [example.rs] + use alloy::{primitives::address, providers::ProviderBuilder}; + use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = ProviderBuilder::new_with_network::() + .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) + .await?; + + let balance = ITIP20::new( // [!code focus] + address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code focus] + &provider, // [!code focus] + ) // [!code focus] + .balanceOf(address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb")) // [!code focus] + .call() // [!code focus] + .await?; // [!code focus] + + println!("Balance: {balance:?}"); // [!code focus] + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from web3 import Web3 + from eth_abi import encode + from provider import w3 + + token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD + account_address = "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb" + + # balanceOf(address) selector: 0x70a08231 + calldata = "0x70a08231" + encode(["address"], [account_address]).hex() # [!code hl] + result = w3.eth.call({"to": token_address, "data": calldata}) # [!code hl] + balance = int.from_bytes(result, "big") # [!code hl] + + print(f"Balance: {balance}") + ``` + + ```python [provider.py] + from web3 import Web3 + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "fmt" + "log" + "math/big" + "strings" + ) + + func main() { + c := newClient() + ctx := context.Background() + + token := "0x20c0000000000000000000000000000000000001" + account := "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb" + + // balanceOf(address) — ABI-encoded eth_call // [!code hl] + calldata := "0x70a08231" + fmt.Sprintf("%064s", strings.TrimPrefix(account, "0x")) // [!code hl] -::: + resp, err := c.SendRequest(ctx, "eth_call", map[string]interface{}{ // [!code hl] + "to": token, // [!code hl] + "data": calldata, // [!code hl] + }, "latest") // [!code hl] + if err != nil { + log.Fatal(err) + } + + balance := new(big.Int) // [!code hl] + balance.SetString(strings.TrimPrefix(resp.Result.(string), "0x"), 16) // [!code hl] + fmt.Printf("Balance: %s\n", balance) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast call 0x20c0000000000000000000000000000000000001 \ + "balanceOf(address)(uint256)" \ + 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ + --rpc-url $TEMPO_RPC_URL + ``` + + + ### Listen for Transfer Events -:::code-group - -```ts [TypeScript] -import { watchEvent } from 'viem' - -// Watch for incoming transfers -const unwatch = watchEvent(client, { - address: '0x20c0000000000000000000000000000000000001', - event: { - type: 'event', - name: 'Transfer', - inputs: [ - { name: 'from', type: 'address', indexed: true }, - { name: 'to', type: 'address', indexed: true }, - { name: 'value', type: 'uint256' }, - ], - }, - onLogs: (logs) => { - logs.forEach((log) => { - if (log.args.to === yourAddress) { - console.log('Received payment:', { - from: log.args.from, - amount: log.args.value, - }) - } + + + + :::code-group + + ```ts [example.ts] + import { client } from './viem.config' + + // Watch for incoming transfers + const unwatch = client.watchEvent({ + address: '0x20c0000000000000000000000000000000000001', + event: { + type: 'event', + name: 'Transfer', + inputs: [ + { name: 'from', type: 'address', indexed: true }, + { name: 'to', type: 'address', indexed: true }, + { name: 'value', type: 'uint256' }, + ], + }, + onLogs: (logs) => { // [!code focus] + logs.forEach((log) => { // [!code focus] + console.log('Received payment:', { // [!code focus] + from: log.args.from, // [!code focus] + amount: log.args.value, // [!code focus] + }) // [!code focus] + }) // [!code focus] + }, // [!code focus] }) - }, -}) -``` + ``` -```rust [Rust] -use alloy::{primitives::address, providers::ProviderBuilder}; -use futures::StreamExt; -use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; - - // Watch for incoming transfers // [!code focus] - let mut transfers = ITIP20::new( // [!code focus] - address!("0x20c0000000000000000000000000000000000001"), // [!code focus] - &provider, // [!code focus] - ) // [!code focus] - .Transfer_filter() // [!code focus] - .watch() // [!code focus] - .await? // [!code focus] - .into_stream(); // [!code focus] - - while let Some(Ok((payment, _))) = transfers.next().await { // [!code focus] - println!("Received payment: {payment:?}") // [!code focus] - } // [!code focus] - - Ok(()) -} -``` + ```ts [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::{primitives::address, providers::ProviderBuilder}; + use futures::StreamExt; + use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = ProviderBuilder::new_with_network::() + .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) + .await?; + + // Watch for incoming transfers // [!code focus] + let mut transfers = ITIP20::new( // [!code focus] + address!("0x20c0000000000000000000000000000000000001"), // [!code focus] + &provider, // [!code focus] + ) // [!code focus] + .Transfer_filter() // [!code focus] + .watch() // [!code focus] + .await? // [!code focus] + .into_stream(); // [!code focus] + + while let Some(Ok((payment, _))) = transfers.next().await { // [!code focus] + println!("Received payment: {payment:?}") // [!code focus] + } // [!code focus] + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + import json + from web3 import Web3 + from provider import w3 + + token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD + + transfer_event_abi = { # [!code focus] + "anonymous": False, # [!code focus] + "name": "Transfer", # [!code focus] + "type": "event", # [!code focus] + "inputs": [ # [!code focus] + {"indexed": True, "name": "from", "type": "address"}, # [!code focus] + {"indexed": True, "name": "to", "type": "address"}, # [!code focus] + {"indexed": False, "name": "value", "type": "uint256"}, # [!code focus] + ], # [!code focus] + } # [!code focus] + + contract = w3.eth.contract( # [!code focus] + address=Web3.to_checksum_address(token_address), # [!code focus] + abi=[transfer_event_abi], # [!code focus] + ) # [!code focus] + + # Get historical Transfer events # [!code focus] + events = contract.events.Transfer().get_logs(from_block="latest") # [!code focus] + for event in events: # [!code focus] + print(f"Transfer: {event.args['from']} -> {event.args['to']}: {event.args['value']}") # [!code focus] + ``` -::: + ```python [provider.py] + from web3 import Web3 + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "fmt" + "log" + + "github.com/ethereum/go-ethereum/crypto" + ) + + func main() { + c := newClient() + ctx := context.Background() + + token := "0x20c0000000000000000000000000000000000001" + + // Transfer(address,address,uint256) event topic // [!code focus] + transferTopic := crypto.Keccak256Hash([]byte("Transfer(address,address,uint256)")) // [!code focus] + + resp, err := c.SendRequest(ctx, "eth_getLogs", map[string]interface{}{ // [!code focus] + "fromBlock": "0x0", // [!code focus] + "toBlock": "latest", // [!code focus] + "address": token, // [!code focus] + "topics": []interface{}{transferTopic.Hex()}, // [!code focus] + }) // [!code focus] + if err != nil { + log.Fatal(err) + } + + logs, _ := resp.Result.([]interface{}) // [!code focus] + for _, entry := range logs { // [!code focus] + l := entry.(map[string]interface{}) // [!code focus] + topics := l["topics"].([]interface{}) // [!code focus] + fmt.Printf("Transfer: %s -> %s (data: %s)\n", topics[1], topics[2], l["data"]) // [!code focus] + } // [!code focus] + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast logs \ + --address 0x20c0000000000000000000000000000000000001 \ + "Transfer(address indexed, address indexed, uint256)" \ + --rpc-url $TEMPO_RPC_URL + ``` + + + ## Payment Reconciliation with Memos If payments include memos (invoice IDs, order numbers, etc.), you can reconcile them automatically: -:::code-group - -```ts [TypeScript] -// Watch for TransferWithMemo events -const unwatch = watchEvent(client, { - address: tokenAddress, - event: { - type: 'event', - name: 'TransferWithMemo', - inputs: [ - { name: 'from', type: 'address', indexed: true }, - { name: 'to', type: 'address', indexed: true }, - { name: 'value', type: 'uint256' }, - { name: 'memo', type: 'bytes32', indexed: true }, - ], - }, - onLogs: (logs) => { - logs.forEach((log) => { - if (log.args.to === yourAddress) { - const invoiceId = log.args.memo - // Mark invoice as paid in your database - markInvoiceAsPaid(invoiceId, log.args.value) - } + + + + :::code-group + + ```ts [example.ts] + import { client } from './viem.config' + + // Watch for TransferWithMemo events + const unwatch = client.watchEvent({ + address: '0x20c0000000000000000000000000000000000001', + event: { + type: 'event', + name: 'TransferWithMemo', + inputs: [ + { name: 'from', type: 'address', indexed: true }, + { name: 'to', type: 'address', indexed: true }, + { name: 'value', type: 'uint256' }, + { name: 'memo', type: 'bytes32', indexed: true }, + ], + }, + onLogs: (logs) => { // [!code focus] + logs.forEach((log) => { // [!code focus] + const invoiceId = log.args.memo // [!code focus] + // Mark invoice as paid in your database // [!code focus] + markInvoiceAsPaid(invoiceId, log.args.value) // [!code focus] + }) // [!code focus] + }, // [!code focus] }) - }, -}) -``` + ``` -```rust [Rust] -use alloy::{primitives::address, providers::ProviderBuilder}; -use futures::StreamExt; -use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; - - let mut transfers = ITIP20::new( // [!code focus] - address!("0x20c0000000000000000000000000000000000001"), // [!code focus] - &provider, // [!code focus] - ) // [!code focus] - .TransferWithMemo_filter() // [!code focus] - .watch() // [!code focus] - .await? // [!code focus] - .into_stream(); // [!code focus] - - while let Some(Ok((transfer, _))) = transfers.next().await { // [!code focus] - let invoice_id = transfer.memo; // [!code focus] - println!("Transfer received with memo: {invoice_id:?}"); // [!code focus] - } // [!code focus] - - Ok(()) -} -``` + ```ts [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::{primitives::address, providers::ProviderBuilder}; + use futures::StreamExt; + use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = ProviderBuilder::new_with_network::() + .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) + .await?; + + let mut transfers = ITIP20::new( // [!code focus] + address!("0x20c0000000000000000000000000000000000001"), // [!code focus] + &provider, // [!code focus] + ) // [!code focus] + .TransferWithMemo_filter() // [!code focus] + .watch() // [!code focus] + .await? // [!code focus] + .into_stream(); // [!code focus] + + while let Some(Ok((transfer, _))) = transfers.next().await { // [!code focus] + let invoice_id = transfer.memo; // [!code focus] + println!("Transfer received with memo: {invoice_id:?}"); // [!code focus] + } // [!code focus] + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from web3 import Web3 + from provider import w3 + + token_address = "0x20c0000000000000000000000000000000000001" # AlphaUSD + + transfer_memo_abi = { # [!code focus] + "anonymous": False, # [!code focus] + "name": "TransferWithMemo", # [!code focus] + "type": "event", # [!code focus] + "inputs": [ # [!code focus] + {"indexed": True, "name": "from", "type": "address"}, # [!code focus] + {"indexed": True, "name": "to", "type": "address"}, # [!code focus] + {"indexed": False, "name": "value", "type": "uint256"}, # [!code focus] + {"indexed": True, "name": "memo", "type": "bytes32"}, # [!code focus] + ], # [!code focus] + } # [!code focus] + + contract = w3.eth.contract( # [!code focus] + address=Web3.to_checksum_address(token_address), # [!code focus] + abi=[transfer_memo_abi], # [!code focus] + ) # [!code focus] + + events = contract.events.TransferWithMemo().get_logs(from_block="latest") # [!code focus] + for event in events: # [!code focus] + invoice_id = event.args["memo"] # [!code focus] + print(f"Transfer with memo {invoice_id.hex()}: {event.args['value']}") # [!code focus] + ``` + + ```python [provider.py] + from web3 import Web3 + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "fmt" + "log" + + "github.com/ethereum/go-ethereum/crypto" + ) + + func main() { + c := newClient() + ctx := context.Background() + + token := "0x20c0000000000000000000000000000000000001" + + // TransferWithMemo(address,address,uint256,bytes32) event topic // [!code focus] + memoTopic := crypto.Keccak256Hash( // [!code focus] + []byte("TransferWithMemo(address,address,uint256,bytes32)"), // [!code focus] + ) // [!code focus] + + resp, err := c.SendRequest(ctx, "eth_getLogs", map[string]interface{}{ // [!code focus] + "fromBlock": "0x0", // [!code focus] + "toBlock": "latest", // [!code focus] + "address": token, // [!code focus] + "topics": []interface{}{memoTopic.Hex()}, // [!code focus] + }) // [!code focus] + if err != nil { + log.Fatal(err) + } + + logs, _ := resp.Result.([]interface{}) // [!code focus] + for _, entry := range logs { // [!code focus] + l := entry.(map[string]interface{}) // [!code focus] + topics := l["topics"].([]interface{}) // [!code focus] + fmt.Printf("Transfer: %s -> %s (memo: %s, data: %s)\n", // [!code focus] + topics[1], topics[2], topics[3], l["data"]) // [!code focus] + } // [!code focus] + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast logs \ + --address 0x20c0000000000000000000000000000000000001 \ + "TransferWithMemo(address indexed, address indexed, uint256, bytes32 indexed)" \ + --rpc-url $TEMPO_RPC_URL + ``` -::: + + ## Smart Contract Integration From 3ed86edb750811bd5f5bded0e17e744b9c077460 Mon Sep 17 00:00:00 2001 From: onbjerg Date: Mon, 16 Mar 2026 11:24:05 +0100 Subject: [PATCH 14/40] docs: add more sdks to "Send a payment" (#15) --- src/pages/guide/payments/send-a-payment.mdx | 993 +++++++++++++++----- 1 file changed, 781 insertions(+), 212 deletions(-) diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/guide/payments/send-a-payment.mdx index 7eaaa891..3beab108 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/guide/payments/send-a-payment.mdx @@ -5,6 +5,7 @@ description: Send stablecoin payments between accounts on Tempo. Include optiona import * as Demo from '../../../components/guides/Demo.tsx' import * as Step from '../../../components/guides/steps' import { Cards, Card } from 'vocs' +import { Tabs, Tab } from 'vocs' # Send a Payment @@ -183,248 +184,816 @@ Now that you have made a payment you can Send a payment using the standard `transfer` function: -:::code-group + + + + :::code-group + + ```ts twoslash [example.ts] + // @noErrors + import { parseUnits } from 'viem' + import { client } from './viem.config' + + const { receipt } = await client.token.transferSync({ + amount: parseUnits('100', 6), // 100 tokens (6 decimals) // [!code hl] + to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', // [!code hl] + token: '0x20c0000000000000000000000000000000000001', // AlphaUSD // [!code hl] + }) + ``` + + ```ts twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + ```tsx twoslash + // @noErrors + import { Hooks } from 'wagmi/tempo' + import { parseUnits } from 'viem' + + function SendPayment() { + const { mutate, isPending } = Hooks.token.useTransferSync() // [!code hl] + + return ( + + ) + } + ``` -```ts [example.ts] -import { parseUnits } from 'viem' -import { client } from './viem.config' + -const { receipt } = await client.token.transferSync({ - amount: parseUnits('100', 6), // 100 tokens (6 decimals) - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', // AlphaUSD -}) -``` + -```ts [viem.config.ts] filename="viem.config.ts" -// [!include ~/snippets/viem.config.ts:setup] -``` + :::code-group -```rs [Rust] -use alloy::{ - primitives::{address, U256}, - providers::ProviderBuilder, -}; -use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; - - let token = ITIP20::new( // [!code focus] - address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code focus] - &provider, // [!code focus] - ); // [!code focus] - - let receipt = token // [!code focus] - .transfer( // [!code focus] - address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), // [!code focus] - U256::from(100_000_000), // 100 tokens (6 decimals) // [!code focus] - ) // [!code focus] - .send() // [!code focus] - .await? // [!code focus] - .get_receipt() // [!code focus] - .await?; // [!code focus] - - println!("Transfer successful: {:?}", receipt.transaction_hash); // [!code focus] - - Ok(()) -} -``` + ```rust [example.rs] + use alloy::{ + primitives::{address, U256}, + providers::ProviderBuilder, + }; + use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; -::: + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let token = ITIP20::new( // [!code hl] + address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code hl] + &provider, // [!code hl] + ); // [!code hl] + + let receipt = token // [!code hl] + .transfer( // [!code hl] + address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), // [!code hl] + U256::from(100_000_000), // 100 tokens (6 decimals) // [!code hl] + ) // [!code hl] + .send() // [!code hl] + .await? // [!code hl] + .get_receipt() // [!code hl] + .await?; // [!code hl] + + println!("Transfer successful: {:?}", receipt.transaction_hash); + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from eth_abi import encode + from pytempo import Call, TempoTransaction + from provider import w3, account + + TRANSFER_SELECTOR = bytes.fromhex("a9059cbb") + ALPHA_USD = "0x20c0000000000000000000000000000000000001" + + data = TRANSFER_SELECTOR + encode( # [!code hl] + ["address", "uint256"], # [!code hl] + ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000], # [!code hl] + ) # [!code hl] + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + Call.create(to=ALPHA_USD, data="0x" + data.hex()), # [!code hl] + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func buildTransferData(to common.Address, amount *big.Int) []byte { + data := make([]byte, 68) // 4 (selector) + 32 (address) + 32 (uint256) + data[0], data[1], data[2], data[3] = 0xa9, 0x05, 0x9c, 0xbb + copy(data[16:36], to.Bytes()) + amount.FillBytes(data[36:68]) + return data + } + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") + alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(100_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( // [!code hl] + alphaUSD, // [!code hl] + big.NewInt(0), // [!code hl] + buildTransferData(recipient, big.NewInt(100_000_000)), // [!code hl] + ). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast erc20 transfer \ + 0x20c0000000000000000000000000000000000001 \ + 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ + 100000000 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY # [!code hl] + ``` + + + + + + ```solidity + import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; + + contract PaymentSender { + ITIP20 public token; + + function sendPayment(address recipient, uint256 amount) external { + token.transfer(recipient, amount); // [!code hl] + } + } + ``` + + + ### Transfer with memo -Include a memo for payment reconciliation and tracking: +Include a memo for payment reconciliation and tracking. The memo is a 32-byte value that can store payment references, invoice IDs, order numbers, or any other metadata. + + + + + :::code-group + + ```ts twoslash [example.ts] + // @noErrors + import { parseUnits, stringToHex, pad } from 'viem' + import { client } from './viem.config' + + const invoiceId = pad(stringToHex('INV-12345'), { size: 32 }) // [!code hl] + + const { receipt } = await client.token.transferSync({ + amount: parseUnits('100', 6), + to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', + token: '0x20c0000000000000000000000000000000000001', + memo: invoiceId, // [!code hl] + }) + ``` + + ```ts twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + ```tsx twoslash + // @noErrors + import { Hooks } from 'wagmi/tempo' + import { parseUnits, stringToHex, pad } from 'viem' + + function SendPaymentWithMemo() { + const { mutate, isPending } = Hooks.token.useTransferSync() + + return ( + + ) + } + ``` -:::code-group + -```ts [example.ts] -import { parseUnits } from 'viem' -import { client } from './viem.config' + -const invoiceId = pad(stringToHex('INV-12345'), { size: 32 }) + :::code-group -const { receipt } = await client.token.transferSync({ - amount: parseUnits('100', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000001', - memo: invoiceId, -}) -``` -```ts [viem.config.ts] filename="viem.config.ts" -// [!include ~/snippets/viem.config.ts:setup] -``` + ```rust [example.rs] + use alloy::{ + primitives::{address, B256, U256}, + providers::ProviderBuilder, + }; + use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; -```rs [Rust] -use alloy::{ - primitives::{address, B256, U256}, - providers::ProviderBuilder, -}; -use tempo_alloy::{TempoNetwork, contracts::precompiles::ITIP20}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; - - let token = ITIP20::new( // [!code focus] - address!("0x20c0000000000000000000000000000000000001"), // AlphaUSD // [!code focus] - &provider, // [!code focus] - ); // [!code focus] - - let receipt = token // [!code focus] - .transferWithMemo( // [!code focus] - address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), // [!code focus] - U256::from(100_000_000), // 100 tokens (6 decimals) // [!code focus] - B256::left_padding_from("INV-12345".as_bytes()), // [!code focus] - ) // [!code focus] - .send() // [!code focus] - .await? // [!code focus] - .get_receipt() // [!code focus] - .await?; // [!code focus] - - println!("Transfer successful: {:?}", receipt.transaction_hash); // [!code focus] - - Ok(()) -} -``` + mod provider; -::: + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; -The memo is a 32-byte value that can store payment references, invoice IDs, order numbers, or any other metadata. + let token = ITIP20::new( + address!("0x20c0000000000000000000000000000000000001"), + &provider, + ); -### Using Solidity + let receipt = token + .transferWithMemo( // [!code hl] + address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), + U256::from(100_000_000), + B256::left_padding_from("INV-12345".as_bytes()), // [!code hl] + ) + .send() + .await? + .get_receipt() + .await?; -If you're building a smart contract that sends payments: + println!("Transfer successful: {:?}", receipt.transaction_hash); -```solidity -interface ITIP20 { - function transfer(address to, uint256 amount) external returns (bool); - function transferWithMemo(address to, uint256 amount, bytes32 memo) external; -} + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group -contract PaymentSender { - ITIP20 public token; - - function sendPayment(address recipient, uint256 amount) external { - token.transfer(recipient, amount); + ```python [example.py] + from eth_abi import encode + from pytempo import Call, TempoTransaction + from provider import w3, account + + TRANSFER_WITH_MEMO_SELECTOR = bytes.fromhex("76a8ee59") + ALPHA_USD = "0x20c0000000000000000000000000000000000001" + + memo = b"INV-12345" + b"\x00" * 23 # right-pad to 32 bytes # [!code hl] + + data = TRANSFER_WITH_MEMO_SELECTOR + encode( # [!code hl] + ["address", "uint256", "bytes32"], # [!code hl] + ["0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000, memo], # [!code hl] + ) # [!code hl] + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + Call.create(to=ALPHA_USD, data="0x" + data.hex()), + ), + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func buildTransferWithMemoData(to common.Address, amount *big.Int, memo [32]byte) []byte { + data := make([]byte, 100) // 4 + 32 + 32 + 32 + data[0], data[1], data[2], data[3] = 0x76, 0xa8, 0xee, 0x59 + copy(data[16:36], to.Bytes()) + amount.FillBytes(data[36:68]) + copy(data[68:100], memo[:]) + return data } - - function sendPaymentWithMemo( - address recipient, - uint256 amount, - bytes32 invoiceId - ) external { - token.transferWithMemo(recipient, amount, invoiceId); + + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") + alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") + + var memo [32]byte // [!code hl] + copy(memo[:], []byte("INV-12345")) // [!code hl] + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(100_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall( // [!code hl] + alphaUSD, // [!code hl] + big.NewInt(0), // [!code hl] + buildTransferWithMemoData(recipient, big.NewInt(100_000_000), memo), // [!code hl] + ). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) } -} -``` + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast send \ + 0x20c0000000000000000000000000000000000001 \ + "transferWithMemo(address,uint256,bytes32)" \ + 0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb \ + 100000000 \ + $(cast --format-bytes32-string "INV-12345") \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY # [!code hl] + ``` + + + + + + ```solidity + import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; + + contract PaymentSender { + ITIP20 public token; + + function sendPaymentWithMemo( + address recipient, + uint256 amount, + bytes32 invoiceId + ) external { + token.transferWithMemo(recipient, amount, invoiceId); // [!code hl] + } + } + ``` + + + ### Batch payment transactions Send multiple payments in a single transaction using batch transactions: -:::code-group + + + + :::code-group + + ```ts twoslash [example.ts] + // @noErrors + import { encodeFunctionData, parseUnits } from 'viem' + import { Abis } from 'viem/tempo' + import { client } from './viem.config' + + const tokenABI = Abis.tip20 + const recipient1 = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb' + const recipient2 = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' + + const calls = [ // [!code hl] + { // [!code hl] + to: '0x20c0000000000000000000000000000000000001', // [!code hl] + data: encodeFunctionData({ // [!code hl] + abi: tokenABI, // [!code hl] + functionName: 'transfer', // [!code hl] + args: [recipient1, parseUnits('100', 6)], // [!code hl] + }), // [!code hl] + }, // [!code hl] + { // [!code hl] + to: '0x20c0000000000000000000000000000000000001', // [!code hl] + data: encodeFunctionData({ // [!code hl] + abi: tokenABI, // [!code hl] + functionName: 'transfer', // [!code hl] + args: [recipient2, parseUnits('50', 6)], // [!code hl] + }), // [!code hl] + }, // [!code hl] + ] // [!code hl] + + const hash = await client.sendTransaction({ calls }) + ``` + + ```ts twoslash [viem.config.ts] + // [!include ~/snippets/viem.config.ts:setup] + ``` + + ::: + + + + + + ```tsx twoslash + // @noErrors + import { useSendTransactionSync } from 'wagmi' + import { encodeFunctionData, parseUnits } from 'viem' + import { Abis } from 'viem/tempo' + + function BatchPayment() { + const { sendTransactionSync, isPending } = useSendTransactionSync() + + const tokenABI = Abis.tip20 + const token = '0x20c0000000000000000000000000000000000001' + + return ( + + ) + } + ``` + + + + + + :::code-group + + ```rust [example.rs] + use alloy::{ + primitives::{address, Address, U256}, + providers::{Provider, ProviderBuilder}, + sol_types::SolCall, + }; + use tempo_alloy::{ + TempoNetwork, contracts::precompiles::ITIP20, primitives::transaction::Call, + rpc::TempoTransactionRequest, + }; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let recipient1 = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"); + let recipient2 = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); + let token_address: Address = address!("0x20c0000000000000000000000000000000000001"); + + let calls = vec![ // [!code hl] + Call { // [!code hl] + to: token_address.into(), // [!code hl] + input: ITIP20::transferCall { // [!code hl] + to: recipient1, // [!code hl] + amount: U256::from(100_000_000), // [!code hl] + } // [!code hl] + .abi_encode() // [!code hl] + .into(), // [!code hl] + value: U256::ZERO, // [!code hl] + }, // [!code hl] + Call { // [!code hl] + to: token_address.into(), // [!code hl] + input: ITIP20::transferCall { // [!code hl] + to: recipient2, // [!code hl] + amount: U256::from(50_000_000), // [!code hl] + } // [!code hl] + .abi_encode() // [!code hl] + .into(), // [!code hl] + value: U256::ZERO, // [!code hl] + }, // [!code hl] + ]; // [!code hl] + + let pending = provider + .send_transaction(TempoTransactionRequest { + calls, + ..Default::default() + }) + .await?; + let tx_hash = pending.tx_hash(); + + println!("Batch transaction sent: {tx_hash:?}"); + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from eth_abi import encode + from pytempo import Call, TempoTransaction + from provider import w3, account + + TRANSFER_SELECTOR = bytes.fromhex("a9059cbb") + ALPHA_USD = "0x20c0000000000000000000000000000000000001" + + def build_transfer(to: str, amount: int) -> str: + data = TRANSFER_SELECTOR + encode( + ["address", "uint256"], [to, amount] + ) + return "0x" + data.hex() + + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=200_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( # [!code hl] + Call.create( # [!code hl] + to=ALPHA_USD, # [!code hl] + data=build_transfer("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", 100_000_000), # [!code hl] + ), # [!code hl] + Call.create( # [!code hl] + to=ALPHA_USD, # [!code hl] + data=build_transfer("0x70997970C51812dc3A010C7d01b50e0d17dc79C8", 50_000_000), # [!code hl] + ), # [!code hl] + ), # [!code hl] + ) + + signed_tx = tx.sign(account.key.hex()) + tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func buildTransferData(to common.Address, amount *big.Int) []byte { + data := make([]byte, 68) + data[0], data[1], data[2], data[3] = 0xa9, 0x05, 0x9c, 0xbb + copy(data[16:36], to.Bytes()) + amount.FillBytes(data[36:68]) + return data + } -```ts [example.ts] -import { encodeFunctionData, parseUnits } from 'viem' -import { Abis } from 'viem/tempo' -import { client } from './viem.config' - -const tokenABI = Abis.tip20 -const recipient1 = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb' -const recipient2 = '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' - -const calls = [ - { - to: '0x20c0000000000000000000000000000000000001', // AlphaUSD address - data: encodeFunctionData({ - abi: tokenABI, - functionName: 'transfer', - args: [recipient1, parseUnits('100', 6)], - }), - }, - { - to: '0x20c0000000000000000000000000000000000001', - data: encodeFunctionData({ - abi: tokenABI, - functionName: 'transfer', - args: [recipient2, parseUnits('50', 6)], - }), - }, -] - -const hash = await client.sendTransaction({ calls }) -``` + func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() -```ts [viem.config.ts] filename="viem.config.ts" -// [!include ~/snippets/viem.config.ts:setup] -``` + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) -```rust [Rust] -use alloy::{ - primitives::{address, Address, U256}, - providers::{Provider, ProviderBuilder}, - sol_types::SolCall, -}; -use tempo_alloy::{ - TempoNetwork, contracts::precompiles::ITIP20, primitives::transaction::Call, - rpc::TempoTransactionRequest, -}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let provider = ProviderBuilder::new_with_network::() - .connect(&std::env::var("RPC_URL").expect("No RPC URL set")) - .await?; - - let recipient1 = address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"); // [!code focus] - let recipient2 = address!("0x70997970C51812dc3A010C7d01b50e0d17dc79C8"); // [!code focus] - let token_address: Address = address!("0x20c0000000000000000000000000000000000001"); // [!code focus] - - let calls = vec![ // [!code focus] - Call { // [!code focus] - to: token_address.into(), // [!code focus] - input: ITIP20::transferCall { // [!code focus] - to: recipient1, // [!code focus] - amount: U256::from(100_000_000), // [!code focus] - } // [!code focus] - .abi_encode() // [!code focus] - .into(), // [!code focus] - value: U256::ZERO, // [!code focus] - }, // [!code focus] - Call { // [!code focus] - to: token_address.into(), // [!code focus] - input: ITIP20::transferCall { // [!code focus] - to: recipient2, // [!code focus] - amount: U256::from(50_000_000), // [!code focus] - } // [!code focus] - .abi_encode() // [!code focus] - .into(), // [!code focus] - value: U256::ZERO, // [!code focus] - }, // [!code focus] - ]; // [!code focus] - - let pending = provider // [!code focus] - .send_transaction(TempoTransactionRequest { // [!code focus] - calls, // [!code focus] - ..Default::default() // [!code focus] - }) // [!code focus] - .await?; // [!code focus] - let tx_hash = pending.tx_hash(); // [!code focus] - - println!("Batch transaction sent: {tx_hash:?}"); // [!code focus] - - Ok(()) -} -``` + recipient1 := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") + recipient2 := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") + alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") -::: + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(200_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient1, big.NewInt(100_000_000))). // [!code hl] + AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient2, big.NewInt(50_000_000))). // [!code hl] + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Batch transaction sent: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + + + + ```bash + $ cast batch-send \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --call "0x20c0000000000000000000000000000000000001::transfer(address,uint256):0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb,100000000" \ + --call "0x20c0000000000000000000000000000000000001::transfer(address,uint256):0x70997970C51812dc3A010C7d01b50e0d17dc79C8,50000000" # [!code hl] + ``` + + + + + + ```solidity + import {ITIP20} from "tempo-std/interfaces/ITIP20.sol"; + + contract BatchPaymentSender { + ITIP20 public token; + + struct Payment { + address recipient; + uint256 amount; + } + + function batchPay(Payment[] calldata payments) external { + for (uint256 i = 0; i < payments.length; i++) { + token.transfer(payments[i].recipient, payments[i].amount); // [!code hl] + } + } + } + ``` + + + ### Payment events From 51fe216a51e2fe44329c362485349087d13f6d6d Mon Sep 17 00:00:00 2001 From: onbjerg Date: Mon, 16 Mar 2026 13:08:09 +0100 Subject: [PATCH 15/40] chore: update new code examples to mainnet (#19) --- src/pages/guide/payments/accept-a-payment.mdx | 6 ++-- src/pages/guide/payments/send-a-payment.mdx | 12 +++---- src/pages/sdk/foundry/index.mdx | 2 +- src/pages/sdk/go/index.mdx | 22 ++++++------- src/pages/sdk/python/index.mdx | 18 +++++------ src/snippets/tempo-tx-properties.mdx | 32 +++++++++---------- 6 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/pages/guide/payments/accept-a-payment.mdx b/src/pages/guide/payments/accept-a-payment.mdx index dd363330..eb5e974b 100644 --- a/src/pages/guide/payments/accept-a-payment.mdx +++ b/src/pages/guide/payments/accept-a-payment.mdx @@ -111,7 +111,7 @@ Check if a payment has been received by querying the token balance or listening ```python [provider.py] from web3 import Web3 - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: @@ -295,7 +295,7 @@ Check if a payment has been received by querying the token balance or listening ```python [provider.py] from web3 import Web3 - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: @@ -485,7 +485,7 @@ If payments include memos (invoice IDs, order numbers, etc.), you can reconcile ```python [provider.py] from web3 import Web3 - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) ``` ::: diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/guide/payments/send-a-payment.mdx index 3beab108..b6af31ec 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/guide/payments/send-a-payment.mdx @@ -320,7 +320,7 @@ Send a payment using the standard `transfer` function: from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -363,7 +363,7 @@ Send a payment using the standard `transfer` function: recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). @@ -568,7 +568,7 @@ Include a memo for payment reconciliation and tracking. The memo is a 32-byte va from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -615,7 +615,7 @@ Include a memo for payment reconciliation and tracking. The memo is a 32-byte va var memo [32]byte // [!code hl] copy(memo[:], []byte("INV-12345")) // [!code hl] - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(100_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). @@ -890,7 +890,7 @@ Send multiple payments in a single transaction using batch transactions: from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -934,7 +934,7 @@ Send multiple payments in a single transaction using batch transactions: recipient2 := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(200_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). diff --git a/src/pages/sdk/foundry/index.mdx b/src/pages/sdk/foundry/index.mdx index 6c5a9a9e..58ebe4bc 100644 --- a/src/pages/sdk/foundry/index.mdx +++ b/src/pages/sdk/foundry/index.mdx @@ -90,7 +90,7 @@ forge script script/Mail.s.sol ```bash # Set environment variables -export TEMPO_RPC_URL=https://rpc.moderato.tempo.xyz +export TEMPO_RPC_URL=https://rpc.tempo.xyz export VERIFIER_URL=https://contracts.tempo.xyz # Optional: create a new keypair and request some testnet tokens from the faucet. diff --git a/src/pages/sdk/go/index.mdx b/src/pages/sdk/go/index.mdx index a0623cf9..c98fdf77 100644 --- a/src/pages/sdk/go/index.mdx +++ b/src/pages/sdk/go/index.mdx @@ -38,7 +38,7 @@ import ( ) func main() { - c := client.New("https://rpc.testnet.tempo.xyz") + c := client.New("https://rpc.tempo.xyz") ctx := context.Background() blockNum, _ := c.GetBlockNumber(ctx) @@ -49,7 +49,7 @@ func main() { For authenticated RPC endpoints: ```go [main.go] -c := client.New("https://rpc.testnet.tempo.xyz", +c := client.New("https://rpc.tempo.xyz", client.WithAuth("username", "password"), ) ``` @@ -96,7 +96,7 @@ import ( ) func main() { - c := client.New("https://rpc.testnet.tempo.xyz") + c := client.New("https://rpc.tempo.xyz") s, _ := signer.NewSigner("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80") ctx := context.Background() @@ -105,7 +105,7 @@ func main() { recipient := common.HexToAddress("0x70997970C51812dc3A010C7d01b50e0d17dc79C8") // [!code hl:10] - tx := transaction.NewBuilder(big.NewInt(42431)). // Tempo testnet + tx := transaction.NewBuilder(big.NewInt(4217)). // Tempo mainnet SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(20000000000)). // 20 gwei base fee @@ -152,7 +152,7 @@ amount := big.NewInt(100_000_000) // 100 tokens (6 decimals) transferData, _ := erc20ABI.Pack("transfer", recipient, amount) -tx := transaction.NewBuilder(big.NewInt(42429)). +tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). @@ -175,7 +175,7 @@ copy(memo[:], "INV-12345") memoData, _ := tip20ABI.Pack("transferWithMemo", recipient, amount, memo) -tx := transaction.NewBuilder(big.NewInt(42429)). +tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). @@ -189,7 +189,7 @@ tx := transaction.NewBuilder(big.NewInt(42429)). Execute multiple operations atomically in a single transaction: ```go [batch.go] -tx := transaction.NewBuilder(big.NewInt(42429)). +tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(200000). SetMaxFeePerGas(big.NewInt(10000000000)). @@ -207,7 +207,7 @@ transaction.SignTransaction(tx, s) Send multiple transactions concurrently using different nonce keys: ```go [parallel.go] -tx1 := transaction.NewBuilder(big.NewInt(42429)). +tx1 := transaction.NewBuilder(big.NewInt(4217)). SetNonceKey(big.NewInt(1)). // Sequence A // [!code hl] SetNonce(0). SetGas(100000). @@ -216,7 +216,7 @@ tx1 := transaction.NewBuilder(big.NewInt(42429)). AddCall(recipient1, big.NewInt(0), data1). Build() -tx2 := transaction.NewBuilder(big.NewInt(42429)). +tx2 := transaction.NewBuilder(big.NewInt(4217)). SetNonceKey(big.NewInt(2)). // Sequence B (parallel) // [!code hl] SetNonce(0). SetGas(100000). @@ -238,7 +238,7 @@ go func() { c.SendRawTransaction(ctx, serialize(tx2)) }() Have another account pay for transaction fees: ```go [feepayer.go] -tx := transaction.NewBuilder(big.NewInt(42429)). +tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). @@ -258,7 +258,7 @@ Set a time window during which the transaction is valid: ```go [validity.go] now := time.Now() -tx := transaction.NewBuilder(big.NewInt(42429)). +tx := transaction.NewBuilder(big.NewInt(4217)). SetNonce(nonce). SetGas(100000). SetMaxFeePerGas(big.NewInt(10000000000)). diff --git a/src/pages/sdk/python/index.mdx b/src/pages/sdk/python/index.mdx index 2203d2d8..db000b8f 100644 --- a/src/pages/sdk/python/index.mdx +++ b/src/pages/sdk/python/index.mdx @@ -29,7 +29,7 @@ To interact with Tempo, create a web3.py client connected to a Tempo node: ```python [main.py] from web3 import Web3 -w3 = Web3(Web3.HTTPProvider("https://rpc.testnet.tempo.xyz")) # [!code hl] +w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) # [!code hl] block_number = w3.eth.block_number print(f"Connected to Tempo at block {block_number}") @@ -44,7 +44,7 @@ import os from web3 import Web3 from pytempo import Call, TempoTransaction # [!code hl] -w3 = Web3(Web3.HTTPProvider("https://rpc.testnet.tempo.xyz")) +w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) private_key = os.environ["PRIVATE_KEY"] account = w3.eth.account.from_key(private_key) @@ -84,7 +84,7 @@ amount = 100_000_000 # 100 tokens (6 decimals) transfer_data = token.encode_abi("transfer", args=[recipient, amount]) tx = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -101,7 +101,7 @@ Use a TIP-20 token to pay for transaction fees instead of the native token: ```python [fee_token.py] tx = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -119,7 +119,7 @@ Execute multiple operations atomically in a single transaction: ```python [batch.py] tx = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=300_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -141,7 +141,7 @@ Send multiple transactions concurrently using different nonce keys: ```python [parallel.py] tx1 = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -151,7 +151,7 @@ tx1 = TempoTransaction.create( ) tx2 = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -174,7 +174,7 @@ Have another account pay for transaction fees: ```python [fee_payer.py] # User creates and signs a transaction marked for fee sponsorship tx = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, @@ -197,7 +197,7 @@ import time now = int(time.time()) tx = TempoTransaction.create( - chain_id=42429, + chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 0dee9dbf..6b7b261c 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -135,7 +135,7 @@ user's preferred fee token and the validator's preferred token. from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -167,7 +167,7 @@ user's preferred fee token and the validator's preferred token. nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). @@ -387,7 +387,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -421,7 +421,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee nonce, _ := c.GetTransactionCount(ctx, senderSgn.Address().Hex()) // Sender builds and signs a sponsored transaction - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). @@ -722,7 +722,7 @@ parameter. from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -754,7 +754,7 @@ parameter. nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(600_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). @@ -1048,7 +1048,7 @@ transactions thereafter can be signed by the access key. from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -1086,7 +1086,7 @@ transactions thereafter can be signed by the access key. c := newClient() ctx := context.Background() - chainID := big.NewInt(transaction.ChainIdModerato) + chainID := big.NewInt(transaction.ChainIdMainnet) gasPrice := big.NewInt(25_000_000_000) keychainAddr := common.HexToAddress(keychain.AccountKeychainAddress) @@ -1375,7 +1375,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -1423,7 +1423,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. wg.Add(1) go func(p txParams) { defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code hl] SetGas(300_000). @@ -1647,7 +1647,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -1683,7 +1683,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo validBefore := uint64(time.Now().Unix()) + 20 - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). @@ -1906,7 +1906,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -1953,7 +1953,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** wg.Add(1) go func(p txParams) { defer wg.Done() - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(0). SetNonceKey(big.NewInt(p.nonceKey)). // [!code focus] SetGas(300_000). @@ -2178,7 +2178,7 @@ the transaction can be included in a block. from web3 import Web3 from eth_account import Account - w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + w3 = Web3(Web3.HTTPProvider("https://rpc.tempo.xyz")) account = Account.from_key("0x...") ``` @@ -2216,7 +2216,7 @@ the transaction can be included in a block. // 2026-01-02 00:00:00 UTC validBefore := uint64(time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC).Unix()) - tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdMainnet)). SetNonce(nonce). SetGas(300_000). SetMaxFeePerGas(big.NewInt(25_000_000_000)). From d620701bb0a07e8543ee465860d80a0658502695 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:08:33 +0100 Subject: [PATCH 16/40] chore(python): bump pytempo v0.4.0 (#18) --- src/pages/sdk/python/index.mdx | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pages/sdk/python/index.mdx b/src/pages/sdk/python/index.mdx index db000b8f..67a359f2 100644 --- a/src/pages/sdk/python/index.mdx +++ b/src/pages/sdk/python/index.mdx @@ -72,16 +72,11 @@ print(f"Transaction hash: {tx_hash.hex()}") ### Token Transfer -Send a TIP-20 token transfer using web3.py's contract interface: +Send a TIP-20 token transfer using pytempo's typed contract helpers: ```python [transfer.py] -erc20_abi = '[{"name":"transfer","type":"function","inputs":[{"name":"to","type":"address"},{"name":"amount","type":"uint256"}],"outputs":[{"name":"","type":"bool"}]}]' -token = w3.eth.contract(address="0x20c0000000000000000000000000000000000001", abi=erc20_abi) - -recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" -amount = 100_000_000 # 100 tokens (6 decimals) - -transfer_data = token.encode_abi("transfer", args=[recipient, amount]) +from pytempo import TempoTransaction +from pytempo.contracts import TIP20, ALPHA_USD tx = TempoTransaction.create( chain_id=4217, @@ -90,7 +85,10 @@ tx = TempoTransaction.create( max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), calls=( - Call.create(to=token.address, data=transfer_data), + TIP20(ALPHA_USD).transfer( # [!code hl] + to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8", # [!code hl] + amount=100_000_000, # 100 tokens (6 decimals) # [!code hl] + ), # [!code hl] ), ) ``` @@ -100,13 +98,16 @@ tx = TempoTransaction.create( Use a TIP-20 token to pay for transaction fees instead of the native token: ```python [fee_token.py] +from pytempo import TempoTransaction, Call +from pytempo.contracts import ALPHA_USD + tx = TempoTransaction.create( chain_id=4217, gas_limit=100_000, max_fee_per_gas=10_000_000_000, max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), - fee_token="0x20c0000000000000000000000000000000000001", # AlphaUSD // [!code hl] + fee_token=ALPHA_USD, # [!code hl] calls=( Call.create(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8"), ), @@ -118,6 +119,11 @@ tx = TempoTransaction.create( Execute multiple operations atomically in a single transaction: ```python [batch.py] +from pytempo import TempoTransaction +from pytempo.contracts import TIP20, ALPHA_USD + +token = TIP20(ALPHA_USD) + tx = TempoTransaction.create( chain_id=4217, gas_limit=300_000, @@ -125,9 +131,9 @@ tx = TempoTransaction.create( max_priority_fee_per_gas=1_000_000_000, nonce=w3.eth.get_transaction_count(account.address), calls=( - Call.create(to=addr1, data=transfer1_data), # [!code hl] - Call.create(to=addr2, data=transfer2_data), # [!code hl] - Call.create(to=addr3, data=contract_call_data), # [!code hl] + token.transfer(to="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", amount=100_000_000), # [!code hl] + token.transfer(to="0x70997970C51812dc3A010C7d01b50e0d17dc79C8", amount=50_000_000), # [!code hl] + token.transfer(to="0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", amount=25_000_000), # [!code hl] ), ) From 29499b814c06771da482b2d8ad2e17647b0dc999 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 16 Mar 2026 13:08:51 +0100 Subject: [PATCH 17/40] fix(rust): use signer provider for sending txs (#17) --- src/pages/guide/payments/send-a-payment.mdx | 6 ++-- src/snippets/rust-signer-provider.rs | 21 +++++++++++++ src/snippets/tempo-tx-properties.mdx | 34 +++++++++++---------- 3 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 src/snippets/rust-signer-provider.rs diff --git a/src/pages/guide/payments/send-a-payment.mdx b/src/pages/guide/payments/send-a-payment.mdx index b6af31ec..d25821e4 100644 --- a/src/pages/guide/payments/send-a-payment.mdx +++ b/src/pages/guide/payments/send-a-payment.mdx @@ -277,7 +277,7 @@ Send a payment using the standard `transfer` function: ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -523,7 +523,7 @@ Include a memo for payment reconciliation and tracking. The memo is a 32-byte va ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -839,7 +839,7 @@ Send multiple payments in a single transaction using batch transactions: ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: diff --git a/src/snippets/rust-signer-provider.rs b/src/snippets/rust-signer-provider.rs new file mode 100644 index 00000000..c44c2b63 --- /dev/null +++ b/src/snippets/rust-signer-provider.rs @@ -0,0 +1,21 @@ +// [!region setup] +use alloy::providers::ProviderBuilder; +use alloy::signers::local::PrivateKeySigner; +use tempo_alloy::TempoNetwork; + +pub async fn get_provider() -> Result< + impl alloy::providers::Provider, + Box, +> { + let signer: PrivateKeySigner = std::env::var("PRIVATE_KEY") + .expect("PRIVATE_KEY not set") + .parse()?; + + let provider = ProviderBuilder::new_with_network::() + .wallet(signer) + .connect(&std::env::var("RPC_URL").expect("RPC_URL not set")) + .await?; + + Ok(provider) +} +// [!endregion setup] diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index 6b7b261c..eba877e0 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -71,6 +71,7 @@ user's preferred fee token and the validator's preferred token. ```rust [example.rs] use alloy::primitives::{address, bytes}; + use alloy::providers::Provider; use tempo_alloy::rpc::TempoTransactionRequest; mod provider; @@ -95,7 +96,7 @@ user's preferred fee token and the validator's preferred token. ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -307,6 +308,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee ```rust [example.rs] use alloy::primitives::{U256, address, bytes}; + use alloy::providers::Provider; use alloy::signers::{SignerSync, local::PrivateKeySigner}; use tempo_alloy::primitives::transaction::tempo_transaction::Call; use tempo_alloy::rpc::TempoTransactionRequest; @@ -317,7 +319,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee async fn main() -> Result<(), Box> { let provider = provider::get_provider().await?; - let mut tx = TempoTransactionRequest { + let tx = TempoTransactionRequest { calls: vec![Call { to: address!("0xcafebabecafebabecafebabecafebabecafebabe").into(), value: U256::ZERO, @@ -326,16 +328,16 @@ over the transaction with a special "fee payer envelope" to commit to paying fee ..Default::default() }; - // Fill gas, nonce, etc. from the network - let filled = provider.fill(tx).await?; - let mut tempo_tx = filled.build_aa()?; + // Step 1: Build the transaction + let mut tempo_tx = provider.fill(tx).await?.build_aa()?; + let sender_addr = provider.default_signer_address(); + let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); - // Fee payer signs the transaction // [!code hl] - let fee_payer = PrivateKeySigner::from_str("0x...")?; // [!code hl] - let sender_addr = provider.default_signer_address(); // [!code hl] - let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); // [!code hl] + // Step 2: Fee payer counter-signs the transaction // [!code hl] + let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] + // Step 3: Broadcast let pending = provider.send_transaction(tempo_tx).await?; Ok(()) @@ -343,7 +345,7 @@ over the transaction with a special "fee payer envelope" to commit to paying fee ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -677,7 +679,7 @@ parameter. ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -985,7 +987,7 @@ transactions thereafter can be signed by the access key. ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -1337,7 +1339,7 @@ In **Viem** and **Wagmi**, expiring nonces are handled automatically. ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -1603,7 +1605,7 @@ Expiring nonces can be used by setting `nonceKey` to `maxUint256` and `validBefo ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -1869,7 +1871,7 @@ For cases requiring ordered sequences within a key, Tempo's **2D nonce system** ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: @@ -2131,7 +2133,7 @@ the transaction can be included in a block. ``` ```rust [provider.rs] - // [!include ~/snippets/rust-provider.rs:setup] + // [!include ~/snippets/rust-signer-provider.rs:setup] ``` ::: From 158032f734502552fb84b1eb0ab51d1add9f0129 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 11:58:34 -0400 Subject: [PATCH 18/40] docs: testnet -> mainnet for code examples, network details, etc (#8) * chore: tweaks * docs: testnet -> mainnet for code examples, network details, etc --- src/components/TokenList.tsx | 6 ++-- src/pages/guide/_template.mdx | 12 +++---- .../guide/issuance/create-a-stablecoin.mdx | 16 ++++----- .../guide/issuance/distribute-rewards.mdx | 12 +++---- .../guide/issuance/manage-stablecoin.mdx | 28 +++++++-------- src/pages/guide/issuance/mint-stablecoins.mdx | 12 +++---- src/pages/guide/issuance/use-for-fees.mdx | 4 +-- .../guide/payments/sponsor-user-fees.mdx | 8 ++--- .../stablecoin-dex/managing-fee-liquidity.mdx | 24 ++++++------- src/pages/guide/use-accounts/add-funds.mdx | 2 +- .../guide/use-accounts/connect-to-wallets.mdx | 34 +++++++++++++------ .../guide/use-accounts/embed-passkeys.mdx | 12 +++---- src/pages/index.mdx | 4 +-- src/pages/learn/partners.mdx | 2 +- src/pages/quickstart/connection-details.mdx | 23 +++++++++---- src/pages/quickstart/tokenlist.mdx | 17 ++++++---- .../sdk/typescript/server/handler.compose.mdx | 4 +-- .../typescript/server/handler.feePayer.mdx | 8 ++--- .../typescript/server/handler.keyManager.mdx | 4 +-- src/snippets/setup.ts | 4 +-- src/snippets/tempo-tx-properties.mdx | 6 ++-- src/snippets/unformatted/withFeePayer.ts | 6 ++-- src/snippets/viem.config.ts | 4 +-- src/snippets/wagmi.config.ts | 12 +++---- 24 files changed, 146 insertions(+), 118 deletions(-) diff --git a/src/components/TokenList.tsx b/src/components/TokenList.tsx index 302090bf..0b1dd429 100644 --- a/src/components/TokenList.tsx +++ b/src/components/TokenList.tsx @@ -4,9 +4,9 @@ import { useQuery } from '@tanstack/react-query' export function TokenListDemo() { const tokenList = useQuery({ - queryKey: ['tokenList', 42431], + queryKey: ['tokenList', 4217], queryFn: async () => { - const response = await fetch('https://tokenlist.tempo.xyz/list/42431') + const response = await fetch('https://tokenlist.tempo.xyz/list/4217') const data = await response.json() if (!Object.hasOwn(data, 'tokens')) throw new Error('Invalid token list') return data.tokens as Array<{ @@ -29,7 +29,7 @@ export function TokenListDemo() { target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 text-content" - href={`https://tokenlist.tempo.xyz/asset/42431/${token.address}`} + href={`https://tokenlist.tempo.xyz/asset/4217/${token.address}`} > {token.name} {token.name} diff --git a/src/pages/guide/_template.mdx b/src/pages/guide/_template.mdx index 27cb4f21..516907a4 100644 --- a/src/pages/guide/_template.mdx +++ b/src/pages/guide/_template.mdx @@ -42,16 +42,16 @@ export function Component() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], transports: { - [tempoModerato.id]: http(), + [tempo.id]: http(), }, }) ``` @@ -74,16 +74,16 @@ export function Component() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], transports: { - [tempoModerato.id]: http(), + [tempo.id]: http(), }, }) ``` diff --git a/src/pages/guide/issuance/create-a-stablecoin.mdx b/src/pages/guide/issuance/create-a-stablecoin.mdx index 4599ca55..cfe25a5c 100644 --- a/src/pages/guide/issuance/create-a-stablecoin.mdx +++ b/src/pages/guide/issuance/create-a-stablecoin.mdx @@ -68,11 +68,11 @@ export function AddFunds() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -129,11 +129,11 @@ export function CreateStablecoin() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -197,11 +197,11 @@ export function CreateStablecoin() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -265,11 +265,11 @@ export function CreateStablecoin() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], diff --git a/src/pages/guide/issuance/distribute-rewards.mdx b/src/pages/guide/issuance/distribute-rewards.mdx index 0e5bab24..dd9c43af 100644 --- a/src/pages/guide/issuance/distribute-rewards.mdx +++ b/src/pages/guide/issuance/distribute-rewards.mdx @@ -76,11 +76,11 @@ export function OptInToRewards() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -136,11 +136,11 @@ export function StartReward() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -189,11 +189,11 @@ export function ClaimReward() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], diff --git a/src/pages/guide/issuance/manage-stablecoin.mdx b/src/pages/guide/issuance/manage-stablecoin.mdx index fdb4560b..4cd91cb0 100644 --- a/src/pages/guide/issuance/manage-stablecoin.mdx +++ b/src/pages/guide/issuance/manage-stablecoin.mdx @@ -73,11 +73,11 @@ export function GrantRoles() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -152,11 +152,11 @@ export function GrantRoles() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -265,11 +265,11 @@ export function RevokeRoles() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -336,11 +336,11 @@ export function SetSupplyCap() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -445,11 +445,11 @@ export function LinkTokenPolicy() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -525,11 +525,11 @@ export function PauseUnpauseTransfers() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -599,11 +599,11 @@ export function BurnBlocked() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], diff --git a/src/pages/guide/issuance/mint-stablecoins.mdx b/src/pages/guide/issuance/mint-stablecoins.mdx index 70621a4b..b4b8caca 100644 --- a/src/pages/guide/issuance/mint-stablecoins.mdx +++ b/src/pages/guide/issuance/mint-stablecoins.mdx @@ -76,11 +76,11 @@ export function GrantIssuerRole() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -179,11 +179,11 @@ export function MintToken() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], @@ -338,11 +338,11 @@ export function BurnToken() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], diff --git a/src/pages/guide/issuance/use-for-fees.mdx b/src/pages/guide/issuance/use-for-fees.mdx index 0c5cdc36..4ad675c4 100644 --- a/src/pages/guide/issuance/use-for-fees.mdx +++ b/src/pages/guide/issuance/use-for-fees.mdx @@ -200,11 +200,11 @@ export function PayWithIssuedToken() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ keyManager: KeyManager.localStorage(), })], diff --git a/src/pages/guide/payments/sponsor-user-fees.mdx b/src/pages/guide/payments/sponsor-user-fees.mdx index c3c56618..6360739a 100644 --- a/src/pages/guide/payments/sponsor-user-fees.mdx +++ b/src/pages/guide/payments/sponsor-user-fees.mdx @@ -127,10 +127,10 @@ The example above uses a fee payer server to sign and sponsor transactions. If y // @noErrors import { createClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' const client = createClient({ - chain: tempoModerato, + chain: tempo, transport: http(), }) @@ -169,10 +169,10 @@ The `withFeePayer` transport and `feePayer: true` parameter handle this automati // @noErrors import { createClient, http, parseUnits } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' const client = createClient({ - chain: tempoModerato, + chain: tempo, transport: http(), }) diff --git a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx b/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx index e765be52..70fa56f2 100644 --- a/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx +++ b/src/pages/guide/stablecoin-dex/managing-fee-liquidity.mdx @@ -54,7 +54,7 @@ function ManageFeeLiquidity() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -64,7 +64,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), @@ -122,7 +122,7 @@ function ManageFeeLiquidity() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -132,7 +132,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), @@ -197,7 +197,7 @@ function ManageFeeLiquidity() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -207,7 +207,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), @@ -284,7 +284,7 @@ function ManageFeeLiquidity() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -294,7 +294,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), @@ -354,7 +354,7 @@ function MonitorSwaps() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -364,7 +364,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), @@ -422,7 +422,7 @@ function RebalancePool() { ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -432,7 +432,7 @@ export const config = createConfig({ keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { [tempo.id]: http(), diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index fcced800..34343329 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -1,5 +1,5 @@ --- -description: Get test stablecoins on Tempo testnet using the faucet. Request pathUSD, AlphaUSD, BetaUSD, and ThetaUSD tokens for development and testing. +description: Get test stablecoins on Tempo Testnet using the faucet. Request pathUSD, AlphaUSD, BetaUSD, and ThetaUSD tokens for development and testing. mipd: true --- diff --git a/src/pages/guide/use-accounts/connect-to-wallets.mdx b/src/pages/guide/use-accounts/connect-to-wallets.mdx index f950a0dd..73915995 100644 --- a/src/pages/guide/use-accounts/connect-to-wallets.mdx +++ b/src/pages/guide/use-accounts/connect-to-wallets.mdx @@ -43,11 +43,11 @@ We can also utilize [wallet connectors](https://wagmi.sh/react/api/connectors) f ```tsx twoslash [config.ts] // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { metaMask } from 'wagmi/connectors' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [metaMask()], // [!code ++] multiInjectedProviderDiscovery: true, // [!code ++] transports: { @@ -104,11 +104,11 @@ export function Connect() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { metaMask } from 'wagmi/connectors' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [metaMask()], // [!code ++] multiInjectedProviderDiscovery: true, // [!code ++] transports: { @@ -164,11 +164,11 @@ export function Account() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { metaMask } from 'wagmi/connectors' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [metaMask()], // [!code ++] multiInjectedProviderDiscovery: true, // [!code ++] transports: { @@ -200,7 +200,7 @@ If the wallet is not on the Tempo network, we can display a "Add Tempo" button s ```tsx twoslash [Account.tsx] // @noErrors import { useConnection, useDisconnect, useSwitchChain } from 'wagmi' -import { tempoModerato } from 'viem/chains' // [!code ++] +import { tempo } from 'viem/chains' // [!code ++] export function Account() { const account = useConnection() @@ -241,11 +241,11 @@ export function Account() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { metaMask } from 'wagmi/connectors' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [metaMask()], // [!code ++] multiInjectedProviderDiscovery: true, // [!code ++] transports: { @@ -272,7 +272,7 @@ It is worth noting that some wallets that are included in the above libraries ma ## Add to Wallet Manually -You can add Tempo testnet to a wallet that supports custom networks (e.g. MetaMask) manually. +You can add Tempo to a wallet that supports custom networks (e.g. MetaMask) manually. For example, if you are using MetaMask: @@ -280,13 +280,25 @@ For example, if you are using MetaMask: 2. Click "Add a custom network" 3. Enter the network details: +#### Mainnet + +| **Name** | `Tempo Mainnet` | +|-------------------|-------| +| **Currency** | `USD` | +| **Chain ID** | `4217` | +| **HTTP URL** | `https://rpc.presto.tempo.xyz` | +| **WebSocket URL** | `wss://rpc.presto.tempo.xyz` | +| **Block Explorer** | [`https://explore.mainnet.tempo.xyz`](https://explore.mainnet.tempo.xyz) | + +#### Testnet + | **Name** | `Tempo Testnet (Moderato)` | |-------------------|-------| | **Currency** | `USD` | | **Chain ID** | `42431` | | **HTTP URL** | `https://rpc.moderato.tempo.xyz` | | **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | -| **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | +| **Block Explorer** | [`https://explore.moderato.tempo.xyz`](https://explore.moderato.tempo.xyz) | The official documentation from MetaMask on this process is also available [here](https://support.metamask.io/configure/networks/how-to-add-a-custom-network-rpc#adding-a-network-manually). diff --git a/src/pages/guide/use-accounts/embed-passkeys.mdx b/src/pages/guide/use-accounts/embed-passkeys.mdx index e7097bc4..c5266c9c 100644 --- a/src/pages/guide/use-accounts/embed-passkeys.mdx +++ b/src/pages/guide/use-accounts/embed-passkeys.mdx @@ -48,11 +48,11 @@ Next, we will need to configure the `webAuthn` connector in our Wagmi config. ```tsx twoslash [config.ts] // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ // [!code ++] keyManager: KeyManager.localStorage(), // [!code ++] })], // [!code ++] @@ -130,11 +130,11 @@ export function Example() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ // [!code ++] keyManager: KeyManager.localStorage(), // [!code ++] })], // [!code ++] @@ -208,11 +208,11 @@ export function Example() { ```tsx twoslash [config.ts] filename="config.ts" // @noErrors import { createConfig, http } from 'wagmi' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' // [!code ++] export const config = createConfig({ - chains: [tempoModerato], + chains: [tempo], connectors: [webAuthn({ // [!code ++] keyManager: KeyManager.localStorage(), // [!code ++] })], // [!code ++] diff --git a/src/pages/index.mdx b/src/pages/index.mdx index f3d5fad4..dd427868 100644 --- a/src/pages/index.mdx +++ b/src/pages/index.mdx @@ -22,10 +22,10 @@ Whether you're new to stablecoins, ready to start building, or looking for partn title="Learn About Stablecoins" /> @@ -26,12 +26,23 @@ To connect via CLI, we recommend using [`cast`](https://getfoundry.sh/cast/overv ```bash /dev/null/monitor.sh#L1-11 # Check block height (should be steadily increasing) -cast block-number --rpc-url https://rpc.moderato.tempo.xyz +cast block-number --rpc-url https://rpc.presto.tempo.xyz ``` ## Direct Connection Details -If you're manually connecting to Tempo Testnet, you can use the following details: +### Mainnet + +| **Property** | **Value** | +|-------------------|-------| +| **Network Name** | Tempo Mainnet | +| **Currency** | `USD` | +| **Chain ID** | `4217` | +| **HTTP URL** | `https://rpc.presto.tempo.xyz` | +| **WebSocket URL** | `wss://rpc.presto.tempo.xyz` | +| **Block Explorer** | [`https://explore.mainnet.tempo.xyz`](https://explore.mainnet.tempo.xyz) | + +### Testnet | **Property** | **Value** | |-------------------|-------| @@ -40,4 +51,4 @@ If you're manually connecting to Tempo Testnet, you can use the following detail | **Chain ID** | `42431` | | **HTTP URL** | `https://rpc.moderato.tempo.xyz` | | **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | -| **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | +| **Block Explorer** | [`https://explore.moderato.tempo.xyz`](https://explore.moderato.tempo.xyz) | diff --git a/src/pages/quickstart/tokenlist.mdx b/src/pages/quickstart/tokenlist.mdx index 1ec420fb..99daa1b3 100644 --- a/src/pages/quickstart/tokenlist.mdx +++ b/src/pages/quickstart/tokenlist.mdx @@ -9,7 +9,7 @@ import { TokenListDemo } from '../../components/TokenList.tsx' A [Uniswap Token Lists](https://tokenlists.org)-compatible API for token metadata and icons on Tempo. -As an example, here's Tempo Testnet's tokenlist, fetched from [tokenlist.tempo.xyz/list/42431](https://tokenlist.tempo.xyz/list/42431): +As an example, here's Tempo's tokenlist, fetched from [tokenlist.tempo.xyz/list/4217](https://tokenlist.tempo.xyz/list/4217): @@ -17,10 +17,15 @@ As an example, here's Tempo Testnet's tokenlist, fetched from [tokenlist.tempo.x | Endpoint | Description | |----------|-------------| -[`/list/{chain_id}`](https://tokenlist.tempo.xyz/list/42431) | Token list for a chain | -[`/asset/{chain_id}/{id}`](https://tokenlist.tempo.xyz/asset/42431/pathUSD) | Get a single token by symbol or address​ -[`/icon/{chain_id}`](https://tokenlist.tempo.xyz/icon/42431) | Chain icon (SVG) | -[`/icon/{chain_id}/{address}`](https://tokenlist.tempo.xyz/icon/42431/0x20c0000000000000000000000000000000000000) | Token icon (SVG) | +[`/list/{chain_id}`](https://tokenlist.tempo.xyz/list/4217) | Token list for a chain | +[`/asset/{chain_id}/{id}`](https://tokenlist.tempo.xyz/asset/4217/pathUSD) | Get a single token by symbol or address​ +[`/icon/{chain_id}`](https://tokenlist.tempo.xyz/icon/4217) | Chain icon (SVG) | +[`/icon/{chain_id}/{address}`](https://tokenlist.tempo.xyz/icon/4217/0x20c0000000000000000000000000000000000000) | Token icon (SVG) | + +| Chain | `chain_id` | +|-------|------------| +| Mainnet | `4217` | +| Testnet (Moderato) | `42431` | ## Adding a New Token @@ -32,7 +37,7 @@ As an example, here's Tempo Testnet's tokenlist, fetched from [tokenlist.tempo.x "name": "piUSD", "symbol": "PiUSD", "decimals": 6, - "chainId": 42431, + "chainId": 4217, "address": "0x..." } ``` diff --git a/src/pages/sdk/typescript/server/handler.compose.mdx b/src/pages/sdk/typescript/server/handler.compose.mdx index f72682ac..2f6b2798 100644 --- a/src/pages/sdk/typescript/server/handler.compose.mdx +++ b/src/pages/sdk/typescript/server/handler.compose.mdx @@ -11,7 +11,7 @@ Composes multiple handlers into a single handler. This is useful when you want t ```ts twoslash [server.ts] // @noErrors import { Handler, Kv } from 'tempo.ts/server' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' @@ -19,7 +19,7 @@ const handler = Handler.compose([ // Create a fee payer handler Handler.feePayer({ account: privateKeyToAccount('0x...'), - chain: tempoModerato.extend({ + chain: tempo.extend({ feeToken: '0x20c0...0001' }), transport: http(), diff --git a/src/pages/sdk/typescript/server/handler.feePayer.mdx b/src/pages/sdk/typescript/server/handler.feePayer.mdx index 6344d28c..aa897add 100644 --- a/src/pages/sdk/typescript/server/handler.feePayer.mdx +++ b/src/pages/sdk/typescript/server/handler.feePayer.mdx @@ -14,13 +14,13 @@ This enables you to subsidize gas costs for your users by signing transactions w ```ts twoslash [server.ts] // @noErrors import { Handler } from 'tempo.ts/server' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' const handler = Handler.feePayer({ account: privateKeyToAccount('0x...'), - chain: tempoModerato.extend({ feeToken: '0x20c0...0001' }), + chain: tempo.extend({ feeToken: '0x20c0...0001' }), path: '/fee-payer', transport: http(), }) @@ -29,11 +29,11 @@ const handler = Handler.feePayer({ ```ts twoslash [example.client.ts] // @noErrors import { createClient, http, walletActions } from 'viem' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { tempoActions, withFeePayer } from 'viem/tempo' const client = createClient({ - chain: tempoModerato, + chain: tempo, transport: withFeePayer( http(), // Default transport http('http://localhost:3000/fee-payer'), // Fee payer transport (your server) diff --git a/src/pages/sdk/typescript/server/handler.keyManager.mdx b/src/pages/sdk/typescript/server/handler.keyManager.mdx index f7104ce0..c5f04c6c 100644 --- a/src/pages/sdk/typescript/server/handler.keyManager.mdx +++ b/src/pages/sdk/typescript/server/handler.keyManager.mdx @@ -23,7 +23,7 @@ const handler = Handler.keyManager({ ```ts twoslash [wagmi.config.ts] // @noErrors -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -34,7 +34,7 @@ export const config = createConfig({ rpId: 'example.com', }), ], - chains: [tempoModerato], + chains: [tempo], transports: { [tempo.id]: http(), }, diff --git a/src/snippets/setup.ts b/src/snippets/setup.ts index d3fca6ad..119968b4 100644 --- a/src/snippets/setup.ts +++ b/src/snippets/setup.ts @@ -2,12 +2,12 @@ import { createClient, http, publicActions, walletActions } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { tempoActions } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), - chain: tempoModerato, + chain: tempo, transport: http(), }) .extend(publicActions) diff --git a/src/snippets/tempo-tx-properties.mdx b/src/snippets/tempo-tx-properties.mdx index eba877e0..e078dd0e 100644 --- a/src/snippets/tempo-tx-properties.mdx +++ b/src/snippets/tempo-tx-properties.mdx @@ -885,7 +885,7 @@ transactions thereafter can be signed by the access key. ```tsx twoslash [example.ts] // @noErrors - import { tempoModerato } from 'viem/chains' + import { tempo } from 'viem/chains' import { KeyManager, webAuthn } from 'wagmi/tempo' import { createConfig, http } from 'wagmi' @@ -896,10 +896,10 @@ transactions thereafter can be signed by the access key. keyManager: KeyManager.localStorage(), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { - [tempoModerato.id]: http(), + [tempo.id]: http(), }, }) ``` diff --git a/src/snippets/unformatted/withFeePayer.ts b/src/snippets/unformatted/withFeePayer.ts index 55d533cf..2ec49c4e 100644 --- a/src/snippets/unformatted/withFeePayer.ts +++ b/src/snippets/unformatted/withFeePayer.ts @@ -5,7 +5,7 @@ import { withFeePayer } from 'viem/tempo' const _client = createClient({ account: privateKeyToAccount('0x...'), - chain: tempoModerato, + chain: tempo, transport: withFeePayer( // [!code hl] http(), // [!code hl] @@ -34,10 +34,10 @@ import { Handler } from 'tempo.ts/server' // [!region server] import { createClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' const client = createClient({ - chain: tempoModerato.extend({ + chain: tempo.extend({ feeToken: '0x20c0000000000000000000000000000000000001', }), transport: http(), diff --git a/src/snippets/viem.config.ts b/src/snippets/viem.config.ts index 8b92be10..5404b57c 100644 --- a/src/snippets/viem.config.ts +++ b/src/snippets/viem.config.ts @@ -1,12 +1,12 @@ // [!region setup] import { createClient, http, publicActions, walletActions } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { tempoActions } from 'viem/tempo' export const client = createClient({ account: privateKeyToAccount('0x...'), - chain: tempoModerato, + chain: tempo, transport: http(), }) .extend(publicActions) diff --git a/src/snippets/wagmi.config.ts b/src/snippets/wagmi.config.ts index 2620b8b1..0fce1d1f 100644 --- a/src/snippets/wagmi.config.ts +++ b/src/snippets/wagmi.config.ts @@ -4,7 +4,7 @@ import { KeyManager, webAuthn } from 'tempo.ts/wagmi' // [!region setup] -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { createConfig, http } from 'wagmi' import { KeyManager, webAuthn } from 'wagmi/tempo' @@ -14,10 +14,10 @@ export const config = createConfig({ keyManager: KeyManager.http('https://keys.tempo.xyz'), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { - [tempoModerato.id]: http(), + [tempo.id]: http(), }, }) @@ -25,7 +25,7 @@ export const config = createConfig({ import { KeyManager, webAuthn } from 'tempo.ts/wagmi' // [!region withFeePayer] -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import { withFeePayer } from 'viem/tempo' import { createConfig, http } from 'wagmi' import { KeyManager, webAuthn } from 'wagmi/tempo' @@ -36,10 +36,10 @@ export const config = createConfig({ keyManager: KeyManager.http('https://keys.tempo.xyz'), }), ], - chains: [tempoModerato], + chains: [tempo], multiInjectedProviderDiscovery: false, transports: { - [tempoModerato.id]: withFeePayer(http(), http('https://sponsor.moderato.tempo.xyz')), + [tempo.id]: withFeePayer(http(), http('https://sponsor.moderato.tempo.xyz')), }, }) // [!endregion withFeePayer] From 0ec6e2726131d34e9f2c2a609c12854fd12b94b9 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 12:31:00 -0400 Subject: [PATCH 19/40] ci: sync workflow (#22) --- .github/workflows/sync.yml | 33 +++++++++++++++++++++++++++++++++ README.md | 4 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/sync.yml diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml new file mode 100644 index 00000000..819434dd --- /dev/null +++ b/.github/workflows/sync.yml @@ -0,0 +1,33 @@ +name: Sync from docs +on: + workflow_dispatch: + +jobs: + sync: + name: Sync + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + pull-requests: write + + steps: + - name: Clone repository + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Fetch upstream and create sync branch + run: | + git remote add upstream https://github.com/tempoxyz/docs.git + git fetch upstream main + branch="sync/docs-$(date +%Y%m%d-%H%M%S)" + git checkout -b "$branch" + git merge upstream/main --no-edit + git push origin "$branch" + echo "branch=$branch" >> "$GITHUB_ENV" + + - name: Create pull request + run: gh pr create --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)." + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 1111acb2..e3a1c82f 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@

- - tempo combomark + + tempo lockup

From 0879a31cd0e15d103d92ef8003eb5b2440102b3e Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 12:33:29 -0400 Subject: [PATCH 20/40] chore: docs sync (#23) --- .github/workflows/sync.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 819434dd..d03507a3 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -28,6 +28,6 @@ jobs: echo "branch=$branch" >> "$GITHUB_ENV" - name: Create pull request - run: gh pr create --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)." + run: 'gh pr create --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)."' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From a78a3ae4e6ded07332d3c86a0aa5f5e2082845d0 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 12:42:43 -0400 Subject: [PATCH 21/40] ci: up (#24) --- .github/workflows/sync.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index d03507a3..6455a2bc 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -19,15 +19,24 @@ jobs: - name: Fetch upstream and create sync branch run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git remote add upstream https://github.com/tempoxyz/docs.git git fetch upstream main branch="sync/docs-$(date +%Y%m%d-%H%M%S)" git checkout -b "$branch" git merge upstream/main --no-edit - git push origin "$branch" - echo "branch=$branch" >> "$GITHUB_ENV" + if git diff --quiet origin/main; then + echo "No changes to sync." + echo "has_changes=false" >> "$GITHUB_ENV" + else + git push origin "$branch" + echo "branch=$branch" >> "$GITHUB_ENV" + echo "has_changes=true" >> "$GITHUB_ENV" + fi - name: Create pull request - run: 'gh pr create --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)."' + if: env.has_changes == 'true' + run: 'gh pr create --head "$branch" --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)."' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 396ce30d37cf853913d74f468b05e8747d2d5de9 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 13:06:09 -0400 Subject: [PATCH 22/40] chore: sync from tempoxyz/docs (#26) * chore: remove thirdweb from developer tools page (#146) Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> * fix(docs): add fee recipient language (#147) --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> --- src/pages/guide/node/validator.mdx | 2 ++ src/pages/quickstart/developer-tools.mdx | 6 ------ 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/guide/node/validator.mdx b/src/pages/guide/node/validator.mdx index d592c070..1471343b 100644 --- a/src/pages/guide/node/validator.mdx +++ b/src/pages/guide/node/validator.mdx @@ -49,6 +49,8 @@ tempo node --datadir \ --telemetry-url ``` +If you are not prepared to accept fees, we recommend setting the `--consensus.fee-recipient` field to `0x0000000000000000000000000000000000000001` as that will funnel the funds to a non-user controllable wallet (there is no known private key for the address). + ### Optional flags | Flag | Description | diff --git a/src/pages/quickstart/developer-tools.mdx b/src/pages/quickstart/developer-tools.mdx index cd5953db..756cfc23 100644 --- a/src/pages/quickstart/developer-tools.mdx +++ b/src/pages/quickstart/developer-tools.mdx @@ -152,12 +152,6 @@ You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/c Check out Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. ::: -### thirdweb - -[thirdweb](https://thirdweb.com) provides a full-stack developer platform for building modern onchain applications. Developers can create wallets, deploy tokens, use blockchain-native AI tools, and integrate native internet payments—all with built-in support for Tempo. Access the Tempo Testnet via the [thirdweb dashboard](https://thirdweb.com/tempo-testnet) and explore tooling in the [Thirdweb developer docs](https://portal.thirdweb.com). - -Try features live in the [thirdweb Playground](https://playground.thirdweb.com) and create an account by signing up [here](https://thirdweb.com/login). - ### Turnkey [Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo transactions, automate wallet operations, and build custom logic around how keys are used. From 8c227b75ec808f017aed317f2ed93ab22ec29796 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 13:06:29 -0400 Subject: [PATCH 23/40] chore: bump vocs next (#27) --- .github/workflows/sync.yml | 42 -------------------- package.json | 4 +- pnpm-lock.yaml | 78 ++++++++++++++++++++++++++++---------- 3 files changed, 61 insertions(+), 63 deletions(-) delete mode 100644 .github/workflows/sync.yml diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml deleted file mode 100644 index 6455a2bc..00000000 --- a/.github/workflows/sync.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Sync from docs -on: - workflow_dispatch: - -jobs: - sync: - name: Sync - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - contents: write - pull-requests: write - - steps: - - name: Clone repository - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Fetch upstream and create sync branch - run: | - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git remote add upstream https://github.com/tempoxyz/docs.git - git fetch upstream main - branch="sync/docs-$(date +%Y%m%d-%H%M%S)" - git checkout -b "$branch" - git merge upstream/main --no-edit - if git diff --quiet origin/main; then - echo "No changes to sync." - echo "has_changes=false" >> "$GITHUB_ENV" - else - git push origin "$branch" - echo "branch=$branch" >> "$GITHUB_ENV" - echo "has_changes=true" >> "$GITHUB_ENV" - fi - - - name: Create pull request - if: env.has_changes == 'true' - run: 'gh pr create --head "$branch" --title "chore: sync from tempoxyz/docs" --body "Automated sync of latest changes from [tempoxyz/docs](https://github.com/tempoxyz/docs)."' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package.json b/package.json index 24470774..c5170de5 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,9 @@ "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", "viem": "^2.44.4", - "vocs": "https://pkg.pr.new/wevm/vocs@e5ad67e", + "vocs": "https://pkg.pr.new/vocs@728c0ca", "wagmi": "3.4.1", - "waku": "1.0.0-alpha.2", + "waku": "1.0.0-alpha.4", "zod": "^4.3.5" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47067146..9a5659b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,14 +89,14 @@ importers: specifier: ^2.44.4 version: 2.44.4(typescript@5.9.3)(zod@4.3.5) vocs: - specifier: https://pkg.pr.new/wevm/vocs@e5ad67e - version: https://pkg.pr.new/wevm/vocs@e5ad67e(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.2(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: https://pkg.pr.new/vocs@728c0ca + version: https://pkg.pr.new/vocs@728c0ca(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) wagmi: specifier: 3.4.1 version: 3.4.1(@tanstack/query-core@5.90.19)(@tanstack/react-query@5.90.19(react@19.2.3))(@types/react@19.2.9)(ox@0.11.3(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(viem@2.44.4(typescript@5.9.3)(zod@4.3.5)) waku: - specifier: 1.0.0-alpha.2 - version: 1.0.0-alpha.2(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + specifier: 1.0.0-alpha.4 + version: 1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) zod: specifier: ^4.3.5 version: 4.3.5 @@ -803,6 +803,9 @@ packages: '@rolldown/pluginutils@1.0.0-beta.53': resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} + '@rolldown/pluginutils@1.0.0-rc.5': + resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} + '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} engines: {node: '>=14.0.0'} @@ -1489,6 +1492,17 @@ packages: react-server-dom-webpack: optional: true + '@vitejs/plugin-rsc@0.5.21': + resolution: {integrity: sha512-uNayLT8IKvWoznvQyfwKuGiEFV28o7lxUDnw/Av36VCuGpDFZnMmvVCwR37gTvnSmnpul9V0tdJqY3tBKEaDqw==} + peerDependencies: + react: '*' + react-dom: '*' + react-server-dom-webpack: '*' + vite: '*' + peerDependenciesMeta: + react-server-dom-webpack: + optional: true + '@wagmi/connectors@7.1.5': resolution: {integrity: sha512-+hrb4RJywjGtUsDZNLSc4eOF+jD6pVkCZ/KFi24p993u0ymsm/kGTLXjhYx5r8Rf/cxFHEiaQaRnEfB9qyDJyw==} peerDependencies: @@ -2052,8 +2066,8 @@ packages: resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} engines: {node: '>=12'} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} dunder-proto@1.0.1: @@ -3446,6 +3460,11 @@ packages: engines: {node: '>=20.16.0'} hasBin: true + srvx@0.11.12: + resolution: {integrity: sha512-AQfrGqntqVPXgP03pvBDN1KyevHC+KmYVqb8vVf4N+aomQqdhaZxjvoVp+AOm4u6x+GgNQY3MVzAUIn+TqwkOA==} + engines: {node: '>=20.16.0'} + hasBin: true + state-local@1.0.7: resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} @@ -3810,8 +3829,8 @@ packages: vite: optional: true - vocs@https://pkg.pr.new/wevm/vocs@e5ad67e: - resolution: {integrity: sha512-CIZ4KPSeTK3f4V2Ti25OG6VPfMpd/2/j5vzbTlapoqd+cbYIbdbB5oOvlnhtInqvfXFLACVU/bNf8fx/HORVHw==, tarball: https://pkg.pr.new/wevm/vocs@e5ad67e} + vocs@https://pkg.pr.new/vocs@728c0ca: + resolution: {tarball: https://pkg.pr.new/vocs@728c0ca} version: 0.0.0 hasBin: true peerDependencies: @@ -3865,14 +3884,14 @@ packages: typescript: optional: true - waku@1.0.0-alpha.2: - resolution: {integrity: sha512-AqS1+jEH+gBeqbxt+Tg8cbDp9Kxqgilr/awifeaWyZsKfsMDfzi0thw/OZlW9KT2W8xJNV1lG4OwSNxgvigJpA==} + waku@1.0.0-alpha.4: + resolution: {integrity: sha512-gZFEaaAL0YWEE55Z5GAggaDkwgpEmkL/ok9uUzq8exykNbXEDAuVd+H/ctXfQqW6VZGZb1aEyEMvjIamDv9igQ==} engines: {node: ^24.0.0 || ^22.12.0 || ^20.19.0} hasBin: true peerDependencies: - react: ~19.2.3 - react-dom: ~19.2.3 - react-server-dom-webpack: ~19.2.3 + react: ~19.2.4 + react-dom: ~19.2.4 + react-server-dom-webpack: ~19.2.4 watchpack@2.5.1: resolution: {integrity: sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==} @@ -4685,6 +4704,8 @@ snapshots: '@rolldown/pluginutils@1.0.0-beta.53': {} + '@rolldown/pluginutils@1.0.0-rc.5': {} + '@rollup/pluginutils@5.3.0(rollup@4.56.0)': dependencies: '@types/estree': 1.0.8 @@ -5304,6 +5325,23 @@ snapshots: optionalDependencies: react-server-dom-webpack: 19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) + '@vitejs/plugin-rsc@0.5.21(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.5 + es-module-lexer: 2.0.0 + estree-walker: 3.0.3 + magic-string: 0.30.21 + periscopic: 4.0.2 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + srvx: 0.11.12 + strip-literal: 3.1.0 + turbo-stream: 3.1.0 + vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + optionalDependencies: + react-server-dom-webpack: 19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) + '@wagmi/connectors@7.1.5(@wagmi/core@3.3.1(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.11.3(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.4.0(react@19.2.3))(viem@2.44.4(typescript@5.9.3)(zod@4.3.5)))(typescript@5.9.3)(viem@2.44.4(typescript@5.9.3)(zod@4.3.5))': dependencies: '@wagmi/core': 3.3.1(@tanstack/query-core@5.90.19)(@types/react@19.2.9)(ox@0.11.3(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(use-sync-external-store@1.6.0(react@19.2.3))(viem@2.44.4(typescript@5.9.3)(zod@4.3.5)) @@ -5848,7 +5886,7 @@ snapshots: dotenv@16.6.1: {} - dotenv@17.2.3: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: dependencies: @@ -7625,6 +7663,8 @@ snapshots: srvx@0.10.1: {} + srvx@0.11.12: {} + state-local@1.0.7: {} static-browser-server@1.0.3: @@ -7974,7 +8014,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vocs@https://pkg.pr.new/wevm/vocs@e5ad67e(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.2(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vocs@https://pkg.pr.new/vocs@728c0ca(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@base-ui/react': 1.1.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@codesandbox/sandpack-react': 2.20.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8050,7 +8090,7 @@ snapshots: optionalDependencies: mermaid: 11.12.2 vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - waku: 1.0.0-alpha.2(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + waku: 1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) transitivePeerDependencies: - '@cfworker/json-schema' - '@remix-run/react' @@ -8112,12 +8152,12 @@ snapshots: - ox - porto - waku@1.0.0-alpha.2(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): + waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@hono/node-server': 1.19.9(hono@4.11.5) '@vitejs/plugin-react': 5.1.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@vitejs/plugin-rsc': 0.5.16(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - dotenv: 17.2.3 + '@vitejs/plugin-rsc': 0.5.21(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + dotenv: 17.3.1 hono: 4.11.5 magic-string: 0.30.21 picocolors: 1.1.1 From 14323789186dc67c020057a45ae47e4cb47e9bf6 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:07:46 +0100 Subject: [PATCH 24/40] docs(sdks): pay fees with stablecoin examples (#20) --- .../payments/pay-fees-in-any-stablecoin.mdx | 339 +++++++++++++++++- 1 file changed, 336 insertions(+), 3 deletions(-) diff --git a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx index c3b3dd28..6822a151 100644 --- a/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx +++ b/src/pages/guide/payments/pay-fees-in-any-stablecoin.mdx @@ -70,6 +70,134 @@ const receipt = await client.token.transferSync({ }) ``` +
+ + + +:::code-group + +```rust [example.rs] +use alloy::{ + primitives::{address, U256}, + providers::Provider, + sol_types::SolCall, +}; +use tempo_alloy::{ + contracts::precompiles::ITIP20, primitives::transaction::Call, + rpc::TempoTransactionRequest, +}; + +mod provider; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let alpha_usd = address!("0x20c0000000000000000000000000000000000001"); + let beta_usd = address!("0x20c0000000000000000000000000000000000002"); + + let calls = vec![Call { + to: alpha_usd.into(), + input: ITIP20::transferCall { + to: address!("0x0000000000000000000000000000000000000000"), + amount: U256::from(100_000_000), + } + .abi_encode() + .into(), + value: U256::ZERO, + }]; + + let pending = provider + .send_transaction(TempoTransactionRequest { + calls, + fee_token: Some(beta_usd), // [!code ++] + ..Default::default() + }) + .await?; + + Ok(()) +} +``` + +```rust [provider.rs] +// [!include ~/snippets/rust-signer-provider.rs:setup] +``` + +::: + + + + + +
+ +```python +from pytempo import TempoTransaction +from pytempo.contracts import TIP20, ALPHA_USD, BETA_USD + +tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + fee_token=BETA_USD, # [!code ++] + calls=( + TIP20(ALPHA_USD).transfer( + to="0x0000000000000000000000000000000000000000", + amount=100_000_000, + ), + ), +) +``` + + + + + +
+ +```go +alphaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000001") +betaUSD := common.HexToAddress("0x20c0000000000000000000000000000000000002") + +tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(100_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetFeeToken(betaUSD). // [!code ++] + AddCall(alphaUSD, big.NewInt(0), buildTransferData(recipient, big.NewInt(100_000_000))). + Build() +``` + + + + + +
+ +```bash +$ cast send \ + 0x20c0000000000000000000000000000000000001 \ + "transfer(address,uint256)" \ + 0x0000000000000000000000000000000000000000 \ + 100000000 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY \ + --tempo.fee-token 0x20c0000000000000000000000000000000000002 # [!code ++] +``` + + + + + +
+ +:::info +The fee token for a given transaction cannot be set from Solidity — it is a transaction-level parameter handled by the signing SDK. However, you can configure a **default** fee token for an account using [`setUserToken`](#set-user-fee-token), which will apply to all future transactions unless explicitly overridden at submission. +::: + @@ -331,11 +459,49 @@ const receipt = await client.token.transferSync({ :::: - -# Recipes + + +:::info +For Rust integration, refer to the [Quick Snippet](#quick-snippet) above and the [Set user fee token](#set-user-fee-token) below. +::: + + + + + +:::info +For Python integration, refer to the [Quick Snippet](#quick-snippet) above and the [Set user fee token](#set-user-fee-token) below. +::: + + + + + +:::info +For Go integration, refer to the [Quick Snippet](#quick-snippet) above and the [Set user fee token](#set-user-fee-token) below. +::: + + + + + +:::info +For Cast integration, refer to the [Quick Snippet](#quick-snippet) above and the [Set user fee token](#set-user-fee-token) below. +::: + + + + + +:::info +For Solidity integration, refer to the [Quick Snippet](#quick-snippet) above and the [Set user fee token](#set-user-fee-token) below. +::: + + + -## Set user fee token +## Set user fee token You can also set a persistent default fee token for an account, so users don't need to specify `feeToken` on every transaction. Learn more about fee token preferences [here](/protocol/fees/spec-fee#fee-token-preferences). @@ -359,6 +525,173 @@ You can also set a persistent default fee token for an account, so users don't n // [!include ~/snippets/unformatted/fee.setUserToken.ts:viem] ``` + + + + +
+ +:::code-group + +```rust [example.rs] +use alloy::primitives::address; +use tempo_alloy::contracts::precompiles::IFeeManager; + +mod provider; + +#[tokio::main] +async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let fee_manager = IFeeManager::new( + address!("0xFEEc000000000000000000000000000000000000"), + &provider, + ); + + let receipt = fee_manager + .setUserToken( // [!code hl] + address!("0x20c0000000000000000000000000000000000001"), // [!code hl] + ) // [!code hl] + .send() + .await? + .get_receipt() + .await?; + + println!("Transaction hash: {:?}", receipt.transaction_hash); + + Ok(()) +} +``` + +```rust [provider.rs] +// [!include ~/snippets/rust-signer-provider.rs:setup] +``` + +::: + + + + + +
+ +:::code-group + +```python [example.py] +from pytempo import TempoTransaction +from pytempo.contracts import FeeManager, ALPHA_USD +from provider import w3, account + +tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + calls=( + FeeManager.set_user_token(ALPHA_USD), # [!code hl] + ), +) + +signed_tx = tx.sign(account.key.hex()) +tx_hash = w3.eth.send_raw_transaction(signed_tx.encode()) +``` + +```python [provider.py] +from web3 import Web3 +from eth_account import Account + +w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) +account = Account.from_key("0x...") +``` + +::: + + + + + +
+ +:::code-group + +```go [main.go] +package main + +import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" +) + +func main() { + sgn, _ := signer.NewSigner("0x...") + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, sgn.Address().Hex()) + + feeManager := common.HexToAddress("0xFEEc000000000000000000000000000000000000") + token := common.HexToAddress("0x20c0000000000000000000000000000000000001") + + // setUserToken(address) selector: 0xe7897444 + data := make([]byte, 36) // [!code hl] + data[0], data[1], data[2], data[3] = 0xe7, 0x89, 0x74, 0x44 // [!code hl] + copy(data[16:36], token.Bytes()) // [!code hl] + + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(100_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + AddCall(feeManager, big.NewInt(0), data). + Build() + + _ = transaction.SignTransaction(tx, sgn) + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Transaction hash: %s", txHash) +} +``` + +```go [provider.go] +// [!include ~/snippets/go-provider.go:setup] +``` + +::: + + + + + +
+ +```bash +$ cast send \ + 0xFEEc000000000000000000000000000000000000 \ + "setUserToken(address)" \ + 0x20c0000000000000000000000000000000000001 \ + --rpc-url $TEMPO_RPC_URL \ + --private-key $PRIVATE_KEY # [!code hl] +``` + + + + + +
+ +```solidity +import {StdPrecompiles} from "tempo-std/StdPrecompiles.sol"; + +StdPrecompiles.TIP_FEE_MANAGER.setUserToken(0x20c0000000000000000000000000000000000001); // [!code hl] +``` + From af4a8e4457f113c2e68f4172988d6822c6850a44 Mon Sep 17 00:00:00 2001 From: tmm Date: Mon, 16 Mar 2026 13:42:22 -0400 Subject: [PATCH 25/40] chore: bump vocs to 8b55a2c (#28) Amp-Thread-ID: https://ampcode.com/threads/T-019cf7ba-9f13-7756-aa25-35383710d245 --- package.json | 2 +- pnpm-lock.yaml | 46 ++++++---------------------------------------- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/package.json b/package.json index c5170de5..938d28cc 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "unplugin-auto-import": "^21.0.0", "unplugin-icons": "^23.0.1", "viem": "^2.44.4", - "vocs": "https://pkg.pr.new/vocs@728c0ca", + "vocs": "https://pkg.pr.new/vocs@8b55a2c", "wagmi": "3.4.1", "waku": "1.0.0-alpha.4", "zod": "^4.3.5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9a5659b0..abccd96d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,8 +89,8 @@ importers: specifier: ^2.44.4 version: 2.44.4(typescript@5.9.3)(zod@4.3.5) vocs: - specifier: https://pkg.pr.new/vocs@728c0ca - version: https://pkg.pr.new/vocs@728c0ca(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + specifier: https://pkg.pr.new/vocs@8b55a2c + version: https://pkg.pr.new/vocs@8b55a2c(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) wagmi: specifier: 3.4.1 version: 3.4.1(@tanstack/query-core@5.90.19)(@tanstack/react-query@5.90.19(react@19.2.3))(@types/react@19.2.9)(ox@0.11.3(typescript@5.9.3)(zod@4.3.5))(react@19.2.3)(typescript@5.9.3)(viem@2.44.4(typescript@5.9.3)(zod@4.3.5)) @@ -1481,17 +1481,6 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 - '@vitejs/plugin-rsc@0.5.16': - resolution: {integrity: sha512-BopxxopgDQjKIikc6VW1BeC1uRJgY0HOi2uJnF3L+J44oQVjmHIunHEaO85InW6FnFtaMpBeLQ/OJ/sbnwhiEA==} - peerDependencies: - react: '*' - react-dom: '*' - react-server-dom-webpack: '*' - vite: '*' - peerDependenciesMeta: - react-server-dom-webpack: - optional: true - '@vitejs/plugin-rsc@0.5.21': resolution: {integrity: sha512-uNayLT8IKvWoznvQyfwKuGiEFV28o7lxUDnw/Av36VCuGpDFZnMmvVCwR37gTvnSmnpul9V0tdJqY3tBKEaDqw==} peerDependencies: @@ -3455,11 +3444,6 @@ packages: resolution: {integrity: sha512-o2yiy7fYXK1HvzA8P6wwj8QSuwG3e/XcpWht/jIxkQX99c0SVPw0OXdLSV9fHASPiYB09HLA0uq8hokGydi/QA==} hasBin: true - srvx@0.10.1: - resolution: {integrity: sha512-A//xtfak4eESMWWydSRFUVvCTQbSwivnGCEf8YGPe2eHU0+Z6znfUTCPF0a7oV3sObSOcrXHlL6Bs9vVctfXdg==} - engines: {node: '>=20.16.0'} - hasBin: true - srvx@0.11.12: resolution: {integrity: sha512-AQfrGqntqVPXgP03pvBDN1KyevHC+KmYVqb8vVf4N+aomQqdhaZxjvoVp+AOm4u6x+GgNQY3MVzAUIn+TqwkOA==} engines: {node: '>=20.16.0'} @@ -3829,8 +3813,8 @@ packages: vite: optional: true - vocs@https://pkg.pr.new/vocs@728c0ca: - resolution: {tarball: https://pkg.pr.new/vocs@728c0ca} + vocs@https://pkg.pr.new/vocs@8b55a2c: + resolution: {tarball: https://pkg.pr.new/vocs@8b55a2c} version: 0.0.0 hasBin: true peerDependencies: @@ -5309,22 +5293,6 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitejs/plugin-rsc@0.5.16(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': - dependencies: - es-module-lexer: 2.0.0 - estree-walker: 3.0.3 - magic-string: 0.30.21 - periscopic: 4.0.2 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - srvx: 0.10.1 - strip-literal: 3.1.0 - turbo-stream: 3.1.0 - vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - optionalDependencies: - react-server-dom-webpack: 19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1) - '@vitejs/plugin-rsc@0.5.21(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@rolldown/pluginutils': 1.0.0-rc.5 @@ -7661,8 +7629,6 @@ snapshots: argparse: 2.0.1 nearley: 2.20.1 - srvx@0.10.1: {} - srvx@0.11.12: {} state-local@1.0.7: {} @@ -8014,7 +7980,7 @@ snapshots: optionalDependencies: vite: 7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) - vocs@https://pkg.pr.new/vocs@728c0ca(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + vocs@https://pkg.pr.new/vocs@8b55a2c(@types/react@19.2.9)(mermaid@11.12.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(rollup@4.56.0)(typescript@5.9.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))(waku@1.0.0-alpha.4(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): dependencies: '@base-ui/react': 1.1.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@codesandbox/sandpack-react': 2.20.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -8038,7 +8004,7 @@ snapshots: '@takumi-rs/image-response': 0.62.8 '@takumi-rs/wasm': 0.62.8 '@vitejs/plugin-react': 5.1.2(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) - '@vitejs/plugin-rsc': 0.5.16(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) + '@vitejs/plugin-rsc': 0.5.21(react-dom@19.2.3(react@19.2.3))(react-server-dom-webpack@19.2.3(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(webpack@5.104.1))(react@19.2.3)(vite@7.3.1(@types/node@25.0.10)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) cac: 6.7.14 cva: class-variance-authority@0.7.1 debug: 4.4.3 From 19be559728d3851d835fd92bef1ccc57fb734077 Mon Sep 17 00:00:00 2001 From: 0xrusowsky <90208954+0xrusowsky@users.noreply.github.com> Date: Mon, 16 Mar 2026 19:21:30 +0100 Subject: [PATCH 26/40] docs(sdks): fee sponsorship (#21) --- .../guide/payments/sponsor-user-fees.mdx | 288 ++++++++++++++---- 1 file changed, 223 insertions(+), 65 deletions(-) diff --git a/src/pages/guide/payments/sponsor-user-fees.mdx b/src/pages/guide/payments/sponsor-user-fees.mdx index 6360739a..dcdfe8f8 100644 --- a/src/pages/guide/payments/sponsor-user-fees.mdx +++ b/src/pages/guide/payments/sponsor-user-fees.mdx @@ -2,7 +2,7 @@ description: Enable gasless transactions by sponsoring fees for your users. Set up a fee payer service and improve UX by removing friction from payment flows. --- -import { Cards, Card } from 'vocs' +import { Cards, Card, Tabs, Tab } from 'vocs' import * as Demo from '../../../components/guides/Demo.tsx' import * as Step from '../../../components/guides/steps' import PublicTestnetSponsorTip from '../../../snippets/public-testnet-sponsor-tip.mdx' @@ -52,7 +52,7 @@ Use the `withFeePayer` transport provided by Viem ([link](https://viem.sh/tempo/ Now you can sponsor transactions by passing `feePayer: true` in the transaction parameters. For more details on how to send a transaction, see the [Send a payment](/guide/payments/send-a-payment) guide. :::info -You can also sponsor transactions with a local account. See the [recipe below](/guide/payments/sponsor-user-fees#local-account-sponsorship) for more details. +You can also build your own fee paying service. See [Build your own fee paying service](#build-your-own-fee-paying-service) below. ::: :::code-group @@ -108,7 +108,7 @@ function SendSponsoredPayment() { ::: -## Next Steps +### Next Steps Now that you've implemented fee sponsorship, you can: - Learn more about the [Tempo Transaction](/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details) type and fee payer signature details @@ -117,30 +117,229 @@ Now that you've implemented fee sponsorship, you can: :::: -## Recipes +## Build your own fee paying service -### Local account sponsorship +Instead of using the hosted fee payer service, you can build your own. -The example above uses a fee payer server to sign and sponsor transactions. If you want to sponsor transactions locally, you can easily do so by passing a local account to the `feePayer` parameter. +The sender must indicate sponsorship **before signing**, because the transaction's signing hash differs based on whether it will be sponsored: -```ts twoslash [client.ts] -// @noErrors -import { createClient, http } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { tempo } from 'viem/chains' - -const client = createClient({ - chain: tempo, - transport: http(), -}) - -const { receipt } = await client.token.transferSync({ - amount: parseUnits('10.5', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000000', - feePayer: privateKeyToAccount('0x...'), // [!code hl] -}) -``` +- **Sponsored transactions**: The `fee_token` field is omitted from the sender's signing payload, and a `0x00` marker is used. This allows the fee payer to choose the fee token. +- **Non-sponsored transactions**: The `fee_token` is included in the sender's signing payload. + +The SDKs handle this automatically. Here's how each SDK manages the dual-signing flow: + + + + + In Viem, pass a local account to the `feePayer` parameter to handle both signatures in your Node.js backend. + + ```ts twoslash + // @noErrors + import { createClient, http, parseUnits } from 'viem' + import { privateKeyToAccount } from 'viem/accounts' + import { tempoModerato } from 'viem/chains' + + const client = createClient({ + chain: tempoModerato, + transport: http(), + }) + + const senderAccount = privateKeyToAccount('0x...') + const feePayerAccount = privateKeyToAccount('0x...') // [!code hl] + + // When feePayer is an Account object, Viem: + // 1. Has the sender sign the transaction (with feeToken skipped) + // 2. Recovers the sender address from their signature + // 3. Has the fee payer sign a separate hash (includes sender + feeToken) + // 4. Serializes the dual-signed transaction + const { receipt } = await client.token.transferSync({ + account: senderAccount, + amount: parseUnits('10.5', 6), + to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', + token: '0x20c0000000000000000000000000000000000000', + feePayer: feePayerAccount, // [!code hl] + }) + ``` + + :::info + Alternatively, use `feePayer: true` with the [`withFeePayer`](https://viem.sh/tempo/transports/withFeePayer) transport to delegate signing to a remote fee payer service. See the [Steps section above](#configure-your-client-to-use-the-fee-payer-service) for setup. + ::: + + + + + + :::code-group + + ```rust [example.rs] + use alloy::{ + primitives::{address, U256}, + providers::Provider, + signers::{SignerSync, local::PrivateKeySigner}, + sol_types::SolCall, + }; + use tempo_alloy::{ + contracts::precompiles::ITIP20, primitives::transaction::Call, + rpc::TempoTransactionRequest, + }; + + mod provider; + + #[tokio::main] + async fn main() -> Result<(), Box> { + let provider = provider::get_provider().await?; + + let tx = TempoTransactionRequest { + calls: vec![Call { + to: address!("0x20c0000000000000000000000000000000000000").into(), + input: ITIP20::transferCall { + to: address!("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb"), + amount: U256::from(10_500_000), + } + .abi_encode() + .into(), + value: U256::ZERO, + }], + ..Default::default() + }; + + // Step 1: Build the transaction (sender signs, fee_token excluded from hash) + let mut tempo_tx = provider.fill(tx).await?.build_aa()?; + let sender_addr = provider.default_signer_address(); + + // Step 2: Compute the fee payer hash (includes sender address + fee_token) + let fee_payer_hash = tempo_tx.fee_payer_signature_hash(sender_addr); // [!code hl] + + // Step 3: Fee payer counter-signs + let fee_payer: PrivateKeySigner = "0x...".parse()?; // [!code hl] + tempo_tx.fee_payer_signature = Some(fee_payer.sign_hash_sync(&fee_payer_hash)?); // [!code hl] + + // Step 4: Broadcast + let pending = provider.send_transaction(tempo_tx).await?; + + println!("Transaction hash: {:?}", pending.tx_hash()); + + Ok(()) + } + ``` + + ```rust [provider.rs] + // [!include ~/snippets/rust-signer-provider.rs:setup] + ``` + + ::: + + + + + + :::code-group + + ```python [example.py] + from pytempo import TempoTransaction + from pytempo.contracts import TIP20 + from provider import w3, account + + TOKEN = "0x20c0000000000000000000000000000000000000" + + # Step 1: Sender creates and signs (fee_token excluded from signing hash) + tx = TempoTransaction.create( + chain_id=w3.eth.chain_id, + gas_limit=100_000, + max_fee_per_gas=w3.eth.gas_price * 2, + max_priority_fee_per_gas=w3.eth.gas_price, + nonce=w3.eth.get_transaction_count(account.address), + awaiting_fee_payer=True, # [!code hl] + calls=( + TIP20(TOKEN).transfer( + to="0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb", + amount=10_500_000, + ), + ), + ) + + signed_by_sender = tx.sign(account.key.hex()) + + # Step 2: Fee payer counter-signs (includes sender address + fee_token) + FEE_PAYER_KEY = "0x..." + fully_signed = signed_by_sender.sign(FEE_PAYER_KEY, for_fee_payer=True) # [!code hl] + + # Step 3: Broadcast + tx_hash = w3.eth.send_raw_transaction(fully_signed.encode()) + ``` + + ```python [provider.py] + from web3 import Web3 + from eth_account import Account + + w3 = Web3(Web3.HTTPProvider("https://rpc.presto.tempo.xyz")) + account = Account.from_key("0x...") + ``` + + ::: + + + + + + :::code-group + + ```go [main.go] + package main + + import ( + "context" + "log" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/tempoxyz/tempo-go/pkg/signer" + "github.com/tempoxyz/tempo-go/pkg/transaction" + ) + + func main() { + senderSigner, _ := signer.NewSigner("0x...") // sender key + feePayerSigner, _ := signer.NewSigner("0x...") // fee payer key + c := newClient() + ctx := context.Background() + + nonce, _ := c.GetTransactionCount(ctx, senderSigner.Address().Hex()) + + token := common.HexToAddress("0x20c0000000000000000000000000000000000000") + recipient := common.HexToAddress("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb") + + // Step 1: Sender creates tx and signs (fee_token excluded from hash) + tx := transaction.NewBuilder(big.NewInt(transaction.ChainIdModerato)). + SetNonce(nonce). + SetGas(100_000). + SetMaxFeePerGas(big.NewInt(25_000_000_000)). + SetMaxPriorityFeePerGas(big.NewInt(1_000_000_000)). + SetSponsored(true). // [!code hl] + AddCall(token, big.NewInt(0), buildTransferData(recipient, big.NewInt(10_500_000))). + Build() + + _ = transaction.SignTransaction(tx, senderSigner) + + // Step 2: Fee payer sets fee token and counter-signs + tx.FeeToken = common.HexToAddress("0x20c0000000000000000000000000000000000001") // [!code hl] + _ = transaction.AddFeePayerSignature(tx, feePayerSigner) // [!code hl] + + // Step 3: Broadcast + serialized, _ := transaction.Serialize(tx, nil) + txHash, _ := c.SendRawTransaction(ctx, serialized) + + log.Printf("Sponsored transaction hash: %s", txHash) + } + ``` + + ```go [provider.go] + // [!include ~/snippets/go-provider.go:setup] + ``` + + ::: + + + ## Best practices @@ -156,47 +355,6 @@ const { receipt } = await client.token.transferSync({ - **Balance checks**: Network verifies fee payer has sufficient balance - **Signature validation**: Both signatures must be valid -### Signing Hash Behavior - -When building fee-sponsored transactions, the sender must indicate sponsorship **before signing**. This is because the transaction's signing hash differs based on whether it will be sponsored: - -- **Sponsored transactions**: The `fee_token` field is omitted from the sender's signing payload, and a `0x00` marker is used. This allows the fee payer to choose the fee token. -- **Non-sponsored transactions**: The `fee_token` is included in the sender's signing payload. - -The `withFeePayer` transport and `feePayer: true` parameter handle this automatically. If you're building transactions manually with a local account as fee payer, the Viem SDK handles this for you: - -```ts twoslash -// @noErrors -import { createClient, http, parseUnits } from 'viem' -import { privateKeyToAccount } from 'viem/accounts' -import { tempo } from 'viem/chains' - -const client = createClient({ - chain: tempo, - transport: http(), -}) - -const senderAccount = privateKeyToAccount('0x...') -const feePayerAccount = privateKeyToAccount('0x...') - -// When feePayer is an Account object, Viem: -// 1. Has the sender sign the transaction (with feeToken) -// 2. Recovers the sender address from their signature -// 3. Has the fee payer sign a separate hash (includes sender + feeToken) -// 4. Serializes the dual-signed transaction -const { receipt } = await client.token.transferSync({ - account: senderAccount, - amount: parseUnits('10.5', 6), - to: '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEbb', - token: '0x20c0000000000000000000000000000000000000', - feePayer: feePayerAccount, // [!code hl] -}) -``` - -:::info -When using `feePayer: true` with the `withFeePayer` transport, the SDK deletes the `feeToken` from the transaction before signing, then sends it to the fee payer service which adds their signature. -::: - ## Learning Resources From 9c578fef52244c52084c0cd4a5d1ee32af27ab0c Mon Sep 17 00:00:00 2001 From: Brendan Ryan Date: Mon, 16 Mar 2026 13:43:31 -0700 Subject: [PATCH 27/40] feat: add interactive terminal demo to machine payments page (#30) --- src/components/TerminalDemo.tsx | 438 +++++++++++++++++++++ src/pages/_root.css | 21 + src/pages/guide/machine-payments/index.mdx | 11 +- 3 files changed, 469 insertions(+), 1 deletion(-) create mode 100644 src/components/TerminalDemo.tsx diff --git a/src/components/TerminalDemo.tsx b/src/components/TerminalDemo.tsx new file mode 100644 index 00000000..68440bcf --- /dev/null +++ b/src/components/TerminalDemo.tsx @@ -0,0 +1,438 @@ +"use client"; + +import { useEffect, useMemo, useRef, useState } from "react"; + +// --------------------------------------------------------------------------- +// Constants & helpers +// --------------------------------------------------------------------------- + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +function randomHex(bytes: number) { + const arr = crypto.getRandomValues(new Uint8Array(bytes)); + return `0x${Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("")}`; +} + +function randomAddress() { + return randomHex(20); +} + +function randomTxHash() { + return randomHex(32); +} + +// --------------------------------------------------------------------------- +// Tiny sub-components +// --------------------------------------------------------------------------- + +function Spinner() { + const [frame, setFrame] = useState(0); + useEffect(() => { + const timer = setInterval( + () => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), + 80, + ); + return () => clearInterval(timer); + }, []); + return ( + {SPINNER_FRAMES[frame]} + ); +} + +// biome-ignore format: contains unicode ✔︎ +function StepIcon({ spinning }: { spinning: boolean }) { + return ( + + {spinning ? ( + + ) : ( + ✔︎ + )} + + ); +} + +function BlankLine() { + return
; +} + +function TruncatedHex({ hash }: { hash: string }) { + return ( + <> + + {hash.slice(0, 6)}…{hash.slice(-4)} + + {hash} + + ); +} + +// --------------------------------------------------------------------------- +// Photo output +// --------------------------------------------------------------------------- + +function PhotoOutput({ url }: { url: string }) { + const [loaded, setLoaded] = useState(false); + + return ( +
+
+ {!loaded && ( +
+ )} + Generated setLoaded(true)} + className="absolute inset-0 w-full h-full object-cover" + style={{ + transition: "opacity 0.5s", + opacity: loaded ? 1 : 0, + }} + /> +
+
+ ); +} + +// --------------------------------------------------------------------------- +// Simulated charge flow +// --------------------------------------------------------------------------- + +function ChargeSteps({ + endpoint, + output, + address, + onDone, +}: { + endpoint: string; + output: string; + address: string; + onDone: () => void; +}) { + const txHash = useMemo(() => randomTxHash(), []); + const doneCalled = useRef(false); + + const steps = useMemo( + () => [ + { key: "wallet", delay: 600 }, + { key: "fund", delay: 1500 }, + { key: "req402", delay: 500 }, + { key: "pay", delay: 500 }, + { key: "req200", delay: 500 }, + ], + [], + ); + + const [step, setStep] = useState(0); + const currentKey = steps[step]?.key ?? "done"; + + const pastStep = (key: string) => { + const idx = steps.findIndex((s) => s.key === key); + return idx !== -1 && step > idx; + }; + const atOrPast = (key: string) => { + const idx = steps.findIndex((s) => s.key === key); + return idx !== -1 && step >= idx; + }; + const atStep = (key: string) => currentKey === key; + + useEffect(() => { + if (currentKey === "done") { + if (!doneCalled.current) { + doneCalled.current = true; + onDone(); + } + return; + } + const delay = steps[step].delay; + const timer = setTimeout(() => setStep((s) => s + 1), delay); + return () => clearTimeout(timer); + }, [step, currentKey, steps, onDone]); + + return ( +
+ + {atOrPast("wallet") && ( +

+ Create a wallet{" "} + {" "} + + + +

+ )} + {atOrPast("fund") && ( +

+ Add test funds{" "} + {" "} + 100 USD +

+ )} + {/* biome-ignore format: contains unicode → */} + {atOrPast("req402") && ( +

+ Call {endpoint} + {pastStep("req402") && ( + <> + {" "}→ 402{" "} + (payment required) + + )} +

+ )} + {atOrPast("pay") && ( +

+ Fulfill payment + {pastStep("pay") && ( + <> + {" "} + {" "} + + {txHash.slice(0, 6)}…{txHash.slice(-4)} + + + )} +

+ )} + {/* biome-ignore format: contains unicode → */} + {atOrPast("req200") && ( +

+ Call {endpoint} + {pastStep("req200") && ( + <> + {" "}→ 200{" "} + (success) + + )} +

+ )} + {pastStep("req200") && ( + <> + + + + + )} +
+ ); +} + +// --------------------------------------------------------------------------- +// CSS triangle for the "Run demo" button +// --------------------------------------------------------------------------- + +function CssTriangle() { + return ( + + ); +} + +// --------------------------------------------------------------------------- +// Main exported component +// --------------------------------------------------------------------------- + +export function TerminalDemo({ className }: { className?: string }) { + const [started, setStarted] = useState(false); + const [done, setDone] = useState(false); + const [key, setKey] = useState(0); + const [address] = useState(() => randomAddress()); + const [photoSeed, setPhotoSeed] = useState(() => Math.random().toString(36).slice(2)); + const photoUrl = `https://picsum.photos/seed/${photoSeed}/400/400`; + + const scrollRef = useRef(null); + const contentRef = useRef(null); + + // Auto-scroll when content grows + useEffect(() => { + const scrollEl = scrollRef.current; + const contentEl = contentRef.current; + if (!scrollEl || !contentEl) return; + const observer = new ResizeObserver(() => { + scrollEl.scrollTo({ + top: scrollEl.scrollHeight - scrollEl.clientHeight, + behavior: "smooth", + }); + }); + observer.observe(contentEl); + return () => observer.disconnect(); + }, []); + + const restart = () => { + setStarted(false); + setDone(false); + setPhotoSeed(Math.random().toString(36).slice(2)); + setKey((k) => k + 1); + }; + + return ( +
+
+ {/* Title bar */} +
+ + + + + +
+ + {/* Terminal body */} +
+
+
+ + {!started && ( +
+ + +

+ Press Enter or click to start +

+
+ )} + + {started && ( + setDone(true)} + /> + )} + + {done && ( + + )} +
+
+
+
+ ); +} diff --git a/src/pages/_root.css b/src/pages/_root.css index 0d130ff5..ceb612e6 100644 --- a/src/pages/_root.css +++ b/src/pages/_root.css @@ -238,3 +238,24 @@ .data-v-mermaid-container { min-height: 200px; } + +/* --------------------------------------------------------------------------- + * Terminal theme — scoped color variables for the embedded terminal demo. + * --------------------------------------------------------------------------- */ + +.terminal-theme { + --term-bg1: light-dark(oklch(0.94 0 0), oklch(0.14 0 0)); + --term-bg2: light-dark(oklch(1 0 0), oklch(0.15 0 0)); + --term-gray1: light-dark(oklch(0.955 0 0), oklch(0.18 0 0)); + --term-gray2: light-dark(oklch(0.933 0 0), oklch(0.2 0 0)); + --term-gray3: light-dark(oklch(0.933 0 0), oklch(0.22 0 0)); + --term-gray4: light-dark(oklch(0.933 0 0), oklch(0.27 0 0)); + --term-gray5: light-dark(oklch(0.549 0 0), oklch(0.45 0 0)); + --term-gray6: light-dark(oklch(0.45 0 0), oklch(0.58 0 0)); + --term-gray10: light-dark(oklch(0.21 0 0), oklch(0.9 0 0)); + --term-blue9: light-dark(hsl(211 100% 42%), hsl(210 100% 70%)); + --term-amber9: light-dark(hsl(30 100% 32%), hsl(39 95% 58%)); + --term-green9: light-dark(hsl(133 50% 32%), hsl(131 50% 60%)); + --term-orange9: light-dark(hsl(25 95% 45%), hsl(25 95% 63%)); + --term-pink9: light-dark(hsl(336 65% 45%), hsl(341 90% 72%)); +} diff --git a/src/pages/guide/machine-payments/index.mdx b/src/pages/guide/machine-payments/index.mdx index f1b34c6a..9b8ef30e 100644 --- a/src/pages/guide/machine-payments/index.mdx +++ b/src/pages/guide/machine-payments/index.mdx @@ -5,11 +5,20 @@ description: Charge for APIs, MCP tools, and digital content using the Machine P import { Card, Cards } from 'vocs' import { MermaidDiagram } from '../../../components/MermaidDiagram' +import { TerminalDemo } from '../../../components/TerminalDemo' # Make Machine Payments The [Machine Payments Protocol](https://mpp.dev) (MPP) adds inline payments to any HTTP endpoint. Clients — apps, agents, or humans — pay as part of their request, and the server verifies payment before returning the response. +## Try it out + +See the full payment flow in action. The terminal creates an ephemeral wallet, funds it with testnet USDC, and makes a paid request to fetch a photo. + +
+ +
+ ## Payment flow A client requests a paid resource, the server responds with `402` and a `Challenge` describing the price. The client pays, retries with a `Credential` transaction, and the server returns the resource with a `Receipt`. @@ -93,7 +102,7 @@ Two [intents](https://mpp.dev/protocol#payment-intents) are available on Tempo: |-----|---------|---------| | TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx viem` | | Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install pympp` | -| Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp-rs` | +| Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp` | See the [full SDK documentation](https://mpp.dev/sdk) for API reference and advanced usage. From 7441b865c464ff2d83086cec8807d54a50eed28e Mon Sep 17 00:00:00 2001 From: Uddhav Date: Mon, 16 Mar 2026 18:09:56 -0400 Subject: [PATCH 28/40] docs(nav): reorganize build and quickstart sections (#32) Update sidebar structure to surface Building with AI under Changelog Rename, move up Build on Tempo --- src/pages/guide/use-accounts/add-funds.mdx | 2 +- .../learn/use-cases/agentic-commerce.mdx | 2 +- .../learn/use-cases/embedded-finance.mdx | 2 +- src/pages/learn/use-cases/global-payouts.mdx | 2 +- .../learn/use-cases/microtransactions.mdx | 2 +- src/pages/learn/use-cases/payroll.mdx | 2 +- src/pages/learn/use-cases/remittances.mdx | 2 +- .../learn/use-cases/tokenized-deposits.mdx | 2 +- src/pages/quickstart/integrate-tempo.mdx | 4 +- vocs.config.ts | 88 +++++++++---------- 10 files changed, 54 insertions(+), 54 deletions(-) diff --git a/src/pages/guide/use-accounts/add-funds.mdx b/src/pages/guide/use-accounts/add-funds.mdx index 34343329..f1465435 100644 --- a/src/pages/guide/use-accounts/add-funds.mdx +++ b/src/pages/guide/use-accounts/add-funds.mdx @@ -10,7 +10,7 @@ import { Tabs, Tab } from 'vocs' # Add Funds to Your Balance -Get test tokens to start building on Tempo testnet. +Get test tokens to build on Tempo testnet. diff --git a/src/pages/learn/use-cases/agentic-commerce.mdx b/src/pages/learn/use-cases/agentic-commerce.mdx index 26e51ec7..9e9c0653 100644 --- a/src/pages/learn/use-cases/agentic-commerce.mdx +++ b/src/pages/learn/use-cases/agentic-commerce.mdx @@ -53,7 +53,7 @@ If you are architecting payment flows for autonomous systems, we can help you un title="Get in Touch" /> Date: Tue, 17 Mar 2026 13:28:36 +1300 Subject: [PATCH 29/40] docs: getting funds page, update building with ai (#35) * add getting funds page, update building with ai cards Amp-Thread-ID: https://ampcode.com/threads/T-019cf8df-2af2-75ef-96cd-c899b6d4230a Co-authored-by: Amp * up --------- Co-authored-by: Amp --- public/learn/wallet-add-funds.png | Bin 0 -> 228083 bytes src/pages/guide/building-with-ai.mdx | 152 +++++++++++++++++---------- src/pages/guide/getting-funds.mdx | 64 +++++++++++ vocs.config.ts | 4 + 4 files changed, 167 insertions(+), 53 deletions(-) create mode 100644 public/learn/wallet-add-funds.png create mode 100644 src/pages/guide/getting-funds.mdx diff --git a/public/learn/wallet-add-funds.png b/public/learn/wallet-add-funds.png new file mode 100644 index 0000000000000000000000000000000000000000..2e82115fee9ebeb1ac646681680fc15a040b92b3 GIT binary patch literal 228083 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�HN64v=w0T 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

&2mu@z(4m7-X5_n$zu zB^n$k$Z9Vtz1TBCCeOh^nlFw!hdT{0Py`16zq*G}Rb2(P0bNk19%R{Pd9SFGXzBgl zH$i6aqj5xeOFH0zT9w{IAS-ybZU!(7Tb$+`Gd5AZGg*?^jgwv(J|T6QM(Oswv=BY% zv%)?^kfFLADL--Mwuht_ZIS;%91H4Tuk*cwn^rCxwrU02=~$x3f7uvE#;z1HTJ`Sp zbe8^U-tUyag>D&HM%DwkNf?yn=0!I+7`#UTNDbePl{tQFAN?&2ynXwgu7rsmIYCaF zY|&v{(;pgtfn{a%JRV&fRLS5z;HMp=ynprMHwc_0*Saa+wgZv7umG%0DsPYz(QR%6 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 + + + + +## Tempo Chain + +### Skills + +Install the [Tempo Skills](https://github.com/tempoxyz/agent-skills) to give AI coding agents (Amp, Claude Code, etc.) access to Tempo documentation, source code via MCP, and examples. + +Check out [`tempoxyz/agent-skills`](https://github.com/tempoxyz/agent-skills) for more info on available skills. + +#### Installation -## llms.txt +```bash +npx skills add tempoxyz/agent-skills +``` + +Or manually: + +```bash +$ git clone https://github.com/tempoxyz/agent-skills.git +$ cp -r agent-skills/skills/tempo ~/.config/agents/skills/ +``` + +Or add to your project's [`.agents/skills/`](https://github.com/tempoxyz/agent-skills/tree/main/skills) directory for project-specific access. + +#### Usage + +Once installed, the skill is automatically available. The agent will use it when relevant tasks are detected. + +**Examples:** + +``` +How do I create a TIP-20 stablecoin? +``` +``` +Show me how fee sponsorship works in Viem +``` +``` +Search the Tempo source for transaction validation +``` + +### llms.txt The docs automatically generate [`llms.txt`](https://llmstxt.org/) files for LLM consumption: @@ -16,11 +74,11 @@ The docs automatically generate [`llms.txt`](https://llmstxt.org/) files for LLM These files are generated at build time and served at the root of the site. -## Markdown Rendering for AI Agents +### Markdown Rendering for AI Agents AI user agents are automatically detected and served raw Markdown instead of rendered HTML. This provides better token efficiency and easier parsing for LLMs. -### Supported AI Agents +#### Supported AI Agents Popular user agents that are automatically detected: @@ -30,7 +88,7 @@ Popular user agents that are automatically detected: See the [full list of supported agents](https://github.com/wevm/vocs/blob/next/src/waku/internal/middleware/md-router.ts#L3) for the most up-to-date list. -### Manual Markdown Access +#### Manual Markdown Access Any page can be accessed as Markdown by appending `.md` to the URL: @@ -40,7 +98,7 @@ https://docs.tempo.xyz/quickstart/integrate-tempo.md This is useful for copying documentation into AI conversations or for custom integrations. -## Ask AI Menu +### Ask AI Menu The docs include a built-in "Ask AI" menu (⌘I / Ctrl+I) that provides: @@ -49,7 +107,7 @@ The docs include a built-in "Ask AI" menu (⌘I / Ctrl+I) - **View as Markdown**: Opens the raw Markdown version of the current page - **Copy MCP URL**: Copies the MCP server URL for AI assistant configuration -## MCP Server +### MCP Server The docs include a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that allows AI assistants to navigate documentation and source code programmatically. @@ -65,7 +123,7 @@ amp mcp add tempo https://docs.tempo.xyz/api/mcp ``` ::: -### Connecting AI Assistants +#### Connecting AI Assistants Configure your AI assistant to connect to the MCP server: @@ -79,68 +137,56 @@ Configure your AI assistant to connect to the MCP server: } ``` -### Available Tools - -The MCP server exposes these tools to AI assistants: +## Tempo Wallet -| Tool | Description | -| --- | --- | -| `list_pages` | List all documentation pages with their paths | -| `read_page` | Read the content of a specific documentation page | -| `search_docs` | Search documentation for a query string | +The [Tempo Wallet](https://wallet.tempo.xyz) distributes a CLI with built in spend controls and service discovery. Agents can use `tempo wallet` to pay for powerful new capabilities on demand. -### Source Code Navigation + + -The MCP server also provides access to source code repositories: +Paste this into your agent to set up Tempo Wallet with your agent: -| Tool | Description | -| --- | --- | -| `list_sources` | List available source code repositories | -| `list_source_files` | List files in a directory | -| `read_source_file` | Read a source code file | -| `get_file_tree` | Get a recursive file tree | -| `search_source` | Search source code for a pattern | -Available source repositories: +``` +Read https://wallet.tempo.xyz/SKILL.md and set up a Tempo Wallet +``` -- [`tempoxyz/tempo`](https://github.com/tempoxyz/tempo) – Tempo node implementation -- [`tempoxyz/tempo-ts`](https://github.com/tempoxyz/tempo-ts) – TypeScript SDK -- [`paradigmxyz/reth`](https://github.com/paradigmxyz/reth) – Reth Ethereum client -- [`foundry-rs/foundry`](https://github.com/foundry-rs/foundry) – Foundry development toolkit -- [`wevm/viem`](https://github.com/wevm/viem) – TypeScript interface for Ethereum -- [`wevm/wagmi`](https://github.com/wevm/wagmi) – React hooks for Ethereum + + +::::steps -## Agent Skills +#### Install the CLI -Install the [Tempo Agent Skills](https://github.com/tempoxyz/agent-skills) to give AI coding agents (Amp, Claude Code, etc.) access to Tempo documentation, source code via MCP, and examples. Check out [`tempoxyz/agent-skills`](https://github.com/tempoxyz/agent-skills) for more info on available skills. +```bash +curl -fsSL https://tempo.xyz/install | bash +``` -### Installation +#### Connect your wallet ```bash -npx skills add tempoxyz/agent-skills +tempo wallet login ``` -Or manually: +#### Verify setup ```bash -$ git clone https://github.com/tempoxyz/agent-skills.git -$ cp -r agent-skills/skills/tempo ~/.config/agents/skills/ +tempo wallet whoami ``` -Or add to your project's [`.agents/skills/`](https://github.com/tempoxyz/agent-skills/tree/main/skills) directory for project-specific access. - -### Usage +#### List available services -Once installed, the skill is automatically available. The agent will use it when relevant tasks are detected. +```bash +tempo wallet services +``` -**Examples:** +#### Make a paid request +```bash +tempo request -X POST \ + --json '{"prompt": "a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` -How do I create a TIP-20 stablecoin? -``` -``` -Show me how fee sponsorship works in Viem -``` -``` -Search the Tempo source for transaction validation -``` + +:::: + + \ No newline at end of file diff --git a/src/pages/guide/getting-funds.mdx b/src/pages/guide/getting-funds.mdx new file mode 100644 index 00000000..ed00a878 --- /dev/null +++ b/src/pages/guide/getting-funds.mdx @@ -0,0 +1,64 @@ +--- +title: Getting Funds on Tempo +description: Bridge assets to Tempo via LayerZero, Squid, or Relay, onramp through the Tempo Wallet, or get testnet funds from the faucet. +--- + +import { Cards, Card } from 'vocs' + +# Getting Funds on Tempo + +There are several ways to get funds on Tempo: + + + + + + + +## Bridge + +Bridge assets from other chains to Tempo using one of the supported bridges: + +- **[LayerZero](https://layerzero.network/)**: Cross-chain messaging protocol supporting asset transfers to Tempo. +- **[Squid](https://app.squidrouter.com/)**: Cross-chain swaps and bridging powered by Axelar, with Tempo support. +- **[Relay](https://relay.link/)**: Instant bridging with low fees across supported chains including Tempo. + +## Tempo Wallet + +The [Tempo Wallet](https://wallet.tempo.xyz) supports onramping funds directly into your Tempo account. + +### Via the Web + +1. Sign up or login to [Tempo Wallet](https://wallet.tempo.xyz) using your Passkey. +2. Press the "Add funds" button on the home dashboard, and choose your preferred option: + + + +### Via an Agent + +Paste this into your agent to set up a Tempo Wallet and fund it: + +``` +Read https://wallet.tempo.xyz/SKILL.md and fund my Tempo Wallet +``` + +## Testnet Funds + +To get testnet tokens for development, use the [Tempo Faucet](/quickstart/faucet). + +The faucet will drip you `pathUSD`, `alphaUSD`, `betaUSD`, and `thetaUSD` test stablecoins. diff --git a/vocs.config.ts b/vocs.config.ts index 4d2ce94f..4e840c56 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -78,6 +78,10 @@ export default defineConfig({ { text: 'Build on Tempo', items: [ + { + text: 'Getting Funds on Tempo', + link: '/guide/getting-funds', + }, { text: 'Use Tempo Transactions', link: '/guide/tempo-transaction', From 2135d2665c1feb7c2246234ed102ea94d79ea82e Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 16 Mar 2026 21:13:37 -0500 Subject: [PATCH 30/40] docs: add Tempo Wallet CLI docs section (#36) Amp-Thread-ID: https://ampcode.com/threads/T-019cf986-a283-7649-8612-6b9cd5d0f478 Co-authored-by: Amp --- src/pages/wallet/index.mdx | 104 +++++++++++++++++++++ src/pages/wallet/recipes.mdx | 134 +++++++++++++++++++++++++++ src/pages/wallet/reference.mdx | 84 +++++++++++++++++ src/pages/wallet/troubleshooting.mdx | 99 ++++++++++++++++++++ src/pages/wallet/use-with-agents.mdx | 36 +++++++ vocs.config.ts | 26 ++++++ 6 files changed, 483 insertions(+) create mode 100644 src/pages/wallet/index.mdx create mode 100644 src/pages/wallet/recipes.mdx create mode 100644 src/pages/wallet/reference.mdx create mode 100644 src/pages/wallet/troubleshooting.mdx create mode 100644 src/pages/wallet/use-with-agents.mdx diff --git a/src/pages/wallet/index.mdx b/src/pages/wallet/index.mdx new file mode 100644 index 00000000..07171863 --- /dev/null +++ b/src/pages/wallet/index.mdx @@ -0,0 +1,104 @@ +--- +title: Tempo Wallet CLI +description: Learn how to use Tempo Wallet CLI and Tempo Request from your terminal, with quick onboarding guides and command references for daily workflows. +--- + +import { Cards, Card, Tab, Tabs } from 'vocs' + +# Tempo Wallet CLI + +Login and manage your Tempo Wallet from your terminal, with built-in [MPP](https://mpp.dev) support for service discovery and paid API requests. + +## Quickstart + + + + +Paste this into your agent to set up Tempo Wallet: + +
+ +``` +Read https://wallet.tempo.xyz/SKILL.md and set up tempo +``` + +
+ + +::::steps + +#### Install the CLI + +```bash +curl -fsSL https://tempo.xyz/install | bash +``` + +#### Connect your wallet + +```bash +tempo wallet login +``` + +#### Verify setup + +```bash +tempo wallet whoami +``` + +Expected result: wallet information is returned and you can continue to service discovery. + +#### List available services + +```bash +tempo wallet services --search ai +``` + +#### Make a paid request + +```bash +tempo request -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev +``` + +#### Preview cost before running a new endpoint + +```bash +tempo request --dry-run -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev +``` + +:::: + + +
+ +## Next Steps + + + + + + + diff --git a/src/pages/wallet/recipes.mdx b/src/pages/wallet/recipes.mdx new file mode 100644 index 00000000..40ac24ed --- /dev/null +++ b/src/pages/wallet/recipes.mdx @@ -0,0 +1,134 @@ +--- +title: Tempo Wallet CLI Recipes +description: Use practical Tempo Wallet CLI recipes for service discovery, paid requests, session management, and funding or transfers. +--- + +# Recipes + +## Core Flow + +```bash +tempo wallet -t whoami +tempo wallet services --search ai +tempo wallet services +tempo request --dry-run --json '{"input":"hello"}' +tempo request --json '{"input":"hello"}' +``` + +## Wallet Operations + +```bash +# Wallet readiness and balances +tempo wallet whoami + +# Key and spending-limit state +tempo wallet keys + +# Fund wallet +tempo wallet fund + +# Transfer tokens +tempo wallet transfer +``` + +For machine-readable output: + +```bash +tempo wallet -t whoami +tempo wallet -t keys +``` + +## Service Discovery + +```bash +# Search services by keyword +tempo wallet services --search ai + +# Inspect one service to see exact endpoints +tempo wallet services +``` + +Tip: copy endpoint URL, method, and payload shape directly from service details. + +## Request Execution + +Preview before paying: + +```bash +tempo request --dry-run --json '{"input":"hello"}' +``` + +Execute paid request: + +```bash +tempo request --json '{"input":"hello"}' +``` + +## Session Management + +```bash +# List sessions +tempo wallet sessions list + +# Reconcile local state against on-chain state +tempo wallet sessions sync + +# Preview close operations first +tempo wallet sessions close --dry-run --all + +# Close orphaned sessions +tempo wallet sessions close --orphaned +``` + +## Agent and Script Mode + +Use TOON output (`-t`) when command output is consumed by agents or scripts. + +```bash +tempo wallet -t whoami +tempo wallet -t services --search ai +tempo wallet -t services +``` + +## Failure Recovery Shortcuts + +```bash +# Wallet not ready / auth missing +tempo wallet login +tempo wallet -t whoami + +# Suspected key issue +tempo wallet logout --yes +tempo wallet login +tempo wallet keys + +# Request failing due to payload/path mismatch +tempo wallet services + +# Insufficient funds +tempo wallet fund +``` + +If issues persist, continue with [Troubleshooting](/wallet/troubleshooting). + +## End-to-End Script Pattern + +```bash +# 1) Ensure wallet is ready +tempo wallet -t whoami + +# 2) Discover service details +tempo wallet -t services --search ai + +# 3) Preview cost +tempo request --dry-run --json '{"input":"hello"}' + +# 4) Execute +tempo request --json '{"input":"hello"}' +``` + +## See Also + +1. [Reference](/wallet/reference) +2. [Troubleshooting](/wallet/troubleshooting) +3. [Use with Agents](/wallet/use-with-agents) diff --git a/src/pages/wallet/reference.mdx b/src/pages/wallet/reference.mdx new file mode 100644 index 00000000..6ed2ce0a --- /dev/null +++ b/src/pages/wallet/reference.mdx @@ -0,0 +1,84 @@ +--- +title: Tempo Wallet CLI Reference +description: Command reference for tempo wallet and tempo request, including the most-used commands and flags for interactive and script-based usage. +--- + +# Reference + +## `tempo wallet` Commands + +| Command | Purpose | +| --- | --- | +| `tempo wallet login` | Connect or create your wallet via browser auth | +| `tempo wallet logout` | Disconnect your wallet | +| `tempo wallet whoami` | Show readiness, address, balances, and key state | +| `tempo wallet keys` | List keys and spending limits | +| `tempo wallet fund` | Fund wallet (faucet/bridge flow depends on network) | +| `tempo wallet transfer ` | Transfer tokens to another address | +| `tempo wallet services` | List available services | +| `tempo wallet services ` | Show service details and endpoints | +| `tempo wallet sessions list` | List payment sessions | +| `tempo wallet sessions sync` | Reconcile local sessions with on-chain state | +| `tempo wallet sessions close ...` | Close sessions by target or in bulk | +| `tempo wallet mpp-sign` | Sign an MPP payment challenge | + +### `tempo wallet` Mini Recipe + +```bash +tempo wallet whoami +tempo wallet keys +tempo wallet services --search ai +tempo wallet services +``` + +## `tempo request` Core Usage + +| Command | Purpose | +| --- | --- | +| `tempo request ` | Make an HTTP request with automatic payment handling | +| `tempo request --dry-run ` | Preview cost without executing payment | +| `tempo request --json '{...}'` | Send a JSON body | +| `tempo request -H 'Header: Value'` | Add custom headers | + +### `tempo request` Mini Recipe + +```bash +tempo request --dry-run -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev + +tempo request -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev +``` + +## Common Flags + +| Flag | Works With | Purpose | +| --- | --- | --- | +| `-t` | `tempo wallet`, `tempo request` | TOON output (`--toon-output`): compact machine-readable output for scripts and agents | +| `--dry-run` | `tempo request`, `tempo wallet fund`, `tempo wallet sessions close` | Preview actions without executing | +| `--help` | all commands | Show full command/flag documentation | +| `--describe` | supported commands | Output command schema for programmatic tooling | + +## Quick Examples + +```bash +tempo wallet -t whoami +tempo wallet services --search ai +tempo wallet sessions close --dry-run --orphaned + +tempo request --dry-run https://openrouter.mpp.tempo.xyz/v1/chat/completions \ + --json '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Ping"}]}' +``` + +## Canonical Sources + +1. Wallet extension repo: [`tempoxyz/wallet`](https://github.com/tempoxyz/wallet) +2. Full CLI help: `tempo wallet --help` and `tempo request --help` + +## See Also + +1. [Recipes](/wallet/recipes) +2. [Use with Agents](/wallet/use-with-agents) +3. [Troubleshooting](/wallet/troubleshooting) diff --git a/src/pages/wallet/troubleshooting.mdx b/src/pages/wallet/troubleshooting.mdx new file mode 100644 index 00000000..582956a7 --- /dev/null +++ b/src/pages/wallet/troubleshooting.mdx @@ -0,0 +1,99 @@ +--- +title: Tempo Wallet CLI Troubleshooting +description: Troubleshoot common Tempo Wallet CLI issues including missing installs, login readiness failures, key provisioning errors, and paid request failures. +--- + +# Troubleshooting + +## Quick Diagnosis + +| Symptom | Run Next | +| --- | --- | +| `tempo: command not found` | Run the install command in the section below, then run `tempo --help` | +| `ready=false` | `tempo wallet login` then `tempo wallet -t whoami` | +| `No wallet configured` | `tempo wallet login` | +| `access key does not exist` | `tempo wallet logout --yes && tempo wallet login` | +| Request schema / `422` | `tempo wallet services ` and validate request body | +| Insufficient funds | `tempo wallet -t whoami` then `tempo wallet fund` | +| Session close confusion | `tempo wallet sessions close --dry-run --all` | + +## `tempo: command not found` + +Install Tempo CLI and retry: + +```bash +curl -fsSL https://tempo.xyz/install | bash +tempo --help +``` + +## Wallet Not Ready (`ready=false`) + +Run login and check status again: + +```bash +tempo wallet login +tempo wallet -t whoami +``` + +## `No wallet configured` or `Run 'tempo wallet login'` + +Your local auth/session is missing. + +```bash +tempo wallet login +tempo wallet whoami +``` + +## `Key is not provisioned` / `access key does not exist` + +Refresh auth state and reprovision: + +```bash +tempo wallet logout --yes +tempo wallet login +tempo wallet keys +``` + +## Request Fails With Schema / 422 Errors + +Usually this means body fields are wrong for the selected provider endpoint. + +```bash +tempo wallet services --search +tempo wallet services +``` + +Copy the exact method/path and expected request schema from service details. + +## Request Fails Due to Insufficient Funds + +Check wallet and fund: + +```bash +tempo wallet -t whoami +tempo wallet fund +``` + +## Session Close Behavior Is Unclear + +Preview before closing: + +```bash +tempo wallet sessions list +tempo wallet sessions close --dry-run --all +tempo wallet sessions close --dry-run --orphaned +``` + +Then execute the intended close command. + +## Still Stuck? + +1. Re-run with `--help` to confirm current flags and usage. +2. Use `-t` output (`tempo wallet -t ...`) for clearer machine-readable diagnostics. +3. Cross-check command behavior against [`tempoxyz/wallet`](https://github.com/tempoxyz/wallet). + +## See Also + +1. [Recipes](/wallet/recipes) +2. [Reference](/wallet/reference) +3. [Use with Agents](/wallet/use-with-agents) diff --git a/src/pages/wallet/use-with-agents.mdx b/src/pages/wallet/use-with-agents.mdx new file mode 100644 index 00000000..643fdf62 --- /dev/null +++ b/src/pages/wallet/use-with-agents.mdx @@ -0,0 +1,36 @@ +--- +title: Use Tempo Wallet CLI with Agents +description: Connect Tempo Wallet CLI to your agent and understand the built-in features that make agent-driven paid requests reliable and safe. +--- + +# Use with Agents + +## Quickstart + +Paste this into your agent to set up Tempo Wallet: + + + +``` +Read https://wallet.tempo.xyz/SKILL.md and set up tempo +``` + +## Auto-installed Skills + +When you run the setup prompt, your agent installs Tempo's built-in skills automatically — no manual skill wiring required. + +- `tempo-wallet`: gives your agent wallet-aware capabilities like readiness checks, balances, service discovery, and session/funding actions. +- `tempo-request`: gives your agent paid HTTP request capabilities with payment preview (`--dry-run`) and execution support. + +This works in supported skill-enabled agents including **Claude Code**, **Amp**, **Codex**, and similar environments. + +## Agent-Ready by Design + +1. **TOON output mode (`-t` / `--toon-output`)** gives compact, machine-readable, token-efficient output so agent tooling can parse command responses reliably. +2. **Built-in service discovery** (`tempo wallet services`) lets agents search providers, inspect endpoint details, and use verified method/path metadata instead of guessing URLs or payload shapes. +3. **`--dry-run` payment previews** let agents validate endpoint reachability, request shape, and expected payment cost before committing funds, which reduces failed paid calls in multi-step workflows. +4. **Scoped access keys and spend controls** let you enforce spending limits per key so agents can only spend within defined budgets, which contains risk if a prompt, endpoint choice, or loop goes wrong. + +## Troubleshooting + +If agent runs fail, continue with [Troubleshooting](/wallet/troubleshooting). diff --git a/vocs.config.ts b/vocs.config.ts index 4e840c56..c4dbf3e0 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -478,6 +478,32 @@ export default defineConfig({ }, ], }, + { + text: 'Tempo Wallet CLI', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/wallet', + }, + { + text: 'Use with Agents', + link: '/wallet/use-with-agents', + }, + { + text: 'Recipes', + link: '/wallet/recipes', + }, + { + text: 'Reference', + link: '/wallet/reference', + }, + { + text: 'Troubleshooting', + link: '/wallet/troubleshooting', + }, + ], + }, { text: 'Tempo SDKs', collapsed: true, From 5a96253e5bfed440532c91c7eb5ea4f3f7ae566b Mon Sep 17 00:00:00 2001 From: jxom <7336481+jxom@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:53:41 +1300 Subject: [PATCH 31/40] docs: add SKILL.md and AI card to homepage (#37) * Add SKILL.md and AI card to homepage Amp-Thread-ID: https://ampcode.com/threads/T-019cfa0d-e023-71d1-a478-bd2f2ce4de3f Co-authored-by: Amp * Rename building-with-ai to using-tempo-with-ai and update config Amp-Thread-ID: https://ampcode.com/threads/T-019cfa0d-e023-71d1-a478-bd2f2ce4de3f Co-authored-by: Amp --------- Co-authored-by: Amp --- SKILL.md | 49 ++++++ src/pages/guide/building-with-ai.mdx | 192 ------------------------ src/pages/guide/using-tempo-with-ai.mdx | 100 ++++++++++++ src/pages/index.mdx | 6 + vocs.config.ts | 8 +- 5 files changed, 161 insertions(+), 194 deletions(-) create mode 100644 SKILL.md delete mode 100644 src/pages/guide/building-with-ai.mdx create mode 100644 src/pages/guide/using-tempo-with-ai.mdx diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 00000000..254e8360 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,49 @@ +--- +name: tempo-docs +description: Answer Tempo blockchain questions using official documentation. Use when asked about Tempo protocol, TIP-20 tokens, fees, transactions, stablecoin DEX, or any Tempo-related questions. +--- + +# Tempo Docs + +Skill for navigating Tempo documentation and source code. + +## Quick Context + +Before using MCP tools, try fetching context directly: + +- **llms.txt** – Concise index of all pages: `https://docs.tempo.xyz/llms.txt` +- **Markdown pages** – Append `.md` to any page URL (e.g. `https://docs.tempo.xyz/quickstart/integrate-tempo.md`) + +Use `read_web_page` to fetch these when you need broad context or a quick answer. + +## MCP Tools + +Use these tools for structured exploration: + +| Tool | Description | +| --- | --- | +| `mcp__tempo_mcp__list_pages` | List all documentation pages | +| `mcp__tempo_mcp__read_page` | Read a specific documentation page | +| `mcp__tempo_mcp__search_docs` | Search documentation | +| `mcp__tempo_mcp__list_sources` | List available source repositories | +| `mcp__tempo_mcp__list_source_files` | List files in a directory | +| `mcp__tempo_mcp__read_source_file` | Read a source code file | +| `mcp__tempo_mcp__get_file_tree` | Get recursive file tree | +| `mcp__tempo_mcp__search_source` | Search source code | + +## Available Sources + +- `tempoxyz/tempo` – Tempo node (Rust) +- `tempoxyz/tempo-ts` – TypeScript SDK +- `paradigmxyz/reth` – Reth Ethereum client +- `foundry-rs/foundry` – Foundry toolkit +- `wevm/viem` – TypeScript Ethereum interface +- `wevm/wagmi` – React hooks for Ethereum + +## Workflow + +1. **Quick lookup**: Use `read_web_page` on `https://docs.tempo.xyz/llms.txt` for an overview, or fetch a specific page as Markdown +2. **Search docs**: Use `mcp__tempo_mcp__search_docs` to find relevant pages +3. **Read pages**: Use `mcp__tempo_mcp__read_page` with the page path +4. **Explore source**: Use `mcp__tempo_mcp__search_source` or `mcp__tempo_mcp__get_file_tree` to find implementations +5. **Read code**: Use `mcp__tempo_mcp__read_source_file` to examine specific files \ No newline at end of file diff --git a/src/pages/guide/building-with-ai.mdx b/src/pages/guide/building-with-ai.mdx deleted file mode 100644 index 53ae8db7..00000000 --- a/src/pages/guide/building-with-ai.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: Building with AI -description: Tempo is built with AI-first principles, providing neccessary skills and tools for AI assistants. ---- - -import { Cards, Card, Tabs, Tab } from 'vocs' - -# Building with AI - -Tempo is built with AI-first principles, providing multiple features to make documentation accessible to LLMs and AI assistants. - -Tempo provides AI-assisted skills and tooling for the following: - - - - - - -## Tempo Chain - -### Skills - -Install the [Tempo Skills](https://github.com/tempoxyz/agent-skills) to give AI coding agents (Amp, Claude Code, etc.) access to Tempo documentation, source code via MCP, and examples. - -Check out [`tempoxyz/agent-skills`](https://github.com/tempoxyz/agent-skills) for more info on available skills. - -#### Installation - -```bash -npx skills add tempoxyz/agent-skills -``` - -Or manually: - -```bash -$ git clone https://github.com/tempoxyz/agent-skills.git -$ cp -r agent-skills/skills/tempo ~/.config/agents/skills/ -``` - -Or add to your project's [`.agents/skills/`](https://github.com/tempoxyz/agent-skills/tree/main/skills) directory for project-specific access. - -#### Usage - -Once installed, the skill is automatically available. The agent will use it when relevant tasks are detected. - -**Examples:** - -``` -How do I create a TIP-20 stablecoin? -``` -``` -Show me how fee sponsorship works in Viem -``` -``` -Search the Tempo source for transaction validation -``` - -### llms.txt - -The docs automatically generate [`llms.txt`](https://llmstxt.org/) files for LLM consumption: - -- `/llms.txt` – A concise index of all pages with titles and descriptions -- `/llms-full.txt` – Complete documentation content in a single file - -These files are generated at build time and served at the root of the site. - -### Markdown Rendering for AI Agents - -AI user agents are automatically detected and served raw Markdown instead of rendered HTML. This provides better token efficiency and easier parsing for LLMs. - -#### Supported AI Agents - -Popular user agents that are automatically detected: - -- **OpenAI**: `GPTBot`, `ChatGPT-User` -- **Anthropic**: `ClaudeBot`, `claude-web` -- **Google**: `Googlebot` - -See the [full list of supported agents](https://github.com/wevm/vocs/blob/next/src/waku/internal/middleware/md-router.ts#L3) for the most up-to-date list. - -#### Manual Markdown Access - -Any page can be accessed as Markdown by appending `.md` to the URL: - -``` -https://docs.tempo.xyz/quickstart/integrate-tempo.md -``` - -This is useful for copying documentation into AI conversations or for custom integrations. - -### Ask AI Menu - -The docs include a built-in "Ask AI" menu (⌘I / Ctrl+I) that provides: - -- **Open in ChatGPT/Claude**: Opens the current page context in popular AI assistants -- **Copy page for AI**: Copies the page content as Markdown to your clipboard -- **View as Markdown**: Opens the raw Markdown version of the current page -- **Copy MCP URL**: Copies the MCP server URL for AI assistant configuration - -### MCP Server - -The docs include a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that allows AI assistants to navigate documentation and source code programmatically. - -:::code-group -```bash [Claude Code] -claude mcp add --transport http tempo https://docs.tempo.xyz/api/mcp -``` -```bash [Codex CLI] -codex mcp add vercel --url https://docs.tempo.xyz/api/mcp -``` -```bash [Amp] -amp mcp add tempo https://docs.tempo.xyz/api/mcp -``` -::: - -#### Connecting AI Assistants - -Configure your AI assistant to connect to the MCP server: - -```json -{ - "mcpServers": { - "tempo-docs": { - "url": "https://docs.tempo.xyz/api/mcp" - } - } -} -``` - -## Tempo Wallet - -The [Tempo Wallet](https://wallet.tempo.xyz) distributes a CLI with built in spend controls and service discovery. Agents can use `tempo wallet` to pay for powerful new capabilities on demand. - - - - -Paste this into your agent to set up Tempo Wallet with your agent: - - -``` -Read https://wallet.tempo.xyz/SKILL.md and set up a Tempo Wallet -``` - - - -::::steps - -#### Install the CLI - -```bash -curl -fsSL https://tempo.xyz/install | bash -``` - -#### Connect your wallet - -```bash -tempo wallet login -``` - -#### Verify setup - -```bash -tempo wallet whoami -``` - -#### List available services - -```bash -tempo wallet services -``` - -#### Make a paid request - -```bash -tempo request -X POST \ - --json '{"prompt": "a sunset over the ocean"}' \ - https://fal.mpp.tempo.xyz/fal-ai/flux/dev -``` - -:::: - - \ No newline at end of file diff --git a/src/pages/guide/using-tempo-with-ai.mdx b/src/pages/guide/using-tempo-with-ai.mdx new file mode 100644 index 00000000..84eda07c --- /dev/null +++ b/src/pages/guide/using-tempo-with-ai.mdx @@ -0,0 +1,100 @@ +--- +title: Using Tempo with AI +description: Tempo is built with AI-first principles, providing neccessary skills and tools for AI assistants. +--- + +import { Cards, Card, Tabs, Tab } from 'vocs' + +# Using Tempo with AI + +Tempo is built with AI-first principles, providing multiple features to make documentation accessible to LLMs and AI assistants. + +Tempo provides AI-assisted skills and tooling for the following: + + + + + + +## Tempo Wallet + +The [Tempo Wallet](https://wallet.tempo.xyz) distributes a CLI with built in spend controls and service discovery. Agents can use `tempo wallet` to pay for powerful new capabilities on demand. + +Paste this into your agent (Claude Code, Amp, Codex, etc.) to set up and start using a Tempo Wallet: + +``` +Read https://tempo.xyz/SKILL.md and set up a Tempo Wallet +``` + +## Tempo Docs + +### Skills + +Install [Tempo Docs Skills](https://github.com/tempoxyz/agent-skills) to give AI coding agents (Amp, Claude Code, etc.) access to Tempo documentation, source code via MCP, and examples. + +```bash +npx skills add tempoxyz/docs +``` + +Once installed, the skill is automatically available. The agent will use it when relevant tasks are detected. + +**Examples:** + +``` +How do I create a TIP-20 stablecoin? +``` +``` +Show me how fee sponsorship works in Viem +``` +``` +Search the Tempo source for transaction validation +``` + +### Ask AI Menu + +The docs include a built-in "Ask AI" menu (⌘I / Ctrl+I) that provides: + +- **Open in ChatGPT/Claude**: Opens the current page context in popular AI assistants +- **Copy page for AI**: Copies the page content as Markdown to your clipboard +- **View as Markdown**: Opens the raw Markdown version of the current page +- **Copy MCP URL**: Copies the MCP server URL for AI assistant configuration + +### MCP Server + +The docs include a built-in [Model Context Protocol (MCP)](https://modelcontextprotocol.io) server that allows AI assistants to navigate documentation and source code programmatically. + +:::code-group +```bash [Claude Code] +claude mcp add --transport http tempo https://docs.tempo.xyz/api/mcp +``` +```bash [Codex CLI] +codex mcp add vercel --url https://docs.tempo.xyz/api/mcp +``` +```bash [Amp] +amp mcp add tempo https://docs.tempo.xyz/api/mcp +``` +::: + +#### Connecting AI Assistants + +Configure your AI assistant to connect to the MCP server: + +```json +{ + "mcpServers": { + "tempo-docs": { + "url": "https://docs.tempo.xyz/api/mcp" + } + } +} +``` \ No newline at end of file diff --git a/src/pages/index.mdx b/src/pages/index.mdx index dd427868..686293bb 100644 --- a/src/pages/index.mdx +++ b/src/pages/index.mdx @@ -21,6 +21,12 @@ Whether you're new to stablecoins, ready to start building, or looking for partn icon="lucide:book-open" title="Learn About Stablecoins" /> + Date: Tue, 17 Mar 2026 11:43:22 -0400 Subject: [PATCH 32/40] chore: sync from tempoxyz/docs (#38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove thirdweb from developer tools page (#146) Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> * fix(docs): add fee recipient language (#147) * feat: add Google Analytics (gtag.js) via static HTML head (#150) * fix(node): prep for mainnet (#144) * fix: revert vocs.config.tsx to .ts to fix Vercel SSR (#151) The Vocs SSR bundle hardcodes an import to dist/server/vocs.config.js. When the source file is .tsx, Vite outputs it differently, causing ERR_MODULE_NOT_FOUND and 500 errors on every page. - Rename vocs.config.tsx back to vocs.config.ts - Replace JSX with React.createElement calls for gtag head config - Update biome.json override to match new filename * fix: load env vars into process.env for vocs.config.ts (#152) Use Vite's loadEnv() to populate process.env with VITE_* env vars before the vocs() plugin initializes. Without this, VITE_GA_MEASUREMENT_ID is not available when vocs.config.ts is evaluated, causing the Google Analytics gtag to be silently skipped. This matches the pattern used in the mpp site's vite.config.ts. * fix: use Vite plugin for Google Analytics (vocs v2 compat) (#153) * fix: use transformIndexHtml for GA instead of vocs.config head Vocs v2 removed the `head` config property — it existed in v1 but is silently ignored in v2. Move Google Analytics injection to a Vite plugin using transformIndexHtml, which correctly injects script tags into the HTML during build. Also keep the loadEnv fix so VITE_* env vars from .env files are available to all plugins. * fix: use React component for GA instead of dead transformIndexHtml Vocs v2 uses Waku (RSC) which bypasses Vite's HTML pipeline entirely. transformIndexHtml is never invoked. Use a client-side React component in _layout.tsx instead, matching the pattern used for Vercel Analytics and PostHog. --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> Co-authored-by: Brendan Ryan --- .env.example | 1 + biome.json | 10 ++++++++ src/components/GoogleAnalytics.tsx | 37 ++++++++++++++++++++++++++++++ src/pages/_layout.tsx | 2 ++ src/pages/guide/node/validator.mdx | 4 ++-- vite.config.ts | 12 +++++++--- 6 files changed, 61 insertions(+), 5 deletions(-) create mode 100644 src/components/GoogleAnalytics.tsx diff --git a/.env.example b/.env.example index f881cd6d..fa329d7d 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,7 @@ INDEXSUPPLY_API_KEY= SLACK_FEEDBACK_WEBHOOK= # e.g. https://hooks.slack.com/services/... VITE_BASE_URL= # e.g. https://docs.tempo.xyz +VITE_GA_MEASUREMENT_ID= VITE_POSTHOG_HOST= VITE_POSTHOG_KEY= VITE_TEMPO_ENV= # testnet|devnet|localnet diff --git a/biome.json b/biome.json index e0242fef..35a9d884 100644 --- a/biome.json +++ b/biome.json @@ -59,6 +59,16 @@ ] }, "overrides": [ + { + "includes": ["vocs.config.ts"], + "linter": { + "rules": { + "security": { + "noDangerouslySetInnerHtml": "off" + } + } + } + }, { "includes": ["env.d.ts"], "linter": { diff --git a/src/components/GoogleAnalytics.tsx b/src/components/GoogleAnalytics.tsx new file mode 100644 index 00000000..fd12f674 --- /dev/null +++ b/src/components/GoogleAnalytics.tsx @@ -0,0 +1,37 @@ +'use client' + +import { useEffect } from 'react' + +declare global { + interface Window { + dataLayer: unknown[] + gtag: (...args: unknown[]) => void + } +} + +function GoogleAnalyticsInit({ id }: { id: string }) { + useEffect(() => { + if (typeof window.gtag !== 'undefined') return + + window.dataLayer = window.dataLayer || [] + window.gtag = function gtag() { + // biome-ignore lint/complexity/noArguments: gtag API requires arguments object + window.dataLayer.push(arguments) + } + window.gtag('js', new Date()) + window.gtag('config', id) + + const script = document.createElement('script') + script.async = true + script.src = `https://www.googletagmanager.com/gtag/js?id=${id}` + document.head.appendChild(script) + }, [id]) + + return null +} + +export default function GoogleAnalytics() { + const id = import.meta.env.VITE_GA_MEASUREMENT_ID + if (!id) return null + return +} diff --git a/src/pages/_layout.tsx b/src/pages/_layout.tsx index c45a1050..6b37fb56 100644 --- a/src/pages/_layout.tsx +++ b/src/pages/_layout.tsx @@ -4,6 +4,7 @@ import { Analytics } from '@vercel/analytics/react' import { SpeedInsights } from '@vercel/speed-insights/react' import type React from 'react' import { Toaster } from 'sonner' +import GoogleAnalytics from '../components/GoogleAnalytics' import PostHogSetup from '../components/PostHogSetup' export default function Layout( @@ -29,6 +30,7 @@ export default function Layout( /> + ) diff --git a/src/pages/guide/node/validator.mdx b/src/pages/guide/node/validator.mdx index 1471343b..c09d66dc 100644 --- a/src/pages/guide/node/validator.mdx +++ b/src/pages/guide/node/validator.mdx @@ -38,12 +38,12 @@ The public key should match the output of the `generate-private-key` command. The process for running a validator node is very similar to [running a full node](/guide/node/rpc). -You should start by downloading the latest snapshot using `tempo download --chain `. Downloading the snapshot allows your validator to start participating in consensus much faster. +You should start by downloading the latest snapshot using `tempo download -u `. Downloading the snapshot allows your validator to start participating in consensus much faster. Once you've downloaded the snapshot and have been whitelisted on-chain, you can proceed to run the validator node as such: ```bash tempo node --datadir \ - --chain moderato \ + --chain \ --consensus.signing-key \ --consensus.fee-recipient \ --telemetry-url diff --git a/vite.config.ts b/vite.config.ts index 31194ff3..5fb213cb 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,12 +2,18 @@ import * as fs from 'node:fs/promises' import * as path from 'node:path' import react from '@vitejs/plugin-react' import { Instance } from 'prool' -import { defineConfig, type Plugin } from 'vite' +import { defineConfig, loadEnv, type Plugin } from 'vite' import { vocs } from 'vocs/vite' // https://vite.dev/config/ -export default defineConfig({ - plugins: [syncTips(), vocs(), react(), tempoNode()], +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + for (const key of Object.keys(env)) { + if (!(key in process.env)) process.env[key] = env[key] + } + return { + plugins: [syncTips(), vocs(), react(), tempoNode()], + } }) function tempoNode(): Plugin { From 73fbddad22ba316428a5b977c204eb5047c0fa49 Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 12:25:26 -0400 Subject: [PATCH 33/40] chore: redirects (#40) --- src/pages/guide/using-tempo-with-ai.mdx | 4 ++-- vercel.json | 4 ++-- vocs.config.ts | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pages/guide/using-tempo-with-ai.mdx b/src/pages/guide/using-tempo-with-ai.mdx index 84eda07c..13325cce 100644 --- a/src/pages/guide/using-tempo-with-ai.mdx +++ b/src/pages/guide/using-tempo-with-ai.mdx @@ -20,7 +20,7 @@ Tempo provides AI-assisted skills and tooling for the following: /> @@ -97,4 +97,4 @@ Configure your AI assistant to connect to the MCP server: } } } -``` \ No newline at end of file +``` diff --git a/vercel.json b/vercel.json index 2c687922..4bc4289f 100644 --- a/vercel.json +++ b/vercel.json @@ -8,7 +8,7 @@ "value": "faucet.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/quickstart/faucet", + "destination": "https://docs.tempo.xyz/guide/getting-funds", "permanent": false }, { @@ -19,7 +19,7 @@ "value": "faucet.tempo.xyz" } ], - "destination": "https://docs.tempo.xyz/quickstart/faucet", + "destination": "https://docs.tempo.xyz/guide/getting-funds", "permanent": false } ], diff --git a/vocs.config.ts b/vocs.config.ts index 52e439d6..bf3886de 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -803,6 +803,11 @@ export default defineConfig({ destination: '/protocol/exchange/quote-tokens#pathusd', status: 301, }, + { + source: '/quickstart/faucet', + destination: '/guide/getting-funds', + status: 301, + }, ], codeHighlight: { langAlias: { From de3050358979b24e8022be7e0b8236c2f0ab7989 Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 14:56:08 -0400 Subject: [PATCH 34/40] fix: redirect (#41) --- vocs.config.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/vocs.config.ts b/vocs.config.ts index bf3886de..88195e69 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -803,11 +803,7 @@ export default defineConfig({ destination: '/protocol/exchange/quote-tokens#pathusd', status: 301, }, - { - source: '/quickstart/faucet', - destination: '/guide/getting-funds', - status: 301, - }, + ], codeHighlight: { langAlias: { From ade518b1c4c9c790c6fc75fd17ceecdc6f5854ca Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 16:32:05 -0400 Subject: [PATCH 35/40] chore: sync from tempoxyz/docs (#42) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove thirdweb from developer tools page (#146) Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> * fix(docs): add fee recipient language (#147) * feat: add Google Analytics (gtag.js) via static HTML head (#150) * fix(node): prep for mainnet (#144) * fix: revert vocs.config.tsx to .ts to fix Vercel SSR (#151) The Vocs SSR bundle hardcodes an import to dist/server/vocs.config.js. When the source file is .tsx, Vite outputs it differently, causing ERR_MODULE_NOT_FOUND and 500 errors on every page. - Rename vocs.config.tsx back to vocs.config.ts - Replace JSX with React.createElement calls for gtag head config - Update biome.json override to match new filename * fix: load env vars into process.env for vocs.config.ts (#152) Use Vite's loadEnv() to populate process.env with VITE_* env vars before the vocs() plugin initializes. Without this, VITE_GA_MEASUREMENT_ID is not available when vocs.config.ts is evaluated, causing the Google Analytics gtag to be silently skipped. This matches the pattern used in the mpp site's vite.config.ts. * fix: use Vite plugin for Google Analytics (vocs v2 compat) (#153) * fix: use transformIndexHtml for GA instead of vocs.config head Vocs v2 removed the `head` config property — it existed in v1 but is silently ignored in v2. Move Google Analytics injection to a Vite plugin using transformIndexHtml, which correctly injects script tags into the HTML during build. Also keep the loadEnv fix so VITE_* env vars from .env files are available to all plugins. * fix: use React component for GA instead of dead transformIndexHtml Vocs v2 uses Waku (RSC) which bypasses Vite's HTML pipeline entirely. transformIndexHtml is never invoked. Use a client-side React component in _layout.tsx instead, matching the pattern used for Vercel Analytics and PostHog. * fix: sql editor theme sync (#157) --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> Co-authored-by: Brendan Ryan --- src/components/SqlEditor.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SqlEditor.tsx b/src/components/SqlEditor.tsx index f97cf7f2..93aae35b 100644 --- a/src/components/SqlEditor.tsx +++ b/src/components/SqlEditor.tsx @@ -54,7 +54,7 @@ export function SqlEditor(props: SqlEditorProps) { const [isDark, setIsDark] = React.useState(() => { // Check dark mode on initial render if (typeof document !== 'undefined') { - return document.documentElement.classList.contains('dark') + return document.documentElement.style.colorScheme === 'dark' } return false }) @@ -66,7 +66,7 @@ export function SqlEditor(props: SqlEditorProps) { // Detect dark mode React.useEffect(() => { const checkDarkMode = () => { - setIsDark(document.documentElement.classList.contains('dark')) + setIsDark(document.documentElement.style.colorScheme === 'dark') } checkDarkMode() @@ -74,7 +74,7 @@ export function SqlEditor(props: SqlEditorProps) { const observer = new MutationObserver(checkDarkMode) observer.observe(document.documentElement, { attributes: true, - attributeFilter: ['class'], + attributeFilter: ['style'], }) return () => observer.disconnect() From 58a645a57038731a7fcbd1c100035f4e5f0bb5c0 Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 18:01:07 -0400 Subject: [PATCH 36/40] chore: point index supply guide to mainnet (#39) --- src/components/IndexSupplyQuery.tsx | 4 ++-- src/pages/_api/api/index-supply.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/IndexSupplyQuery.tsx b/src/components/IndexSupplyQuery.tsx index ceac85cb..6df8045c 100644 --- a/src/components/IndexSupplyQuery.tsx +++ b/src/components/IndexSupplyQuery.tsx @@ -1,7 +1,7 @@ 'use client' import * as React from 'react' import { isAddress, isHash } from 'viem' -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' import type * as z from 'zod/mini' import LucideExternalLink from '~icons/lucide/external-link' import { Container } from './Container' @@ -68,7 +68,7 @@ function getExplorerHost() { if (VITE_TEMPO_ENV !== 'testnet' && VITE_EXPLORER_OVERRIDE !== undefined) { return VITE_EXPLORER_OVERRIDE } - return tempoModerato.blockExplorers.default.url + return tempo.blockExplorers.default.url } function classifyHash(value: string | number | boolean | null): { diff --git a/src/pages/_api/api/index-supply.ts b/src/pages/_api/api/index-supply.ts index e8f7c401..b796567e 100644 --- a/src/pages/_api/api/index-supply.ts +++ b/src/pages/_api/api/index-supply.ts @@ -1,4 +1,4 @@ -import { tempoModerato } from 'viem/chains' +import { tempo } from 'viem/chains' type QueryRequest = { query: string @@ -51,7 +51,7 @@ export async function POST(request: Request): Promise { const signatures = body.signatures && body.signatures.length > 0 ? body.signatures : [''] - const chainId = tempoModerato.id + const chainId = tempo.id const chainCursor = `${chainId}-0` const response = await fetch(url.toString(), { From 7152f88c08e6cfea9c29e45549956e4b6d373df2 Mon Sep 17 00:00:00 2001 From: Uddhav Date: Tue, 17 Mar 2026 18:28:54 -0400 Subject: [PATCH 37/40] Mainnet ready: ecosystem infrastructure pages, connection updates (#44) * mainnet-ready: update developer tools, connection details, and explorer URLs - Add Bridges, Security & Compliance, Orchestration cards to developer tools - Add CoinGecko, Redstone to Data & Analytics - Add Elliptic, TRM Labs to Security & Compliance - Add Bridge (Stripe) to Orchestration - Restructure Wallets with Embedded and Custodial & Institutional subcategories - Add BitGo, Fireblocks to Custodial & Institutional - Add Bridges section with Relay and Squid - Update mainnet/testnet RPC and explorer URLs Amp-Thread-ID: https://ampcode.com/threads/T-019cf98b-501e-7538-8115-cd3039d8ad94 Co-authored-by: Amp * add missing providers and update connection tables - Add SonarX, SQD, Zerion to Data & Analytics - Add Pimlico to Smart Contract Libraries - Add Validation Cloud to Node Infrastructure - Move Utila to Custodial & Institutional - Add Across, Bungee to Bridges - Update Tempo Explorer with mainnet/testnet URLs - Make connect-to-wallets tables consistent with connection-details Amp-Thread-ID: https://ampcode.com/threads/T-019cf98b-501e-7538-8115-cd3039d8ad94 Co-authored-by: Amp * split Developer Tools into Tempo Ecosystem Infrastructure section - Create /ecosystem/ with individual pages for each category: Bridges, Data & Analytics, Block Explorers, Wallets, Smart Contract Libraries, Node Infrastructure, Security & Compliance, Orchestration - Add collapsible 'Tempo Ecosystem Infrastructure' sidebar section - Add redirect from /quickstart/developer-tools to /ecosystem - Add new providers: Across, Bungee, CoinGecko, Redstone, SonarX, SQD, Zerion, Elliptic, TRM Labs, BitGo, Fireblocks, Pimlico, Validation Cloud, Bridge (Stripe) - Restructure Wallets into Embedded + Custodial & Institutional - Update mainnet/testnet RPC and explorer URLs - Make connection tables consistent across pages Amp-Thread-ID: https://ampcode.com/threads/T-019cf98b-501e-7538-8115-cd3039d8ad94 Co-authored-by: Amp --------- Co-authored-by: Uddhav <255779543+letstokenize@users.noreply.github.com> Co-authored-by: Amp --- src/pages/ecosystem/block-explorers.mdx | 18 +++ src/pages/ecosystem/bridges.mdx | 32 ++++ src/pages/ecosystem/data-analytics.mdx | 89 +++++++++++ src/pages/ecosystem/index.mdx | 67 ++++++++ src/pages/ecosystem/node-infrastructure.mdx | 50 ++++++ src/pages/ecosystem/orchestration.mdx | 30 ++++ src/pages/ecosystem/security-compliance.mdx | 32 ++++ .../ecosystem/smart-contract-libraries.mdx | 26 ++++ src/pages/ecosystem/wallets.mdx | 79 ++++++++++ src/pages/guide/getting-funds.mdx | 4 +- .../guide/use-accounts/connect-to-wallets.mdx | 14 +- src/pages/quickstart/connection-details.mdx | 10 +- src/pages/quickstart/developer-tools.mdx | 145 ++++++++++++++++-- vocs.config.ts | 50 +++++- 14 files changed, 618 insertions(+), 28 deletions(-) create mode 100644 src/pages/ecosystem/block-explorers.mdx create mode 100644 src/pages/ecosystem/bridges.mdx create mode 100644 src/pages/ecosystem/data-analytics.mdx create mode 100644 src/pages/ecosystem/index.mdx create mode 100644 src/pages/ecosystem/node-infrastructure.mdx create mode 100644 src/pages/ecosystem/orchestration.mdx create mode 100644 src/pages/ecosystem/security-compliance.mdx create mode 100644 src/pages/ecosystem/smart-contract-libraries.mdx create mode 100644 src/pages/ecosystem/wallets.mdx diff --git a/src/pages/ecosystem/block-explorers.mdx b/src/pages/ecosystem/block-explorers.mdx new file mode 100644 index 00000000..643836b0 --- /dev/null +++ b/src/pages/ecosystem/block-explorers.mdx @@ -0,0 +1,18 @@ +--- +title: Block Explorers +description: View transactions, blocks, accounts, and token activity on the Tempo network with block explorers. +--- + +# Block Explorers + +View transactions, blocks, accounts, and token activity on Tempo. + +## Tempo Explorer + +Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/quickstart/connection-details). + +## Tenderly + +[Tenderly](https://tenderly.co) delivers full-stack observability, debugging, and simulation tools for Tempo smart contract development and monitoring. With Tenderly you get real-time error tracking, EVM-level tracing, and off-chain transaction simulation — enabling you to catch bugs, analyze reverts, and inspect gas usage before transactions go live. + +You can enable Tempo in the [Tenderly Dashboard](https://dashboard.tenderly.co/) to use its tracing, alerts, and debugging tools with no infrastructure to manage. diff --git a/src/pages/ecosystem/bridges.mdx b/src/pages/ecosystem/bridges.mdx new file mode 100644 index 00000000..497033ec --- /dev/null +++ b/src/pages/ecosystem/bridges.mdx @@ -0,0 +1,32 @@ +--- +title: Bridges +description: Move assets to and from Tempo with cross-chain bridges including Across, Bungee, Relay, and Squid. +--- + +# Bridges + +Move assets to and from Tempo with cross-chain bridges. + +## Across + +[Across](https://across.to) provides fast, capital-efficient bridging for moving assets to and from Tempo. Across uses an intent-based architecture with optimistic verification, enabling near-instant cross-chain transfers with competitive fees. + +Bridge assets to Tempo through the [Across app](https://app.across.to/) and explore the integration docs at [docs.across.to](https://docs.across.to/). + +## Bungee + +[Bungee](https://bungee.exchange) enables seamless swaps within and between blockchains. Bungee aggregates bridge and DEX liquidity to deliver fast, cost-efficient cross-chain transfers and swaps to and from Tempo — with a simple integration path via widget or API. + +Get started with the [Bungee docs](https://docs.bungee.exchange/) or try the [Bungee app](https://bungee.exchange). + +## Relay + +[Relay](https://relay.link) provides instant cross-chain bridging and transaction execution. Relay enables users and applications to move assets to Tempo from other chains with fast finality and low fees, powered by a network of relayers that fill orders on the destination chain. + +Bridge to Tempo through the [Relay app](https://relay.link) and explore the [Relay docs](https://docs.relay.link/). + +## Squid + +[Squid](https://www.squidrouter.com) enables cross-chain swaps and bridging in a single transaction. Squid's intent-based routing engine aggregates DEXs, bridges, and market makers to find the optimal path for moving assets to and from Tempo — with sub-second execution and zero fees on stablecoin swaps. Developers can integrate cross-chain functionality via a REST API, TypeScript SDK, or drop-in widget. + +Get started with the [Squid docs](https://docs.squidrouter.com/) or try the [bridge app](https://app.squidrouter.com/). diff --git a/src/pages/ecosystem/data-analytics.mdx b/src/pages/ecosystem/data-analytics.mdx new file mode 100644 index 00000000..b394eb8f --- /dev/null +++ b/src/pages/ecosystem/data-analytics.mdx @@ -0,0 +1,89 @@ +--- +title: Data & Analytics +description: Query blockchain data on Tempo with indexers, analytics platforms, oracles, and monitoring tools. +--- + +# Data & Analytics + +Query blockchain data with indexers, analytics platforms, and monitoring tools. + +## Allium + +[Allium](https://www.allium.so) is an enterprise blockchain data platform that delivers real-time, analytics-ready datasets through a unified schema across chains. Developers can fetch wallet, token, and price data in milliseconds without managing infrastructure, decoding raw data, or inferring transactions—making it easy to focus on building Tempo applications. + +Get access to Tempo data through the [Allium App](https://app.allium.so/join), explore the full API in the [Allium docs](https://docs.allium.so/), and browse real examples of production apps built on Allium [here](https://docs.allium.so/api/developer/overview). + +:::tip +Allium has a [ready-to-use recipe](https://github.com/Allium-Science/allium-recipes/tree/main/tempo) for querying Tempo data with SQL. +::: + +## Artemis + +[Artemis](https://www.artemisanalytics.com/products/terminal) provides a unified analytics terminal for monitoring onchain activity across stablecoins, assets, and networks. Developers use Artemis to analyze flows, liquidity, token performance, and ecosystem-level trends through a clean, queryable interface. + +Tempo is already supported within Artemis, with a dedicated analytics page for [Tempo Testnet](https://app.artemisanalytics.com/asset/tempo_moderato). + +Artemis also maintains a cross-chain stablecoin dashboard covering major USD-pegged assets across numerous networks. Stablecoins launched on Tempo will appear in the [Stablecoins dashboard](https://app.artemisanalytics.com/stablecoins). + +## Chainlink + +[Chainlink](https://chain.link) is the industry-standard oracle platform powering the majority of DeFi and bringing capital markets onchain. The Chainlink stack provides the data, interoperability, and security needed for tokenized assets, stablecoins, payments, lending, and other advanced onchain use cases. + +Chainlink supports Tempo through: + +- **Cross-Chain Interoperability Protocol (CCIP):** A secure interoperability layer for sending messages and value across chains, enabling cross-chain user flows and multi-chain architectures. +Explore CCIP in the [Chainlink CCIP docs](https://docs.chain.link/ccip). +- **Data Streams:** Chainlink Data Streams delivers low-latency market data offchain, which can be verified onchain. This pull-based design gives dApps on-demand access to high-frequency market data backed by decentralized, fault-tolerant, and transparent infrastructure—an improvement over traditional push-based oracles that update only at fixed intervals or price thresholds. +View the Chainlink Data Stream deployed on Tempo [here](https://explore.tempo.xyz/address/0x72790f9eB82db492a7DDb6d2af22A270Dcc3Db64?tab=contract). + +Developers can explore CCIP, Data Streams, and the full Chainlink platform through the [Chainlink Developer Docs](https://docs.chain.link/). + +## CoinGecko + +[CoinGecko](https://www.coingecko.com) provides comprehensive cryptocurrency market data, including prices, trading volume, market capitalization, and token metadata. Developers can use the CoinGecko API to access Tempo token data for building dashboards, portfolio trackers, and analytics tools. + +Get started with the [CoinGecko API](https://docs.coingecko.com/reference/introduction). + +## Goldsky + +[Goldsky](https://goldsky.com) makes it easy to access real-time Tempo data with minimal maintenance. Goldsky offers two core products for indexing and streaming onchain data: + +- **[Subgraphs](https://docs.goldsky.com/subgraphs/):** A fully backwards-compatible subgraph indexing solution that handles reorgs, RPC failures, and scaling automatically, with improved reliability and performance over traditional subgraph hosts. +- **[Mirror](https://docs.goldsky.com/mirror/):** A simple way to replicate subgraph or chain-level streams directly into your own databases or message queues, powering flexible front-end and back-end data pipelines. + +Start indexing Tempo [here](https://goldsky.com/chains/tempo). + +## Range + +[Range](https://www.range.org) powers the Stablecoin Explorer, which provides a unified view of major stablecoins across 100+ chains. Tempo is fully supported, allowing developers and users to trace stablecoin flows in a way traditional explorers cannot. + +Range stands out through: +- **Complete cross-chain visibility**, showing the entire lifecycle of a transfer in one place +- **Enriched context**, including bridge routes, verified entities, and risk signals +- **Built-in compliance checks** via global sanctions lists + +Explore Tempo activity in the [Stablecoin Explorer](https://explorer.money/transactions?dn=tempo-testnet&sc=INTRACHAIN&sn=tempo-testnet). + +## Redstone + +[Redstone](https://redstone.finance) delivers modular oracle infrastructure with a push and pull model for onchain price feeds. Redstone's architecture minimizes gas costs by delivering data on-demand, making it well-suited for DeFi applications, lending protocols, and stablecoin systems on Tempo. + +Explore the available data feeds and integration guides in the [Redstone docs](https://docs.redstone.finance/). + +## SonarX + +[SonarX](https://www.sonarx.com) delivers standardized, auditable on-chain data built for institutional confidence and enterprise integration. SonarX provides indexed Tempo data from genesis to tip through instant data shares on Snowflake, Databricks, and BigQuery, as well as real-time streaming and REST APIs — all backed by a robust data quality framework and SOC 2 compliance. + +Start a trial at [sonarx.com](https://www.sonarx.com/trial) and explore the [SonarX docs](https://docs.sonarx.com/). + +## SQD + +[SQD](https://sqd.ai) is a decentralized query engine and high-performance indexing toolkit for extracting and transforming on-chain data. With the Squid SDK, developers can build custom indexers for Tempo that are up to 100x faster than direct RPC indexing, with data served through the SQD Network's decentralized data layer. + +Get started with the [SQD docs](https://docs.sqd.ai/) and deploy indexers via [SQD Cloud](https://app.subsquid.io/). + +## Zerion + +[Zerion](https://zerion.io/api) provides an enterprise-grade wallet data API that delivers portfolio balances, transaction history, DeFi positions, PnL tracking, and real-time webhooks — including Tempo — through a single unified interface. Developers can add comprehensive blockchain data to their applications without running any indexing infrastructure. + +Get a free API key from the [Zerion dashboard](https://dashboard.zerion.io/) and explore the [API documentation](https://developers.zerion.io/reference/authentication). diff --git a/src/pages/ecosystem/index.mdx b/src/pages/ecosystem/index.mdx new file mode 100644 index 00000000..d7484671 --- /dev/null +++ b/src/pages/ecosystem/index.mdx @@ -0,0 +1,67 @@ +--- +title: Tempo Ecosystem Infrastructure +description: Explore Tempo's ecosystem partners providing bridges, wallets, node infrastructure, data analytics, security, and more for building on Tempo. +--- + +import { Cards, Card } from 'vocs' + +# Tempo Ecosystem Infrastructure + +Integrating with Tempo is easy by leveraging services provided by our infrastructure partners. These partners take advantage of Tempo transactions, TIP-20 tokens, and more. Visit their documentation for more information on how to get started. + + + + + + + + + + + + diff --git a/src/pages/ecosystem/node-infrastructure.mdx b/src/pages/ecosystem/node-infrastructure.mdx new file mode 100644 index 00000000..e39fd761 --- /dev/null +++ b/src/pages/ecosystem/node-infrastructure.mdx @@ -0,0 +1,50 @@ +--- +title: Node Infrastructure +description: Connect to Tempo with reliable RPC endpoints and managed node services from infrastructure partners. +--- + +# Node Infrastructure + +Connect to Tempo with reliable RPC endpoints and managed node services. + +## Alchemy + +With [Alchemy](https://alchemy.com), build the fastest and most reliable Tempo applications, powered by industry-leading latency, uptime, and elastic throughput. Alchemy's global RPC infrastructure supports everything from stablecoins to tokenization and large-scale consumer apps. + +Sign up through the [Alchemy dashboard](https://dashboard.alchemy.com) and visit the [Alchemy docs](https://www.alchemy.com/docs/node#tldr) to start building. + +## Blockdaemon + +[Blockdaemon](https://app.blockdaemon.com/) provides institutional-grade node and API infrastructure, along with staking and MPC wallet services. Their globally distributed platform supports enterprise-scale, production workloads with strong reliability and compliance guarantees. + +Sign up through the [Blockdaemon Developer Dashboard](https://app.blockdaemon.com/) and deploy a Tempo node by navigating to **Nodes & RPC → Deploy a Node**. + +## Chainstack + +[Chainstack](https://chainstack.com) provides managed blockchain infrastructure with high-performance, secure RPC nodes. The platform offers reliable Tempo endpoints with built-in monitoring and analytics. + +Create an account through the [Chainstack console](https://console.chainstack.com) to deploy Tempo nodes and access RPC endpoints. + +## Conduit + +[Conduit](https://conduit.xyz) provides high-performance RPC infrastructure for Tempo Testnet. Developers can create API keys and access Tempo Testnet endpoints through the [Conduit app](https://app.conduit.xyz). + +View the Tempo Testnet RPC endpoint in the [Conduit Hub](https://hub.conduit.xyz/tempo-testnet) and get started with the [Tempo RPC Quickstart](https://docs.conduit.xyz/rpc-nodes/getting-started/tempo-rpc-quickstart). + +## dRPC + +[dRPC](https://drpc.org) provides managed Tempo RPC endpoints through NodeCloud, with smart routing, analytics, key control, and front-end protection across 180+ networks. The platform runs on 40 providers in 8 geoclusters, with a free tier and flat-rate plans starting at $10. + +Get started by visiting the [Tempo chain page](https://drpc.org/chainlist/tempo-testnet-rpc), and learn more about NodeCloud on the [dRPC NodeCloud page](https://drpc.org/nodecloud-multichain-rpc-management). + +## Quicknode + +[Quicknode](https://quicknode.com) is the enterprise-grade development platform for building, scaling, and launching onchain applications with speed and reliability. Their globally optimized RPC network makes it easy to run high-performance Tempo workloads from day one. + +Get started on the [Tempo Chain Page](https://www.quicknode.com/chains/tempo) and follow the [QuickStart guide](https://www.quicknode.com/docs/tempo) to create your Tempo RPC endpoint. + +## Validation Cloud + +[Validation Cloud](https://www.validationcloud.io/tempo) provides institutional-grade, full-archive RPC nodes and validator infrastructure for Tempo. With SOC 2 Type II compliance, high performance, and low latency, Validation Cloud is purpose-built for powering real-world payments and stablecoin use cases at scale. + +Get started at [validationcloud.io/tempo](https://www.validationcloud.io/tempo). diff --git a/src/pages/ecosystem/orchestration.mdx b/src/pages/ecosystem/orchestration.mdx new file mode 100644 index 00000000..faa018c2 --- /dev/null +++ b/src/pages/ecosystem/orchestration.mdx @@ -0,0 +1,30 @@ +--- +title: Orchestration +description: Move money globally between local currencies and stablecoins. Issue, transfer, and manage stablecoins on Tempo. +--- + +# Orchestration + +Move money globally between local currencies and stablecoins. Issue, transfer, and manage stablecoins. + +## Brale + +[Brale](https://brale.xyz) provides infrastructure for issuing, transferring, and managing stablecoins across chains. Developers can create new stablecoins or work with existing issued assets using Brale's APIs to support on- and off-ramps, payouts, and cross-ecosystem stablecoin movement. + +Brale exposes two complementary APIs: + +- **[Stablecoin Movement & Account Management](https://docs.brale.xyz/#stablecoin-movement--account-management-apibralexyz):** + An authenticated API for orchestrating stablecoin workflows, including issuance, transfers across accounts or chains, custody management, and integration with financial institutions. + +- **[Stablecoin Market Data](https://docs.brale.xyz/#stablecoin-market-data-databralexyz):** + A public, read-only API that provides token metadata, stablecoin definitions, and price feeds. + +These APIs support common stablecoin workflows such as minting, redemption, swaps, payouts, and treasury operations, making Brale suitable for fintechs, exchanges, and payment platforms building on Tempo. + +Get started by creating an account [here](https://app.brale.xyz/buy/signup/). + +## Bridge + +[Bridge](https://www.bridge.xyz) (a Stripe Company) provides stablecoin orchestration infrastructure for moving money between fiat and crypto rails. Bridge supports Tempo with APIs for issuance, wallets, and cross-border stablecoin transfers — enabling fintechs and platforms to build payment flows that span traditional and onchain systems. + +Get started with [Bridge's Tempo Integration Guide](https://apidocs.bridge.xyz/get-started/guides/move-money/tempo-integration-guide#tempo-integration-guide). diff --git a/src/pages/ecosystem/security-compliance.mdx b/src/pages/ecosystem/security-compliance.mdx new file mode 100644 index 00000000..deaf13e4 --- /dev/null +++ b/src/pages/ecosystem/security-compliance.mdx @@ -0,0 +1,32 @@ +--- +title: Security & Compliance +description: Transaction scanning, threat detection, and compliance infrastructure for Tempo applications. +--- + +# Security & Compliance + +Transaction scanning, threat detection, and compliance infrastructure for Tempo applications. + +## Blockaid + +[Blockaid](https://blockaid.io) provides real-time security infrastructure for Web3 applications. Its transaction scanning and threat detection systems identify malicious activity before users sign transactions, improving safety across wallets and interfaces. + +Learn how Blockaid's transaction scanning improves security by visiting their [overview page](https://www.blockaid.io/transaction-security), and reach out to their team [here](https://www.blockaid.io/contact) to get started. + +## Chainalysis + +[Chainalysis](https://www.chainalysis.com) delivers industry-leading onchain intelligence, compliance, and security infrastructure. Through Hexagate, Chainalysis supports Tempo with real-time monitoring, anomaly detection, and threat insights to help developers and platforms better understand and manage onchain risk as the ecosystem grows. + +Discover how Hexagate supports Tempo [here](https://www.hexagate.com), or request a dedicated walkthrough from the Chainalysis team through their [demo form](https://www.hexagate.com/request-demo). + +## Elliptic + +[Elliptic](https://www.elliptic.co) provides blockchain analytics and compliance solutions for detecting and preventing financial crime. Elliptic supports Tempo with transaction screening, wallet risk scoring, and regulatory compliance tools — helping platforms meet AML obligations while operating on the Tempo network. + +Learn more about Elliptic's compliance solutions at [elliptic.co](https://www.elliptic.co) or explore their [developer docs](https://docs.elliptic.co/). + +## TRM Labs + +[TRM Labs](https://www.trmlabs.com) delivers blockchain intelligence and compliance infrastructure for detecting fraud, money laundering, and financial crime. TRM supports Tempo with transaction monitoring, wallet screening, and risk assessment tools that help platforms operate safely and meet regulatory requirements. + +Get started at [trmlabs.com](https://www.trmlabs.com) or explore their [documentation](https://docs.trmlabs.com/). diff --git a/src/pages/ecosystem/smart-contract-libraries.mdx b/src/pages/ecosystem/smart-contract-libraries.mdx new file mode 100644 index 00000000..b1055fb8 --- /dev/null +++ b/src/pages/ecosystem/smart-contract-libraries.mdx @@ -0,0 +1,26 @@ +--- +title: Smart Contract Libraries +description: Build with account abstraction and programmable smart contract wallets on Tempo. +--- + +# Smart Contract Libraries + +Build with account abstraction and programmable smart contract wallets. + +## Pimlico + +[Pimlico](https://www.pimlico.io) provides smart account infrastructure for Tempo, including ERC-4337 bundlers and paymasters. With Pimlico, developers can sponsor gas fees, accept ERC-20 tokens for gas, and relay smart account transactions — enabling seamless, gasless onchain experiences for end users. + +Get started on the [Pimlico dashboard](https://dashboard.pimlico.io/) and explore the [Pimlico docs](https://docs.pimlico.io/). + +## Safe *(coming soon)* + +[Safe](https://safe.global) provides a modular smart account framework used across leading Web3 applications and institutions. With Safe, developers can build Tempo applications that take advantage of multi-sig controls, programmable permissions, session keys, and automated transaction policies. + +Safe integration for Tempo is coming soon. Stay tuned for updates as support becomes available. + +## ZeroDev + +[ZeroDev](https://zerodev.app) provides a powerful smart account platform for Tempo, supporting both ERC-4337 and EIP-7702. Developers can onboard users with social logins, enable gas sponsorship, and automate transactions while taking advantage of ZeroDev's chain-abstracted workflows. Its modular wallet stack also allows teams to build customized features such as custom transaction policies and tailored approval logic. + +Create a project in the [ZeroDev dashboard](https://dashboard.zerodev.app) and follow the [SDK quickstart](https://docs.zerodev.app/sdk/getting-started/quickstart) to integrate smart accounts into your Tempo application. diff --git a/src/pages/ecosystem/wallets.mdx b/src/pages/ecosystem/wallets.mdx new file mode 100644 index 00000000..7802060a --- /dev/null +++ b/src/pages/ecosystem/wallets.mdx @@ -0,0 +1,79 @@ +--- +title: Wallets +description: Integrate embedded, custodial, and institutional wallet infrastructure into your Tempo application. +--- + +# Wallets + +Integrate user-friendly wallet experiences directly into your application. + +## Embedded + +### Blockradar +[Blockradar](https://blockradar.co) provides non-custodial wallet infrastructure purpose-built for fintechs running stablecoin payments. The platform focuses on real financial use cases, from merchant settlement to cross-border payouts, with tools designed for payments, compliance, treasury operations, and multi-chain liquidity. Explore the full platform in the [Blockradar Docs](https://docs.blockradar.co/). + +**Wallet and Payment Operations:** Through one unified API, teams can issue wallets for users, merchants, or treasury; accept fiat inflows through virtual accounts; enable gasless stablecoin transactions; apply AML checks automatically; consolidate balances through configurable sweeps; and handle cross-chain movement using swap and bridge. Fintechs can start building immediately from our API or [Blockradar Dashboard](https://dashboard.blockradar.co/). For advanced flows or high-volume programs, fintechs can [book a demo](https://www.blockradar.co/contact) to walk through production architectures. + +### Crossmint + +[Crossmint](https://www.crossmint.com) is an all-in-one platform, with unified APIs for [wallets](https://docs.crossmint.com/wallets/), [stablecoin orchestration](https://docs.crossmint.com/stablecoin-orchestration/), [checkout flows](https://docs.crossmint.com/payments), and [tokenization](https://docs.crossmint.com/minting), giving developers a single interface for everything from payments to asset management on Tempo. + +Crossmint delivers a gasless, seed-phrase-free UX backed by bank-grade security and compliance, along with no-code dashboards for managing programs across your team. + +Set up a project in the [Crossmint console](https://crossmint.com/console) and explore the [Solution Guide](https://docs.crossmint.com/solutions/overview#fintech) tailored for payment use-cases. + +### Dynamic + +[Dynamic](https://dynamic.xyz) combines authentication, smart wallets, and key management into a flexible SDK for Tempo developers. Teams can onboard users with familiar login methods and provision Tempo-compatible wallets through Dynamic's secure infrastructure. + +Enable Tempo testnet in the [Dynamic dashboard](https://app.dynamic.xyz/dashboard/chains-and-networks), and create an account [here](https://www.dynamic.xyz/get-started) to start integrating Dynamic into your app. + +### Para + +[Para](https://getpara.com) is a comprehensive wallet and authentication suite for fintech and crypto applications. It provides flexible login methods, secure MPC-backed wallets, fast authentication, and infrastructure for automating onchain activity. Para is adding Tempo chain support so developers can easily build Tempo-enabled wallets and payment flows. + +Get started by signing up through the [Para Dev Portal](https://developer.getpara.com/) and following the quickstart in the [Para docs](https://docs.getpara.com/v2/introduction/welcome). + +### Privy + +[Privy](https://www.privy.io/) builds secure key management and embedded wallets so any developer can easily build secure, scalable wallets into their app. Easily spin up self-custodial wallets for users, manage your treasury wallets and more. + +Privy takes advantage of Tempo-native experiences to enable better stablecoin and payments experiences. Easily enable gas sponsorship, leverage webhooks for onchain events, delegated signatures, simple wallet funding, etc. + +You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/create/create-a-wallet#param-chain-type-1) an ethereum wallet with Privy and pass in `"caip2": "eip155:42431"` when [making transactions](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction#usage-9). + +:::tip +Check out Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. +::: + +### Turnkey + +[Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo transactions, automate wallet operations, and build custom logic around how keys are used. + +Turnkey also supports sponsor-style workflows, enabling gasless or subsidized transaction flows through configurable signing policies. + +[Create your Turnkey account](https://app.turnkey.com/dashboard) and follow the [Turnkey Embedded Wallet Kit guide](https://docs.turnkey.com/sdks/react/getting-started) to integrate embedded wallets into your Tempo app. + +:::tip +Turnkey has a [`with-tempo`](https://github.com/tkhq/sdk/tree/main/examples/with-tempo) example in their SDK to get you started quickly. +::: + +## Custodial & Institutional + +### BitGo + +[BitGo](https://www.bitgo.com) provides institutional-grade custody, trading, and wallet infrastructure. BitGo supports Tempo with both custodial and self-custody wallet solutions, enabling enterprises to securely store, manage, and transact with Tempo-based assets under robust security and compliance controls. BitGo is a qualified custodian in the United States and globally [licensed and regulated](https://www.bitgo.com/company/licenses/). + +Get started through the [BitGo platform](https://www.bitgo.com) or explore their [developer docs](https://developers.bitgo.com/). + +### Fireblocks + +[Fireblocks](https://www.fireblocks.com) provides enterprise-grade digital asset infrastructure for custody, transfers, and tokenization. Tempo is supported through Fireblocks' MPC-based signing, policy engine, and transaction API — enabling institutions to securely manage Tempo assets with configurable approval workflows and direct network connectivity. + +Access Tempo through the [Fireblocks console](https://console.fireblocks.io/) and explore the [Fireblocks Developer docs](https://developers.fireblocks.com/). + +### Utila + +[Utila](https://utila.io) provides secure MPC wallet infrastructure and asset-management tooling for teams building with stablecoins and digital assets. Developers can use Utila to manage Tempo-based payments and treasury operations across multiple wallets and blockchains, all within a single policy-driven platform. Utila's MPC technology reduces counterparty risk, while its configurable approval engine gives teams granular control over how funds are moved. + +[Learn more](https://utila.io/product/payments/) about how Utila supports stablecoin operations on Tempo, and [request a demo](https://utila.io/request-a-demo/) if you're interested in secure MPC infrastructure. diff --git a/src/pages/guide/getting-funds.mdx b/src/pages/guide/getting-funds.mdx index ed00a878..65fecd4d 100644 --- a/src/pages/guide/getting-funds.mdx +++ b/src/pages/guide/getting-funds.mdx @@ -11,8 +11,8 @@ There are several ways to get funds on Tempo: diff --git a/src/pages/guide/use-accounts/connect-to-wallets.mdx b/src/pages/guide/use-accounts/connect-to-wallets.mdx index 73915995..64425465 100644 --- a/src/pages/guide/use-accounts/connect-to-wallets.mdx +++ b/src/pages/guide/use-accounts/connect-to-wallets.mdx @@ -282,23 +282,25 @@ For example, if you are using MetaMask: #### Mainnet -| **Name** | `Tempo Mainnet` | +| **Property** | **Value** | |-------------------|-------| +| **Network Name** | Tempo Mainnet | | **Currency** | `USD` | | **Chain ID** | `4217` | -| **HTTP URL** | `https://rpc.presto.tempo.xyz` | -| **WebSocket URL** | `wss://rpc.presto.tempo.xyz` | -| **Block Explorer** | [`https://explore.mainnet.tempo.xyz`](https://explore.mainnet.tempo.xyz) | +| **HTTP URL** | `https://rpc.tempo.xyz` | +| **WebSocket URL** | `wss://rpc.tempo.xyz` | +| **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | #### Testnet -| **Name** | `Tempo Testnet (Moderato)` | +| **Property** | **Value** | |-------------------|-------| +| **Network Name** | Tempo Testnet (Moderato) | | **Currency** | `USD` | | **Chain ID** | `42431` | | **HTTP URL** | `https://rpc.moderato.tempo.xyz` | | **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | -| **Block Explorer** | [`https://explore.moderato.tempo.xyz`](https://explore.moderato.tempo.xyz) | +| **Block Explorer** | [`https://explore.testnet.tempo.xyz`](https://explore.testnet.tempo.xyz) | The official documentation from MetaMask on this process is also available [here](https://support.metamask.io/configure/networks/how-to-add-a-custom-network-rpc#adding-a-network-manually). diff --git a/src/pages/quickstart/connection-details.mdx b/src/pages/quickstart/connection-details.mdx index 66740651..fc808255 100644 --- a/src/pages/quickstart/connection-details.mdx +++ b/src/pages/quickstart/connection-details.mdx @@ -26,7 +26,7 @@ To connect via CLI, we recommend using [`cast`](https://getfoundry.sh/cast/overv ```bash /dev/null/monitor.sh#L1-11 # Check block height (should be steadily increasing) -cast block-number --rpc-url https://rpc.presto.tempo.xyz +cast block-number --rpc-url https://rpc.tempo.xyz ``` ## Direct Connection Details @@ -38,9 +38,9 @@ cast block-number --rpc-url https://rpc.presto.tempo.xyz | **Network Name** | Tempo Mainnet | | **Currency** | `USD` | | **Chain ID** | `4217` | -| **HTTP URL** | `https://rpc.presto.tempo.xyz` | -| **WebSocket URL** | `wss://rpc.presto.tempo.xyz` | -| **Block Explorer** | [`https://explore.mainnet.tempo.xyz`](https://explore.mainnet.tempo.xyz) | +| **HTTP URL** | `https://rpc.tempo.xyz` | +| **WebSocket URL** | `wss://rpc.tempo.xyz` | +| **Block Explorer** | [`https://explore.tempo.xyz`](https://explore.tempo.xyz) | ### Testnet @@ -51,4 +51,4 @@ cast block-number --rpc-url https://rpc.presto.tempo.xyz | **Chain ID** | `42431` | | **HTTP URL** | `https://rpc.moderato.tempo.xyz` | | **WebSocket URL** | `wss://rpc.moderato.tempo.xyz` | -| **Block Explorer** | [`https://explore.moderato.tempo.xyz`](https://explore.moderato.tempo.xyz) | +| **Block Explorer** | [`https://explore.testnet.tempo.xyz`](https://explore.testnet.tempo.xyz) | diff --git a/src/pages/quickstart/developer-tools.mdx b/src/pages/quickstart/developer-tools.mdx index 756cfc23..497e55c6 100644 --- a/src/pages/quickstart/developer-tools.mdx +++ b/src/pages/quickstart/developer-tools.mdx @@ -9,6 +9,24 @@ import { Cards, Card } from 'vocs' Integrating with Tempo is easy by leveraging services provided by our infrastructure partners. These partners take advantage of Tempo transactions, TIP-20 tokens, and more. Visit their documentation for more information on how to get started. + + + +## Bridges + +### Across + +[Across](https://across.to) provides fast, capital-efficient bridging for moving assets to and from Tempo. Across uses an intent-based architecture with optimistic verification, enabling near-instant cross-chain transfers with competitive fees. + +Bridge assets to Tempo through the [Across app](https://app.across.to/) and explore the integration docs at [docs.across.to](https://docs.across.to/). + +### Bungee + +[Bungee](https://bungee.exchange) enables seamless swaps within and between blockchains. Bungee aggregates bridge and DEX liquidity to deliver fast, cost-efficient cross-chain transfers and swaps to and from Tempo — with a simple integration path via widget or API. + +Get started with the [Bungee docs](https://docs.bungee.exchange/) or try the [Bungee app](https://bungee.exchange). + +### Relay + +[Relay](https://relay.link) provides instant cross-chain bridging and transaction execution. Relay enables users and applications to move assets to Tempo from other chains with fast finality and low fees, powered by a network of relayers that fill orders on the destination chain. + +Bridge to Tempo through the [Relay app](https://relay.link) and explore the [Relay docs](https://docs.relay.link/). + +### Squid + +[Squid](https://www.squidrouter.com) enables cross-chain swaps and bridging in a single transaction. Squid's intent-based routing engine aggregates DEXs, bridges, and market makers to find the optimal path for moving assets to and from Tempo — with sub-second execution and zero fees on stablecoin swaps. Developers can integrate cross-chain functionality via a REST API, TypeScript SDK, or drop-in widget. + +Get started with the [Squid docs](https://docs.squidrouter.com/) or try the [bridge app](https://app.squidrouter.com/). + ## Data & Analytics ### Allium @@ -80,6 +124,12 @@ View the Chainlink Data Stream deployed on Tempo [here](https://explore.tempo.xy Developers can explore CCIP, Data Streams, and the full Chainlink platform through the [Chainlink Developer Docs](https://docs.chain.link/). +### CoinGecko + +[CoinGecko](https://www.coingecko.com) provides comprehensive cryptocurrency market data, including prices, trading volume, market capitalization, and token metadata. Developers can use the CoinGecko API to access Tempo token data for building dashboards, portfolio trackers, and analytics tools. + +Get started with the [CoinGecko API](https://docs.coingecko.com/reference/introduction). + ### Goldsky [Goldsky](https://goldsky.com) makes it easy to access real-time Tempo data with minimal maintenance. Goldsky offers two core products for indexing and streaming onchain data: @@ -100,12 +150,35 @@ Range stands out through: Explore Tempo activity in the [Stablecoin Explorer](https://explorer.money/transactions?dn=tempo-testnet&sc=INTRACHAIN&sn=tempo-testnet). +### Redstone + +[Redstone](https://redstone.finance) delivers modular oracle infrastructure with a push and pull model for onchain price feeds. Redstone's architecture minimizes gas costs by delivering data on-demand, making it well-suited for DeFi applications, lending protocols, and stablecoin systems on Tempo. + +Explore the available data feeds and integration guides in the [Redstone docs](https://docs.redstone.finance/). + +### SonarX + +[SonarX](https://www.sonarx.com) delivers standardized, auditable on-chain data built for institutional confidence and enterprise integration. SonarX provides indexed Tempo data from genesis to tip through instant data shares on Snowflake, Databricks, and BigQuery, as well as real-time streaming and REST APIs — all backed by a robust data quality framework and SOC 2 compliance. + +Start a trial at [sonarx.com](https://www.sonarx.com/trial) and explore the [SonarX docs](https://docs.sonarx.com/). + +### SQD + +[SQD](https://sqd.ai) is a decentralized query engine and high-performance indexing toolkit for extracting and transforming on-chain data. With the Squid SDK, developers can build custom indexers for Tempo that are up to 100x faster than direct RPC indexing, with data served through the SQD Network's decentralized data layer. + +Get started with the [SQD docs](https://docs.sqd.ai/) and deploy indexers via [SQD Cloud](https://app.subsquid.io/). + +### Zerion + +[Zerion](https://zerion.io/api) provides an enterprise-grade wallet data API that delivers portfolio balances, transaction history, DeFi positions, PnL tracking, and real-time webhooks — including Tempo — through a single unified interface. Developers can add comprehensive blockchain data to their applications without running any indexing infrastructure. + +Get a free API key from the [Zerion dashboard](https://dashboard.zerion.io/) and explore the [API documentation](https://developers.zerion.io/reference/authentication). ## Block Explorers ### Tempo Explorer -Tempo's official block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. +Tempo's official Mainnet block explorer is available at [explore.tempo.xyz](https://explore.tempo.xyz). View transactions, blocks, accounts, and token activity on the Tempo network. Testnet block explorer is available at [explore.testnet.tempo.xyz](https://explore.testnet.tempo.xyz). For more connection information, see [Connect to the Network](/quickstart/connection-details). ### Tenderly @@ -113,14 +186,16 @@ Tempo's official block explorer is available at [explore.tempo.xyz](https://expl You can enable Tempo in the [Tenderly Dashboard](https://dashboard.tenderly.co/) to use its tracing, alerts, and debugging tools with no infrastructure to manage. -## Embedded Wallets +## Wallets + +### Embedded -### Blockradar +#### Blockradar [Blockradar](https://blockradar.co) provides non-custodial wallet infrastructure purpose-built for fintechs running stablecoin payments. The platform focuses on real financial use cases, from merchant settlement to cross-border payouts, with tools designed for payments, compliance, treasury operations, and multi-chain liquidity. Explore the full platform in the [Blockradar Docs](https://docs.blockradar.co/). **Wallet and Payment Operations:** Through one unified API, teams can issue wallets for users, merchants, or treasury; accept fiat inflows through virtual accounts; enable gasless stablecoin transactions; apply AML checks automatically; consolidate balances through configurable sweeps; and handle cross-chain movement using swap and bridge. Fintechs can start building immediately from our API or [Blockradar Dashboard](https://dashboard.blockradar.co/). For advanced flows or high-volume programs, fintechs can [book a demo](https://www.blockradar.co/contact) to walk through production architectures. -### Crossmint +#### Crossmint [Crossmint](https://www.crossmint.com) is an all-in-one platform, with unified APIs for [wallets](https://docs.crossmint.com/wallets/), [stablecoin orchestration](https://docs.crossmint.com/stablecoin-orchestration/), [checkout flows](https://docs.crossmint.com/payments), and [tokenization](https://docs.crossmint.com/minting), giving developers a single interface for everything from payments to asset management on Tempo. @@ -128,19 +203,19 @@ Crossmint delivers a gasless, seed-phrase-free UX backed by bank-grade security Set up a project in the [Crossmint console](https://crossmint.com/console) and explore the [Solution Guide](https://docs.crossmint.com/solutions/overview#fintech) tailored for payment use-cases. -### Dynamic +#### Dynamic [Dynamic](https://dynamic.xyz) combines authentication, smart wallets, and key management into a flexible SDK for Tempo developers. Teams can onboard users with familiar login methods and provision Tempo-compatible wallets through Dynamic's secure infrastructure. Enable Tempo testnet in the [Dynamic dashboard](https://app.dynamic.xyz/dashboard/chains-and-networks), and create an account [here](https://www.dynamic.xyz/get-started) to start integrating Dynamic into your app. -### Para +#### Para [Para](https://getpara.com) is a comprehensive wallet and authentication suite for fintech and crypto applications. It provides flexible login methods, secure MPC-backed wallets, fast authentication, and infrastructure for automating onchain activity. Para is adding Tempo chain support so developers can easily build Tempo-enabled wallets and payment flows. Get started by signing up through the [Para Dev Portal](https://developer.getpara.com/) and following the quickstart in the [Para docs](https://docs.getpara.com/v2/introduction/welcome). -### Privy +#### Privy [Privy](https://www.privy.io/) builds secure key management and embedded wallets so any developer can easily build secure, scalable wallets into their app. Easily spin up self-custodial wallets for users, manage your treasury wallets and more. @@ -152,7 +227,7 @@ You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/c Check out Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. ::: -### Turnkey +#### Turnkey [Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo transactions, automate wallet operations, and build custom logic around how keys are used. @@ -161,10 +236,24 @@ Turnkey also supports sponsor-style workflows, enabling gasless or subsidized tr [Create your Turnkey account](https://app.turnkey.com/dashboard) and follow the [Turnkey Embedded Wallet Kit guide](https://docs.turnkey.com/sdks/react/getting-started) to integrate embedded wallets into your Tempo app. :::tip -Turnkey has a [`with-tempo` example](https://github.com/tkhq/sdk/tree/main/examples/with-tempo) in their SDK to get you started quickly. +Turnkey has a [`with-tempo`](https://github.com/tkhq/sdk/tree/main/examples/with-tempo) example in their SDK to get you started quickly. ::: -### Utila +### Custodial & Institutional + +#### BitGo + +[BitGo](https://www.bitgo.com) provides institutional-grade custody, trading, and wallet infrastructure. BitGo supports Tempo with both custodial and self-custody wallet solutions, enabling enterprises to securely store, manage, and transact with Tempo-based assets under robust security and compliance controls. BitGo is a qualified custodian in the United States and globally [licensed and regulated](https://www.bitgo.com/company/licenses/). + +Get started through the [BitGo platform](https://www.bitgo.com) or explore their [developer docs](https://developers.bitgo.com/). + +#### Fireblocks + +[Fireblocks](https://www.fireblocks.com) provides enterprise-grade digital asset infrastructure for custody, transfers, and tokenization. Tempo is supported through Fireblocks' MPC-based signing, policy engine, and transaction API — enabling institutions to securely manage Tempo assets with configurable approval workflows and direct network connectivity. + +Access Tempo through the [Fireblocks console](https://console.fireblocks.io/) and explore the [Fireblocks Developer docs](https://developers.fireblocks.com/). + +#### Utila [Utila](https://utila.io) provides secure MPC wallet infrastructure and asset-management tooling for teams building with stablecoins and digital assets. Developers can use Utila to manage Tempo-based payments and treasury operations across multiple wallets and blockchains, all within a single policy-driven platform. Utila's MPC technology reduces counterparty risk, while its configurable approval engine gives teams granular control over how funds are moved. @@ -172,6 +261,12 @@ Turnkey has a [`with-tempo` example](https://github.com/tkhq/sdk/tree/main/examp ## Smart Contract Libraries +### Pimlico + +[Pimlico](https://www.pimlico.io) provides smart account infrastructure for Tempo, including ERC-4337 bundlers and paymasters. With Pimlico, developers can sponsor gas fees, accept ERC-20 tokens for gas, and relay smart account transactions — enabling seamless, gasless onchain experiences for end users. + +Get started on the [Pimlico dashboard](https://dashboard.pimlico.io/) and explore the [Pimlico docs](https://docs.pimlico.io/). + ### Safe *(coming soon)* [Safe](https://safe.global) provides a modular smart account framework used across leading Web3 applications and institutions. With Safe, developers can build Tempo applications that take advantage of multi-sig controls, programmable permissions, session keys, and automated transaction policies. @@ -222,6 +317,12 @@ Get started by visiting the [Tempo chain page](https://drpc.org/chainlist/tempo- Get started on the [Tempo Chain Page](https://www.quicknode.com/chains/tempo) and follow the [QuickStart guide](https://www.quicknode.com/docs/tempo) to create your Tempo RPC endpoint. +### Validation Cloud + +[Validation Cloud](https://www.validationcloud.io/tempo) provides institutional-grade, full-archive RPC nodes and validator infrastructure for Tempo. With SOC 2 Type II compliance, high performance, and low latency, Validation Cloud is purpose-built for powering real-world payments and stablecoin use cases at scale. + +Get started at [validationcloud.io/tempo](https://www.validationcloud.io/tempo). + ## Security & Compliance ### Blockaid @@ -236,7 +337,19 @@ Learn how Blockaid's transaction scanning improves security by visiting their [o Discover how Hexagate supports Tempo [here](https://www.hexagate.com), or request a dedicated walkthrough from the Chainalysis team through their [demo form](https://www.hexagate.com/request-demo). -## Issuance +### Elliptic + +[Elliptic](https://www.elliptic.co) provides blockchain analytics and compliance solutions for detecting and preventing financial crime. Elliptic supports Tempo with transaction screening, wallet risk scoring, and regulatory compliance tools — helping platforms meet AML obligations while operating on the Tempo network. + +Learn more about Elliptic's compliance solutions at [elliptic.co](https://www.elliptic.co) or explore their [developer docs](https://docs.elliptic.co/). + +### TRM Labs + +[TRM Labs](https://www.trmlabs.com) delivers blockchain intelligence and compliance infrastructure for detecting fraud, money laundering, and financial crime. TRM supports Tempo with transaction monitoring, wallet screening, and risk assessment tools that help platforms operate safely and meet regulatory requirements. + +Get started at [trmlabs.com](https://www.trmlabs.com) or explore their [documentation](https://docs.trmlabs.com/). + +## Orchestration ### Brale [Brale](https://brale.xyz) provides infrastructure for issuing, transferring, and managing stablecoins across chains. Developers can create new stablecoins or work with existing issued assets using Brale's APIs to support on- and off-ramps, payouts, and cross-ecosystem stablecoin movement. @@ -252,3 +365,9 @@ Brale exposes two complementary APIs: These APIs support common stablecoin workflows such as minting, redemption, swaps, payouts, and treasury operations, making Brale suitable for fintechs, exchanges, and payment platforms building on Tempo. Get started by creating an account [here](https://app.brale.xyz/buy/signup/). + +### Bridge + +[Bridge](https://www.bridge.xyz) (a Stripe Company) provides stablecoin orchestration infrastructure for moving money between fiat and crypto rails. Bridge supports Tempo with APIs for issuance, wallets, and cross-border stablecoin transfers — enabling fintechs and platforms to build payment flows that span traditional and onchain systems. + +Get started with [Bridge's Tempo Integration Guide](https://apidocs.bridge.xyz/get-started/guides/move-money/tempo-integration-guide#tempo-integration-guide). diff --git a/vocs.config.ts b/vocs.config.ts index 88195e69..a9142857 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -257,7 +257,7 @@ export default defineConfig({ }, { text: 'Developer Tools', - link: '/quickstart/developer-tools', + link: '/ecosystem', }, { text: 'EVM Differences', @@ -281,6 +281,48 @@ export default defineConfig({ }, ], }, + { + text: 'Tempo Ecosystem Infrastructure', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/ecosystem', + }, + { + text: 'Bridges', + link: '/ecosystem/bridges', + }, + { + text: 'Data & Analytics', + link: '/ecosystem/data-analytics', + }, + { + text: 'Block Explorers', + link: '/ecosystem/block-explorers', + }, + { + text: 'Wallets', + link: '/ecosystem/wallets', + }, + { + text: 'Smart Contract Libraries', + link: '/ecosystem/smart-contract-libraries', + }, + { + text: 'Node Infrastructure', + link: '/ecosystem/node-infrastructure', + }, + { + text: 'Security & Compliance', + link: '/ecosystem/security-compliance', + }, + { + text: 'Orchestration', + link: '/ecosystem/orchestration', + }, + ], + }, { text: 'Tempo Protocol Specs', items: [ @@ -744,6 +786,11 @@ export default defineConfig({ destination: '/stablecoin-dex/:path*', status: 301, }, + { + source: '/quickstart/developer-tools', + destination: '/ecosystem', + status: 301, + }, { source: '/guide/ai-support', destination: '/guide/building-with-ai', @@ -803,7 +850,6 @@ export default defineConfig({ destination: '/protocol/exchange/quote-tokens#pathusd', status: 301, }, - ], codeHighlight: { langAlias: { From 71cc930835fd73d929c327fc30287d231acae7d0 Mon Sep 17 00:00:00 2001 From: Liam Horne Date: Tue, 17 Mar 2026 18:30:59 -0400 Subject: [PATCH 38/40] Restructure CLI docs under Tempo Developer Tools (#43) Merge 'Tempo Wallet CLI' and 'Tempo SDKs' sidebar sections into 'Tempo Developer Tools' with CLI and SDKs subsections. CLI section now has four pages: - Overview: what the binary does, install, agent setup with tabbed code group (Claude Code / Amp / Codex) - tempo wallet: auth, keys, funds, service discovery, sessions - tempo request: curl-like MPP client with automatic payment - tempo node: node flags grouped by function, plus tempo download Other changes: - Move URL from /wallet to /cli with 301 redirects - Delete troubleshooting page (redundant) - Add CLI cross-links in Using Tempo with AI, Getting Funds, Machine Payments, and Node Installation pages - Upgrade agent setup prompts to tabbed code groups site-wide Amp-Thread-ID: https://ampcode.com/threads/T-019cfda6-a311-765e-8012-69eeeb9129e2 Co-authored-by: Amp --- src/pages/cli/index.mdx | 73 ++++++++ src/pages/cli/node.mdx | 94 ++++++++++ src/pages/cli/request.mdx | 80 ++++++++ src/pages/cli/wallet.mdx | 201 +++++++++++++++++++++ src/pages/guide/getting-funds.mdx | 38 ++-- src/pages/guide/machine-payments/index.mdx | 3 +- src/pages/guide/node/installation.mdx | 2 +- src/pages/guide/using-tempo-with-ai.mdx | 12 +- src/pages/wallet/index.mdx | 107 ++++------- src/pages/wallet/reference.mdx | 129 ++++++------- src/pages/wallet/troubleshooting.mdx | 99 ---------- vocs.config.ts | 174 ++++++++++-------- 12 files changed, 684 insertions(+), 328 deletions(-) create mode 100644 src/pages/cli/index.mdx create mode 100644 src/pages/cli/node.mdx create mode 100644 src/pages/cli/request.mdx create mode 100644 src/pages/cli/wallet.mdx delete mode 100644 src/pages/wallet/troubleshooting.mdx diff --git a/src/pages/cli/index.mdx b/src/pages/cli/index.mdx new file mode 100644 index 00000000..13f8e0f1 --- /dev/null +++ b/src/pages/cli/index.mdx @@ -0,0 +1,73 @@ +--- +title: Tempo CLI +description: A single binary for using Tempo Wallet from the terminal, making paid HTTP requests, and running a Tempo node. +--- + +import { Cards, Card } from 'vocs' + +# Tempo CLI + +The `tempo` binary covers three core workflows: + +- **`tempo wallet`** — use Tempo Wallet from the terminal: balances, funding, access keys, and service discovery +- **`tempo request`** — make paid HTTP requests with automatic [MPP](https://mpp.dev/overview) payment negotiation +- **`tempo node`** / **`tempo download`** — run and sync a Tempo node + +## Install + +:::code-group +```bash [Terminal] +curl -fsSL https://tempo.xyz/install | bash +``` +::: + +To update later, run `tempoup`. + +## Teach your agent to use Tempo + +Paste this into your AI agent to set up Tempo Wallet and start making paid requests: + +:::code-group +```bash [Claude Code] +claude -p "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" +``` +```bash [Amp] +amp -x "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" +``` +```bash [Codex CLI] +codex exec "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" +``` +::: + +All commands support `--help` for documentation and `--describe` for JSON command schemas. For scripts and agents, pass `-t` (`--toon-output`) to get compact, machine-readable output. + +## Commands + +Dive into each command: + + + + + + + diff --git a/src/pages/cli/node.mdx b/src/pages/cli/node.mdx new file mode 100644 index 00000000..52ea6327 --- /dev/null +++ b/src/pages/cli/node.mdx @@ -0,0 +1,94 @@ +--- +title: tempo node +description: Command reference for running a Tempo node. +--- + +import { Cards, Card } from 'vocs' + +# `tempo node` + +Run a Tempo node. For faster initial sync, first download a snapshot with [`tempo download`](/cli/download). For operational setup guides — system requirements, systemd configs, monitoring, validator onboarding — see [Run a Tempo Node](/guide/node). + +Flags grouped by function: + +### Network + +| Flag | Description | +| --- | --- | +| `--follow` | Run as a full node following a trusted RPC endpoint | +| `--chain ` | Target network (`mainnet`, `moderato`) | +| `--datadir ` | Data directory for chain state | + +### RPC server + +| Flag | Description | +| --- | --- | +| `--http` | Enable the JSON-RPC HTTP server | +| `--http.port ` | JSON-RPC port (default: `8545`) | +| `--http.addr ` | JSON-RPC bind address | +| `--http.api ` | Enabled API namespaces (e.g., `eth,net,web3,txpool,trace`) | + +### Consensus (validators) + +| Flag | Description | +| --- | --- | +| `--consensus.signing-key ` | Path to validator signing key | +| `--consensus.fee-recipient ` | Address to receive validator fees | +| `--consensus.datadir ` | Separate volume for consensus data | + +### Observability + +| Flag | Description | +| --- | --- | +| `--telemetry-url ` | Unified metrics and logs export endpoint | +| `--telemetry-metrics-interval ` | Metrics push interval (default: `10s`) | +| `--metrics ` | Enable Prometheus metrics on this port | + +## Examples + +Download a snapshot and start an RPC node: + +```bash +tempo node \ + --follow \ + --http --http.port 8545 \ + --http.api eth,net,web3,txpool,trace +``` + +Start a validator: + +```bash +tempo node --datadir /data/tempo \ + --chain mainnet \ + --consensus.signing-key /etc/tempo/key \ + --consensus.fee-recipient 0x... +``` + +## Learn more + + + + + + + diff --git a/src/pages/cli/request.mdx b/src/pages/cli/request.mdx new file mode 100644 index 00000000..3447de98 --- /dev/null +++ b/src/pages/cli/request.mdx @@ -0,0 +1,80 @@ +--- +title: tempo request +description: A curl-like HTTP client that handles MPP payment negotiation automatically. +--- + +import { Cards, Card } from 'vocs' + +# `tempo request` + +A curl-like HTTP client that handles [Machine Payments Protocol](https://mpp.dev/overview) negotiation transparently. When a server responds with [`402 Payment Required`](https://mpp.dev/protocol/http-402), `tempo request` reads the [challenge](https://mpp.dev/protocol/challenges), signs and submits the payment onchain, then retries with the [credential](https://mpp.dev/protocol/credentials) — all in one command. + +Requires [`tempo wallet login`](/cli/wallet) first. + +## Usage + +| Command | Description | +| --- | --- | +| `tempo request ` | Make an HTTP request with automatic payment | +| `tempo request --dry-run ` | Preview cost without executing payment | +| `tempo request -X POST --json '{...}'` | Send a JSON body | +| `tempo request -H 'Header: Value'` | Add a custom header | + +## Flags + +| Flag | Description | +| --- | --- | +| `-X ` | HTTP method (`GET`, `POST`, etc.) | +| `--json ` | Send a JSON body (implies `-X POST`) | +| `-H

` | Add a custom header | +| `--dry-run` | Preview the payment cost and validate the request without spending | +| `-t` / `--toon-output` | Compact machine-readable output for scripts and agents | + +## Examples + +Preview cost before paying: + +```bash +tempo request --dry-run -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev +``` + +Execute a paid request: + +```bash +tempo request -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev +``` + +Discover the right URL and request schema first with [`tempo wallet services`](/cli/wallet#service-discovery). + +## Learn more + + + + + + + diff --git a/src/pages/cli/wallet.mdx b/src/pages/cli/wallet.mdx new file mode 100644 index 00000000..d7e554be --- /dev/null +++ b/src/pages/cli/wallet.mdx @@ -0,0 +1,201 @@ +--- +title: tempo wallet +description: Use Tempo Wallet from the terminal — authenticate, check balances, manage access keys, and discover services. +--- + +import { Cards, Card } from 'vocs' + +# `tempo wallet` + +CLI interface for [Tempo Wallet](https://wallet.tempo.xyz), Tempo's web-based passkey wallet. Authenticate in your browser, then manage balances, access keys, funding, service discovery, and payment sessions from the terminal. + + + + + + + + +## Download + +```bash +curl -fsSL https://tempo.xyz/install | bash +``` + +To update later, run `tempoup`. Verify with `tempo --version`. + +## Authenticate + +```bash +tempo wallet login +``` + +Opens a browser flow to connect to your [Tempo Wallet](https://wallet.tempo.xyz). If you don't have one, the flow creates it. + +Once logged in, verify everything works: + +```bash +tempo wallet whoami +``` + +If `ready=true`, the wallet is ready for [`tempo request`](/cli/request). + +To disconnect: + +```bash +tempo wallet logout +``` + +## Use it + +### Check balances + +```bash +tempo wallet whoami +``` + +Shows your address, token balances, and key state. + +### Manage access keys + +```bash +tempo wallet keys +``` + +Each wallet can have multiple access keys with independent spending limits. Use these to constrain what an agent or script can spend. + +### Add funds + +```bash +tempo wallet fund +``` + +On testnet, this opens the faucet. On mainnet, it opens bridging and onramp options. + +To transfer tokens to another address: + +```bash +tempo wallet transfer +``` + +For more options, see [Getting Funds on Tempo](/guide/getting-funds). + +### Discover services + +```bash +tempo wallet services +tempo wallet services --search +tempo wallet services +``` + +The [Machine Payments Protocol](https://mpp.dev/overview) (MPP) lets any HTTP endpoint accept payments inline. The service directory indexes MPP-registered providers — each entry shows endpoint URLs, HTTP methods, pricing, and request schemas. Use it to find the right URL and payload for [`tempo request`](/cli/request). + +### Manage payment sessions + +When you use [pay-as-you-go](/guide/machine-payments/pay-as-you-go) services, MPP opens a [session](https://mpp.dev/payment-methods/tempo/session) — a payment channel where your wallet deposits funds into an escrow contract, then pays per request using signed [vouchers](https://mpp.dev/protocol/credentials) off-chain. This avoids an on-chain transaction for every request, giving sub-100ms latency and near-zero per-request fees. + +The CLI tracks session state locally: + +```bash +tempo wallet sessions list +tempo wallet sessions sync +tempo wallet sessions close --all +tempo wallet sessions close --orphaned +``` + +`sync` reconciles local records with onchain state. `close --orphaned` cleans up sessions whose counterparty is unreachable. Use `--dry-run` with any close command to preview before executing. + +## Command reference + +### Authentication + +| Command | Description | +| --- | --- | +| `tempo wallet login` | Connect or create a wallet via browser auth | +| `tempo wallet logout` | Disconnect and clear local credentials | +| `tempo wallet whoami` | Print readiness, address, balances, and key state | + +### Keys + +| Command | Description | +| --- | --- | +| `tempo wallet keys` | List keys and their spending limits | + +### Funds + +| Command | Description | +| --- | --- | +| `tempo wallet fund` | Fund wallet (faucet on testnet, bridge on mainnet) | +| `tempo wallet transfer ` | Transfer tokens to another address | + +### Services + +| Command | Description | +| --- | --- | +| `tempo wallet services` | List all registered services | +| `tempo wallet services --search ` | Filter services by keyword | +| `tempo wallet services ` | Show endpoints, methods, and request schemas | + +### Sessions + +| Command | Description | +| --- | --- | +| `tempo wallet sessions list` | List active payment sessions | +| `tempo wallet sessions sync` | Reconcile local sessions with onchain state | +| `tempo wallet sessions close --all` | Close all sessions | +| `tempo wallet sessions close --orphaned` | Close sessions whose counterparty is unreachable | + +### Advanced + +| Command | Description | +| --- | --- | +| `tempo wallet mpp-sign` | Sign an MPP payment challenge (used internally by `tempo request`) | + +## Learn more + + + + + + + diff --git a/src/pages/guide/getting-funds.mdx b/src/pages/guide/getting-funds.mdx index 65fecd4d..337f5aa4 100644 --- a/src/pages/guide/getting-funds.mdx +++ b/src/pages/guide/getting-funds.mdx @@ -1,13 +1,13 @@ --- title: Getting Funds on Tempo -description: Bridge assets to Tempo via LayerZero, Squid, or Relay, onramp through the Tempo Wallet, or get testnet funds from the faucet. +description: Bridge assets to Tempo, add funds in Tempo Wallet, or use the faucet on testnet. --- import { Cards, Card } from 'vocs' # Getting Funds on Tempo -There are several ways to get funds on Tempo: +You can get funds onto Tempo in three ways: @@ -32,26 +32,34 @@ There are several ways to get funds on Tempo: ## Bridge -Bridge assets from other chains to Tempo using one of the supported bridges: +Use one of these supported bridges to move assets to Tempo: -- **[LayerZero](https://layerzero.network/)**: Cross-chain messaging protocol supporting asset transfers to Tempo. -- **[Squid](https://app.squidrouter.com/)**: Cross-chain swaps and bridging powered by Axelar, with Tempo support. -- **[Relay](https://relay.link/)**: Instant bridging with low fees across supported chains including Tempo. +- **[LayerZero](https://layerzero.network/)**: Bridge supported assets from other chains to Tempo. +- **[Squid](https://app.squidrouter.com/)**: Swap and bridge assets to Tempo in one flow. +- **[Relay](https://relay.link/)**: Bridge assets to Tempo with low fees. ## Tempo Wallet -The [Tempo Wallet](https://wallet.tempo.xyz) supports onramping funds directly into your Tempo account. +[Tempo Wallet](https://wallet.tempo.xyz) is Tempo's web-based passkey wallet. You can add funds directly in the app. -### Via the Web +### In Tempo Wallet -1. Sign up or login to [Tempo Wallet](https://wallet.tempo.xyz) using your Passkey. -2. Press the "Add funds" button on the home dashboard, and choose your preferred option: +1. Sign up or log in to [Tempo Wallet](https://wallet.tempo.xyz) with your passkey. +2. Click **Add funds** on the home screen, then choose an option: -### Via an Agent +### With the CLI -Paste this into your agent to set up a Tempo Wallet and fund it: +Fund your wallet from the terminal using the [Tempo CLI](/cli): + +```bash +tempo wallet fund +``` + +### With an agent + +Paste this into your agent to set up Tempo Wallet and add funds: ``` Read https://wallet.tempo.xyz/SKILL.md and fund my Tempo Wallet @@ -59,6 +67,6 @@ Read https://wallet.tempo.xyz/SKILL.md and fund my Tempo Wallet ## Testnet Funds -To get testnet tokens for development, use the [Tempo Faucet](/quickstart/faucet). +For development and testing, use the [Tempo Faucet](/quickstart/faucet). -The faucet will drip you `pathUSD`, `alphaUSD`, `betaUSD`, and `thetaUSD` test stablecoins. +The faucet provides `pathUSD`, `alphaUSD`, `betaUSD`, and `thetaUSD` test stablecoins. diff --git a/src/pages/guide/machine-payments/index.mdx b/src/pages/guide/machine-payments/index.mdx index 9b8ef30e..ff1b48d9 100644 --- a/src/pages/guide/machine-payments/index.mdx +++ b/src/pages/guide/machine-payments/index.mdx @@ -98,8 +98,9 @@ Two [intents](https://mpp.dev/protocol#payment-intents) are available on Tempo: ## SDKs and tools -| SDK | Package | Install | +| Tool | Package | Install | |-----|---------|---------| +| CLI | [`tempo request`](/cli/request) | `curl -fsSL https://tempo.xyz/install \| bash` | | TypeScript | [`mppx`](https://github.com/wevm/mppx) | `npm install mppx viem` | | Python | [`pympp`](https://github.com/tempoxyz/pympp) | `pip install pympp` | | Rust | [`mpp-rs`](https://github.com/tempoxyz/mpp-rs) | `cargo add mpp` | diff --git a/src/pages/guide/node/installation.mdx b/src/pages/guide/node/installation.mdx index ed42fb9c..b9dc6b61 100644 --- a/src/pages/guide/node/installation.mdx +++ b/src/pages/guide/node/installation.mdx @@ -4,7 +4,7 @@ description: Install Tempo node using pre-built binaries, build from source with # Installation -We provide three different installation paths - installing a pre-built binary, building from source or using our provided Docker image. +We provide three different installation paths — installing a pre-built binary, building from source, or using our provided Docker image. For the full CLI command reference, see [`tempo node`](/cli/node). ## Versions diff --git a/src/pages/guide/using-tempo-with-ai.mdx b/src/pages/guide/using-tempo-with-ai.mdx index 13325cce..73f6bcb0 100644 --- a/src/pages/guide/using-tempo-with-ai.mdx +++ b/src/pages/guide/using-tempo-with-ai.mdx @@ -28,13 +28,21 @@ Tempo provides AI-assisted skills and tooling for the following: ## Tempo Wallet -The [Tempo Wallet](https://wallet.tempo.xyz) distributes a CLI with built in spend controls and service discovery. Agents can use `tempo wallet` to pay for powerful new capabilities on demand. +The [Tempo CLI](/cli) gives agents a wallet with built-in spend controls and service discovery. Agents can use [`tempo wallet`](/cli/wallet) to manage keys and balances, and [`tempo request`](/cli/request) to pay for services on demand. Paste this into your agent (Claude Code, Amp, Codex, etc.) to set up and start using a Tempo Wallet: +:::code-group +```bash [Claude Code] +claude -p "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" +``` +```bash [Amp] +amp -x "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" ``` -Read https://tempo.xyz/SKILL.md and set up a Tempo Wallet +```bash [Codex CLI] +codex exec "Read https://wallet.tempo.xyz/SKILL.md and set up tempo" ``` +::: ## Tempo Docs diff --git a/src/pages/wallet/index.mdx b/src/pages/wallet/index.mdx index 07171863..1171c808 100644 --- a/src/pages/wallet/index.mdx +++ b/src/pages/wallet/index.mdx @@ -1,104 +1,67 @@ --- -title: Tempo Wallet CLI -description: Learn how to use Tempo Wallet CLI and Tempo Request from your terminal, with quick onboarding guides and command references for daily workflows. +title: Tempo CLI +description: A terminal client for Tempo wallet management, service discovery, and paid HTTP requests via the Machine Payments Protocol. --- -import { Cards, Card, Tab, Tabs } from 'vocs' +# Tempo CLI -# Tempo Wallet CLI +The `tempo` CLI is a terminal client for Tempo. It has two command families: -Login and manage your Tempo Wallet from your terminal, with built-in [MPP](https://mpp.dev) support for service discovery and paid API requests. +- **`tempo wallet`**: manages your onchain identity: authentication, key management, balances, funding, and transfers. It also provides a service directory for discovering [MPP](https://mpp.dev)-compatible endpoints and their request schemas. +- **`tempo request`**: a curl-like HTTP client that handles [Machine Payments Protocol](https://mpp.dev) negotiation automatically. It sends your request, intercepts `402 Payment Required` challenges, signs and submits the payment onchain, then retries with the credential, all in a single command. -## Quickstart +Together they let you browse paid APIs, preview costs, and execute paid requests from a terminal, script, or AI agent without writing any integration code. - - - -Paste this into your agent to set up Tempo Wallet: - -
- -``` -Read https://wallet.tempo.xyz/SKILL.md and set up tempo -``` - -
- - -::::steps - -#### Install the CLI +## Install ```bash curl -fsSL https://tempo.xyz/install | bash ``` -#### Connect your wallet +## Authenticate ```bash tempo wallet login -``` - -#### Verify setup - -```bash tempo wallet whoami ``` -Expected result: wallet information is returned and you can continue to service discovery. +`login` opens a browser flow that creates or connects a Tempo wallet. `whoami` confirms readiness, prints your address, and shows token balances. -#### List available services +## Discover and call a paid API ```bash +# Find services tempo wallet services --search ai -``` -#### Make a paid request +# Inspect a service's endpoints and request schema +tempo wallet services -```bash +# Preview cost without paying +tempo request --dry-run -X POST \ + --json '{"prompt":"a sunset over the ocean"}' \ + https://fal.mpp.tempo.xyz/fal-ai/flux/dev + +# Execute the paid request tempo request -X POST \ --json '{"prompt":"a sunset over the ocean"}' \ https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` -#### Preview cost before running a new endpoint +## Scripting and agents + +The CLI is designed for non-interactive use. Two features matter here: + +- **`-t` (TOON output)**: compact, machine-readable output that minimizes token usage when consumed by an LLM or parsed by a script. +- **`--dry-run`**: previews the payment cost and validates the request shape without spending funds. Useful for agents that need to confirm cost before committing. + +To set up an AI agent (Claude Code, Amp, Codex) with wallet and request capabilities: -```bash -tempo request --dry-run -X POST \ - --json '{"prompt":"a sunset over the ocean"}' \ - https://fal.mpp.tempo.xyz/fal-ai/flux/dev ``` +Read https://wallet.tempo.xyz/SKILL.md and set up tempo +``` + +This installs `tempo-wallet` and `tempo-request` skills automatically. The agent can then discover services, preview costs, and make paid requests within scoped spending limits. + +## Next -:::: - - -
- -## Next Steps - - - - - - - +- [Reference](/wallet/reference): complete command and flag reference diff --git a/src/pages/wallet/reference.mdx b/src/pages/wallet/reference.mdx index 6ed2ce0a..5d946948 100644 --- a/src/pages/wallet/reference.mdx +++ b/src/pages/wallet/reference.mdx @@ -1,84 +1,85 @@ --- -title: Tempo Wallet CLI Reference -description: Command reference for tempo wallet and tempo request, including the most-used commands and flags for interactive and script-based usage. +title: Tempo CLI Reference +description: Complete command and flag reference for tempo wallet and tempo request. --- # Reference -## `tempo wallet` Commands +## `tempo wallet` -| Command | Purpose | +Manages your onchain identity and provides service discovery for [MPP](https://mpp.dev) endpoints. + +### Auth + +| Command | Description | | --- | --- | -| `tempo wallet login` | Connect or create your wallet via browser auth | -| `tempo wallet logout` | Disconnect your wallet | -| `tempo wallet whoami` | Show readiness, address, balances, and key state | -| `tempo wallet keys` | List keys and spending limits | -| `tempo wallet fund` | Fund wallet (faucet/bridge flow depends on network) | -| `tempo wallet transfer ` | Transfer tokens to another address | -| `tempo wallet services` | List available services | -| `tempo wallet services ` | Show service details and endpoints | -| `tempo wallet sessions list` | List payment sessions | -| `tempo wallet sessions sync` | Reconcile local sessions with on-chain state | -| `tempo wallet sessions close ...` | Close sessions by target or in bulk | -| `tempo wallet mpp-sign` | Sign an MPP payment challenge | - -### `tempo wallet` Mini Recipe - -```bash -tempo wallet whoami -tempo wallet keys -tempo wallet services --search ai -tempo wallet services -``` - -## `tempo request` Core Usage - -| Command | Purpose | +| `tempo wallet login` | Connect or create wallet via browser auth | +| `tempo wallet logout` | Disconnect wallet and clear local credentials | +| `tempo wallet whoami` | Print readiness, address, balances, and key state | + +### Keys + +Each wallet can have multiple access keys with independent spending limits. This is how you constrain what an agent or script can spend. + +| Command | Description | | --- | --- | -| `tempo request ` | Make an HTTP request with automatic payment handling | -| `tempo request --dry-run ` | Preview cost without executing payment | -| `tempo request --json '{...}'` | Send a JSON body | -| `tempo request -H 'Header: Value'` | Add custom headers | +| `tempo wallet keys` | List keys and their spending limits | -### `tempo request` Mini Recipe +### Funds -```bash -tempo request --dry-run -X POST \ - --json '{"prompt":"a sunset over the ocean"}' \ - https://fal.mpp.tempo.xyz/fal-ai/flux/dev +| Command | Description | +| --- | --- | +| `tempo wallet fund` | Fund wallet (faucet on testnet, bridge on mainnet) | +| `tempo wallet transfer ` | Transfer tokens to another address | -tempo request -X POST \ - --json '{"prompt":"a sunset over the ocean"}' \ - https://fal.mpp.tempo.xyz/fal-ai/flux/dev -``` +### Services -## Common Flags +The service directory indexes [MPP](https://mpp.dev)-registered providers. Each service entry includes endpoint URLs, HTTP methods, and expected request schemas, enough to construct a valid `tempo request` call. -| Flag | Works With | Purpose | -| --- | --- | --- | -| `-t` | `tempo wallet`, `tempo request` | TOON output (`--toon-output`): compact machine-readable output for scripts and agents | -| `--dry-run` | `tempo request`, `tempo wallet fund`, `tempo wallet sessions close` | Preview actions without executing | -| `--help` | all commands | Show full command/flag documentation | -| `--describe` | supported commands | Output command schema for programmatic tooling | +| Command | Description | +| --- | --- | +| `tempo wallet services` | List all registered services | +| `tempo wallet services --search ` | Filter services by keyword | +| `tempo wallet services ` | Show a service's endpoints, methods, and request schemas | -## Quick Examples +### Sessions -```bash -tempo wallet -t whoami -tempo wallet services --search ai -tempo wallet sessions close --dry-run --orphaned +Sessions are the local state for [pay-as-you-go](/guide/machine-payments/pay-as-you-go) payment channels. The CLI tracks them locally and can reconcile against onchain state. -tempo request --dry-run https://openrouter.mpp.tempo.xyz/v1/chat/completions \ - --json '{"model":"openai/gpt-4o-mini","messages":[{"role":"user","content":"Ping"}]}' -``` +| Command | Description | +| --- | --- | +| `tempo wallet sessions list` | List active payment sessions | +| `tempo wallet sessions sync` | Reconcile local sessions with onchain state | +| `tempo wallet sessions close --all` | Close all sessions | +| `tempo wallet sessions close --orphaned` | Close sessions whose counterparty is unreachable | -## Canonical Sources +### Other -1. Wallet extension repo: [`tempoxyz/wallet`](https://github.com/tempoxyz/wallet) -2. Full CLI help: `tempo wallet --help` and `tempo request --help` +| Command | Description | +| --- | --- | +| `tempo wallet mpp-sign` | Sign an MPP payment challenge (used internally by `tempo request`) | + +## `tempo request` + +A curl-like HTTP client that handles [MPP](https://mpp.dev) payment negotiation transparently. On a `402 Payment Required` response, it reads the challenge, signs and submits the payment, then retries with the credential. + +| Command | Description | +| --- | --- | +| `tempo request ` | Make an HTTP request with automatic payment | +| `tempo request --dry-run ` | Preview cost without executing payment | +| `tempo request --json '{...}'` | Send a JSON body (implies `-X POST`) | +| `tempo request -H 'Header: Value'` | Add a custom header | + +## Global flags + +| Flag | Scope | Description | +| --- | --- | --- | +| `-t` / `--toon-output` | `tempo wallet`, `tempo request` | Compact machine-readable output for scripts and agents | +| `--dry-run` | `tempo request`, `tempo wallet fund`, `tempo wallet sessions close` | Preview the action without executing | +| `--help` | all commands | Show command documentation | +| `--describe` | supported commands | Output command schema as JSON for programmatic tooling | -## See Also +## Source -1. [Recipes](/wallet/recipes) -2. [Use with Agents](/wallet/use-with-agents) -3. [Troubleshooting](/wallet/troubleshooting) +- Repository: [`tempoxyz/wallet`](https://github.com/tempoxyz/wallet) +- Full help: `tempo wallet --help`, `tempo request --help` diff --git a/src/pages/wallet/troubleshooting.mdx b/src/pages/wallet/troubleshooting.mdx deleted file mode 100644 index 582956a7..00000000 --- a/src/pages/wallet/troubleshooting.mdx +++ /dev/null @@ -1,99 +0,0 @@ ---- -title: Tempo Wallet CLI Troubleshooting -description: Troubleshoot common Tempo Wallet CLI issues including missing installs, login readiness failures, key provisioning errors, and paid request failures. ---- - -# Troubleshooting - -## Quick Diagnosis - -| Symptom | Run Next | -| --- | --- | -| `tempo: command not found` | Run the install command in the section below, then run `tempo --help` | -| `ready=false` | `tempo wallet login` then `tempo wallet -t whoami` | -| `No wallet configured` | `tempo wallet login` | -| `access key does not exist` | `tempo wallet logout --yes && tempo wallet login` | -| Request schema / `422` | `tempo wallet services ` and validate request body | -| Insufficient funds | `tempo wallet -t whoami` then `tempo wallet fund` | -| Session close confusion | `tempo wallet sessions close --dry-run --all` | - -## `tempo: command not found` - -Install Tempo CLI and retry: - -```bash -curl -fsSL https://tempo.xyz/install | bash -tempo --help -``` - -## Wallet Not Ready (`ready=false`) - -Run login and check status again: - -```bash -tempo wallet login -tempo wallet -t whoami -``` - -## `No wallet configured` or `Run 'tempo wallet login'` - -Your local auth/session is missing. - -```bash -tempo wallet login -tempo wallet whoami -``` - -## `Key is not provisioned` / `access key does not exist` - -Refresh auth state and reprovision: - -```bash -tempo wallet logout --yes -tempo wallet login -tempo wallet keys -``` - -## Request Fails With Schema / 422 Errors - -Usually this means body fields are wrong for the selected provider endpoint. - -```bash -tempo wallet services --search -tempo wallet services -``` - -Copy the exact method/path and expected request schema from service details. - -## Request Fails Due to Insufficient Funds - -Check wallet and fund: - -```bash -tempo wallet -t whoami -tempo wallet fund -``` - -## Session Close Behavior Is Unclear - -Preview before closing: - -```bash -tempo wallet sessions list -tempo wallet sessions close --dry-run --all -tempo wallet sessions close --dry-run --orphaned -``` - -Then execute the intended close command. - -## Still Stuck? - -1. Re-run with `--help` to confirm current flags and usage. -2. Use `-t` output (`tempo wallet -t ...`) for clearer machine-readable diagnostics. -3. Cross-check command behavior against [`tempoxyz/wallet`](https://github.com/tempoxyz/wallet). - -## See Also - -1. [Recipes](/wallet/recipes) -2. [Reference](/wallet/reference) -3. [Use with Agents](/wallet/use-with-agents) diff --git a/vocs.config.ts b/vocs.config.ts index a9142857..ab5a5340 100644 --- a/vocs.config.ts +++ b/vocs.config.ts @@ -521,108 +521,114 @@ export default defineConfig({ ], }, { - text: 'Tempo Wallet CLI', + text: 'Tempo Developer Tools', collapsed: true, items: [ { - text: 'Overview', - link: '/wallet', - }, - { - text: 'Use with Agents', - link: '/wallet/use-with-agents', - }, - { - text: 'Recipes', - link: '/wallet/recipes', - }, - { - text: 'Reference', - link: '/wallet/reference', - }, - { - text: 'Troubleshooting', - link: '/wallet/troubleshooting', - }, - ], - }, - { - text: 'Tempo SDKs', - collapsed: true, - items: [ - { - text: 'Overview', - link: '/sdk', - }, - { - text: 'TypeScript', + text: 'CLI', collapsed: true, items: [ { text: 'Overview', - link: '/sdk/typescript', + link: '/cli', }, { - text: 'Viem Reference', - link: 'https://viem.sh/tempo', + text: 'Wallet', + link: '/cli/wallet', }, { - text: 'Wagmi Reference', - link: 'https://wagmi.sh/tempo', + text: 'Request', + link: '/cli/request', }, { - text: 'Server Reference', + text: 'Download', + link: '/cli/download', + }, + { + text: 'Node', + link: '/cli/node', + }, + ], + }, + { + text: 'SDKs', + collapsed: true, + items: [ + { + text: 'Overview', + link: '/sdk', + }, + { + text: 'TypeScript', + collapsed: true, items: [ { - text: 'Handlers', + text: 'Overview', + link: '/sdk/typescript', + }, + { + text: 'Viem Reference', + link: 'https://viem.sh/tempo', + }, + { + text: 'Wagmi Reference', + link: 'https://wagmi.sh/tempo', + }, + { + text: 'Server Reference', items: [ { - text: 'Overview', - link: '/sdk/typescript/server/handlers', - }, - { - text: 'compose', - link: '/sdk/typescript/server/handler.compose', - }, - { - text: 'feePayer', - link: '/sdk/typescript/server/handler.feePayer', + text: 'Handlers', + items: [ + { + text: 'Overview', + link: '/sdk/typescript/server/handlers', + }, + { + text: 'compose', + link: '/sdk/typescript/server/handler.compose', + }, + { + text: 'feePayer', + link: '/sdk/typescript/server/handler.feePayer', + }, + { + text: 'keyManager', + link: '/sdk/typescript/server/handler.keyManager', + }, + ], }, + ], + }, + { + text: 'Prool Reference', + items: [ { - text: 'keyManager', - link: '/sdk/typescript/server/handler.keyManager', + text: 'Setup', + link: '/sdk/typescript/prool/setup', }, ], }, ], }, { - text: 'Prool Reference', - items: [ - { - text: 'Setup', - link: '/sdk/typescript/prool/setup', - }, - ], + text: 'Go', + link: '/sdk/go', + }, + { + text: 'Foundry', + link: '/sdk/foundry', + }, + { + text: 'Python', + link: '/sdk/python', + }, + { + text: 'Rust', + link: '/sdk/rust', }, ], }, - { - text: 'Go', - link: '/sdk/go', - }, - { - text: 'Foundry', - link: '/sdk/foundry', - }, - { - text: 'Python', - link: '/sdk/python', - }, - { - text: 'Rust', - link: '/sdk/rust', - }, ], }, { @@ -835,6 +841,26 @@ export default defineConfig({ source: '/sdk/typescript/prool', destination: '/sdk/typescript/prool/setup', }, + { + source: '/wallet', + destination: '/cli', + status: 301, + }, + { + source: '/wallet/reference', + destination: '/cli/wallet', + status: 301, + }, + { + source: '/wallet/:path*', + destination: '/cli/:path*', + status: 301, + }, + { + source: '/cli/reference', + destination: '/cli/wallet', + status: 301, + }, { source: '/guide/use-accounts/fee-sponsorship', destination: '/guide/payments/sponsor-user-fees', From a44f633bb4fda1d846563d911d149bd9f0e4ca83 Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 21:08:25 -0400 Subject: [PATCH 39/40] chore: sync from tempoxyz/docs (#47) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: remove thirdweb from developer tools page (#146) Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> * fix(docs): add fee recipient language (#147) * feat: add Google Analytics (gtag.js) via static HTML head (#150) * fix(node): prep for mainnet (#144) * fix: revert vocs.config.tsx to .ts to fix Vercel SSR (#151) The Vocs SSR bundle hardcodes an import to dist/server/vocs.config.js. When the source file is .tsx, Vite outputs it differently, causing ERR_MODULE_NOT_FOUND and 500 errors on every page. - Rename vocs.config.tsx back to vocs.config.ts - Replace JSX with React.createElement calls for gtag head config - Update biome.json override to match new filename * fix: load env vars into process.env for vocs.config.ts (#152) Use Vite's loadEnv() to populate process.env with VITE_* env vars before the vocs() plugin initializes. Without this, VITE_GA_MEASUREMENT_ID is not available when vocs.config.ts is evaluated, causing the Google Analytics gtag to be silently skipped. This matches the pattern used in the mpp site's vite.config.ts. * fix: use Vite plugin for Google Analytics (vocs v2 compat) (#153) * fix: use transformIndexHtml for GA instead of vocs.config head Vocs v2 removed the `head` config property — it existed in v1 but is silently ignored in v2. Move Google Analytics injection to a Vite plugin using transformIndexHtml, which correctly injects script tags into the HTML during build. Also keep the loadEnv fix so VITE_* env vars from .env files are available to all plugins. * fix: use React component for GA instead of dead transformIndexHtml Vocs v2 uses Waku (RSC) which bypasses Vite's HTML pipeline entirely. transformIndexHtml is never invoked. Use a client-side React component in _layout.tsx instead, matching the pattern used for Vercel Analytics and PostHog. * fix: sql editor theme sync (#157) --------- Co-authored-by: Derek Cofausper <256792747+decofe@users.noreply.github.com> Co-authored-by: joshitzko <82132285+joshitzko@users.noreply.github.com> Co-authored-by: zhygis <5236121+Zygimantass@users.noreply.github.com> Co-authored-by: Brendan Ryan --- src/pages/quickstart/developer-tools.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/quickstart/developer-tools.mdx b/src/pages/quickstart/developer-tools.mdx index 497e55c6..fb65c517 100644 --- a/src/pages/quickstart/developer-tools.mdx +++ b/src/pages/quickstart/developer-tools.mdx @@ -227,7 +227,7 @@ You can get started now. Simply [create](https://docs.privy.io/wallets/wallets/c Check out Privy's [example](https://github.com/privy-io/examples/tree/main/examples/privy-next-tempo) peer-to-peer payments app that uses Tempo transaction memos. ::: -#### Turnkey +### Turnkey [Turnkey](https://www.turnkey.com) provides programmable key management and non-custodial wallet infrastructure for applications that need granular signing policies and automated transaction flows. With Turnkey, developers can securely sign Tempo transactions, automate wallet operations, and build custom logic around how keys are used. From 9cb152a58ed89d6e40373650aa60a10fbc62e76f Mon Sep 17 00:00:00 2001 From: tmm Date: Tue, 17 Mar 2026 21:10:36 -0400 Subject: [PATCH 40/40] chore: biome format Amp-Thread-ID: https://ampcode.com/threads/T-019cfdd1-6acc-74a0-b7b6-e79bb18273e8 --- src/components/MermaidDiagram.tsx | 870 +++++++++++++++--------------- src/components/TerminalDemo.tsx | 288 +++++----- 2 files changed, 568 insertions(+), 590 deletions(-) diff --git a/src/components/MermaidDiagram.tsx b/src/components/MermaidDiagram.tsx index b5783219..8b06449f 100644 --- a/src/components/MermaidDiagram.tsx +++ b/src/components/MermaidDiagram.tsx @@ -1,6 +1,6 @@ -"use client"; +'use client' -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from 'react' // --------------------------------------------------------------------------- // Layout constants @@ -37,149 +37,149 @@ export const LAYOUT = { blockLabelFontWeight: 600, messageStroke: 1.2, lifelineStroke: 0.75, -}; +} export interface ThemeColors { - text: string; - textMuted: string; - line: string; - lifeline: string; - arrow: string; - successArrow: string; - errorCode: string; - actorFill: string; - actorStroke: string; - blockStroke: string; - blockHeaderBg: string; - badgeBg: string; - badgeText: string; + text: string + textMuted: string + line: string + lifeline: string + arrow: string + successArrow: string + errorCode: string + actorFill: string + actorStroke: string + blockStroke: string + blockHeaderBg: string + badgeBg: string + badgeText: string } -export const THEMES: Record<"light" | "dark", ThemeColors> = { +export const THEMES: Record<'light' | 'dark', ThemeColors> = { light: { - text: "#27272a", - textMuted: "#3f3f46", - line: "#a1a1aa", - lifeline: "#d4d4d8", - arrow: "#0166ff", - successArrow: "#16a34a", - errorCode: "#dc2626", - actorFill: "#ffffff", - actorStroke: "#e4e4e7", - blockStroke: "#e4e4e7", - blockHeaderBg: "#f4f4f5", - badgeBg: "#e4e4e7", - badgeText: "#52525b", + text: '#27272a', + textMuted: '#3f3f46', + line: '#a1a1aa', + lifeline: '#d4d4d8', + arrow: '#0166ff', + successArrow: '#16a34a', + errorCode: '#dc2626', + actorFill: '#ffffff', + actorStroke: '#e4e4e7', + blockStroke: '#e4e4e7', + blockHeaderBg: '#f4f4f5', + badgeBg: '#e4e4e7', + badgeText: '#52525b', }, dark: { - text: "#e4e4e7", - textMuted: "#e4e4e7", - line: "#71717a", - lifeline: "#3f3f46", - arrow: "#60a5fa", - successArrow: "#4ade80", - errorCode: "#f87171", - actorFill: "#27272a", - actorStroke: "#3f3f46", - blockStroke: "#3f3f46", - blockHeaderBg: "#27272a", - badgeBg: "#3f3f46", - badgeText: "#a1a1aa", + text: '#e4e4e7', + textMuted: '#e4e4e7', + line: '#71717a', + lifeline: '#3f3f46', + arrow: '#60a5fa', + successArrow: '#4ade80', + errorCode: '#f87171', + actorFill: '#27272a', + actorStroke: '#3f3f46', + blockStroke: '#3f3f46', + blockHeaderBg: '#27272a', + badgeBg: '#3f3f46', + badgeText: '#a1a1aa', }, -}; +} // --------------------------------------------------------------------------- // Parser // --------------------------------------------------------------------------- export interface Participant { - id: string; - label: string; + id: string + label: string } export type Step = | { - type: "message"; - from: string; - to: string; - label: string; - num: string | null; - dashed: boolean; + type: 'message' + from: string + to: string + label: string + num: string | null + dashed: boolean } - | { type: "note"; over: string; text: string; num: string | null } - | { type: "loop-start"; label: string } - | { type: "loop-end" }; + | { type: 'note'; over: string; text: string; num: string | null } + | { type: 'loop-start'; label: string } + | { type: 'loop-end' } export interface ParsedDiagram { - participants: Participant[]; - steps: Step[]; + participants: Participant[] + steps: Step[] } export function extractNum(text: string): { num: string | null; rest: string } { - const m = text.match(/^\((\d+)\)\s*(.+)$/); - return m ? { num: m[1], rest: m[2] } : { num: null, rest: text }; + const m = text.match(/^\((\d+)\)\s*(.+)$/) + return m ? { num: m[1], rest: m[2] } : { num: null, rest: text } } export function parse(source: string): ParsedDiagram { const lines = source - .split("\n") + .split('\n') .map((l) => l.trim()) - .filter((l) => l && !l.startsWith("%%")); - const participants: Participant[] = []; - const steps: Step[] = []; - const seen = new Set(); + .filter((l) => l && !l.startsWith('%%')) + const participants: Participant[] = [] + const steps: Step[] = [] + const seen = new Set() const ensure = (id: string) => { if (!seen.has(id)) { - seen.add(id); - participants.push({ id, label: id }); + seen.add(id) + participants.push({ id, label: id }) } - }; + } for (const line of lines) { - if (line === "sequenceDiagram") continue; - const mPartAs = line.match(/^participant\s+(\S+)\s+as\s+(.+)$/i); + if (line === 'sequenceDiagram') continue + const mPartAs = line.match(/^participant\s+(\S+)\s+as\s+(.+)$/i) if (mPartAs) { - seen.add(mPartAs[1]); - participants.push({ id: mPartAs[1], label: mPartAs[2].trim() }); - continue; + seen.add(mPartAs[1]) + participants.push({ id: mPartAs[1], label: mPartAs[2].trim() }) + continue } - const mPart = line.match(/^participant\s+(\S+)$/i); + const mPart = line.match(/^participant\s+(\S+)$/i) if (mPart) { - ensure(mPart[1]); - continue; + ensure(mPart[1]) + continue } - const mNote = line.match(/^Note\s+over\s+(\S+?)\s*:\s*(.+)$/i); + const mNote = line.match(/^Note\s+over\s+(\S+?)\s*:\s*(.+)$/i) if (mNote) { - ensure(mNote[1]); - const e = extractNum(mNote[2].trim()); - steps.push({ type: "note", over: mNote[1], text: e.rest, num: e.num }); - continue; + ensure(mNote[1]) + const e = extractNum(mNote[2].trim()) + steps.push({ type: 'note', over: mNote[1], text: e.rest, num: e.num }) + continue } - const mLoop = line.match(/^loop\s+(.+)$/i); + const mLoop = line.match(/^loop\s+(.+)$/i) if (mLoop) { - steps.push({ type: "loop-start", label: mLoop[1].trim() }); - continue; + steps.push({ type: 'loop-start', label: mLoop[1].trim() }) + continue } if (/^end$/i.test(line)) { - steps.push({ type: "loop-end" }); - continue; + steps.push({ type: 'loop-end' }) + continue } - const mMsg = line.match(/^(\S+?)(--?>>)(\S+?)\s*:\s*(.+)$/); + const mMsg = line.match(/^(\S+?)(--?>>)(\S+?)\s*:\s*(.+)$/) if (mMsg) { - ensure(mMsg[1]); - ensure(mMsg[3]); - const e = extractNum(mMsg[4].trim()); + ensure(mMsg[1]) + ensure(mMsg[3]) + const e = extractNum(mMsg[4].trim()) steps.push({ - type: "message", + type: 'message', from: mMsg[1], to: mMsg[3], label: e.rest, num: e.num, - dashed: mMsg[2] === "-->>", - }); + dashed: mMsg[2] === '-->>', + }) } } - return { participants, steps }; + return { participants, steps } } // --------------------------------------------------------------------------- @@ -187,79 +187,77 @@ export function parse(source: string): ParsedDiagram { // --------------------------------------------------------------------------- export interface LMsg { - x1: number; - x2: number; - y: number; - label: string; - num: string | null; - labelX: number; - labelY: number; - dashed: boolean; - si: number; - isLast: boolean; + x1: number + x2: number + y: number + label: string + num: string | null + labelX: number + labelY: number + dashed: boolean + si: number + isLast: boolean } export interface LNote { - text: string; - num: string | null; - x: number; - y: number; - boxX: number; - boxY: number; - boxW: number; - boxH: number; - lines: string[]; - si: number; + text: string + num: string | null + x: number + y: number + boxX: number + boxY: number + boxW: number + boxH: number + lines: string[] + si: number } export interface LActor { - cx: number; - boxX: number; - boxY: number; - boxW: number; - boxH: number; - label: string; + cx: number + boxX: number + boxY: number + boxW: number + boxH: number + label: string } export interface LBlock { - label: string; - x: number; - y: number; - w: number; - h: number; + label: string + x: number + y: number + w: number + h: number } export interface LLifeline { - x: number; - y1: number; - y2: number; + x: number + y1: number + y2: number } export interface Layout { - w: number; - h: number; - actors: LActor[]; - lifelines: LLifeline[]; - messages: LMsg[]; - notes: LNote[]; - blocks: LBlock[]; - msgCount: number; + w: number + h: number + actors: LActor[] + lifelines: LLifeline[] + messages: LMsg[] + notes: LNote[] + blocks: LBlock[] + msgCount: number } export function doLayout(p: ParsedDiagram): Layout { - const L = LAYOUT; - const n = p.participants.length; - const gap = n === 2 ? L.actorGap2 : L.actorGap; - - const aw = p.participants.map( - (a) => estW(a.label, L.actorFontSize) + L.actorPadX * 2, - ); - const cx: number[] = []; - let xc = L.padding + aw[0] / 2; + const L = LAYOUT + const n = p.participants.length + const gap = n === 2 ? L.actorGap2 : L.actorGap + + const aw = p.participants.map((a) => estW(a.label, L.actorFontSize) + L.actorPadX * 2) + const cx: number[] = [] + let xc = L.padding + aw[0] / 2 for (let i = 0; i < n; i++) { - if (i > 0) xc += Math.max(gap, (aw[i - 1] + aw[i]) / 2 + 60); - cx.push(xc); + if (i > 0) xc += Math.max(gap, (aw[i - 1] + aw[i]) / 2 + 60) + cx.push(xc) } - const idx = new Map(); - for (let i = 0; i < n; i++) idx.set(p.participants[i].id, i); + const idx = new Map() + for (let i = 0; i < n; i++) idx.set(p.participants[i].id, i) - const bY = L.padding; + const bY = L.padding const actors: LActor[] = p.participants.map((a, i) => ({ cx: cx[i], boxX: cx[i] - aw[i] / 2, @@ -267,30 +265,30 @@ export function doLayout(p: ParsedDiagram): Layout { boxW: aw[i], boxH: L.actorBoxH, label: a.label, - })); + })) - let y = bY + L.actorBoxH + L.headerGap; - const messages: LMsg[] = []; - const notes: LNote[] = []; - const blocks: LBlock[] = []; - const bStack: { label: string; x: number; y: number }[] = []; - const rightEdge = cx[n - 1] + aw[n - 1] / 2; - const leftEdge = cx[0] - aw[0] / 2; - const midX = (cx[0] + cx[n - 1]) / 2; + let y = bY + L.actorBoxH + L.headerGap + const messages: LMsg[] = [] + const notes: LNote[] = [] + const blocks: LBlock[] = [] + const bStack: { label: string; x: number; y: number }[] = [] + const rightEdge = cx[n - 1] + aw[n - 1] / 2 + const leftEdge = cx[0] - aw[0] / 2 + const midX = (cx[0] + cx[n - 1]) / 2 // Count total messages to identify the last one - let totalMsgs = 0; + let totalMsgs = 0 for (const s of p.steps) { - if (s.type === "message") totalMsgs++; + if (s.type === 'message') totalMsgs++ } - let msgIdx = 0; + let msgIdx = 0 for (let si = 0; si < p.steps.length; si++) { - const s = p.steps[si]; - if (s.type === "message") { - const fi = idx.get(s.from) ?? 0; - const ti = idx.get(s.to) ?? 0; - msgIdx++; + const s = p.steps[si] + if (s.type === 'message') { + const fi = idx.get(s.from) ?? 0 + const ti = idx.get(s.to) ?? 0 + msgIdx++ messages.push({ x1: cx[fi], x2: cx[ti], @@ -302,18 +300,16 @@ export function doLayout(p: ParsedDiagram): Layout { dashed: s.dashed, si, isLast: msgIdx === totalMsgs, - }); - y += L.rowHeight; - } else if (s.type === "note") { - const maxNW = (rightEdge - leftEdge) * 0.8; - const wrapped = wrapText(s.text, maxNW, L.noteFontSize); - const lineH = L.noteFontSize + 4; - const boxW = - Math.max(...wrapped.map((t) => estW(t, L.noteFontSize))) + - L.noteBoxPadX * 2; - const boxH = wrapped.length * lineH + L.noteBoxPadY * 2; - const boxX = midX - boxW / 2; - const boxY = y - boxH / 2; + }) + y += L.rowHeight + } else if (s.type === 'note') { + const maxNW = (rightEdge - leftEdge) * 0.8 + const wrapped = wrapText(s.text, maxNW, L.noteFontSize) + const lineH = L.noteFontSize + 4 + const boxW = Math.max(...wrapped.map((t) => estW(t, L.noteFontSize))) + L.noteBoxPadX * 2 + const boxH = wrapped.length * lineH + L.noteBoxPadY * 2 + const boxX = midX - boxW / 2 + const boxY = y - boxH / 2 notes.push({ text: s.text, num: s.num, @@ -325,39 +321,39 @@ export function doLayout(p: ParsedDiagram): Layout { boxH, lines: wrapped, si, - }); - y += L.rowHeight + L.noteExtraMargin; - } else if (s.type === "loop-start") { + }) + y += L.rowHeight + L.noteExtraMargin + } else if (s.type === 'loop-start') { bStack.push({ label: s.label, x: leftEdge - L.blockPadX, y: y - L.blockPadTop / 2, - }); - y += L.blockPadTop; - } else if (s.type === "loop-end") { - const blk = bStack.pop(); + }) + y += L.blockPadTop + } else if (s.type === 'loop-end') { + const blk = bStack.pop() if (blk) { - const bw = rightEdge + L.blockPadX - blk.x; + const bw = rightEdge + L.blockPadX - blk.x blocks.push({ label: blk.label, x: blk.x, y: blk.y, w: bw, h: y - blk.y + L.blockPadBottom, - }); - y += L.blockPadBottom; + }) + y += L.blockPadBottom } } } - const llBot = y - L.rowHeight / 2; + const llBot = y - L.rowHeight / 2 const lifelines: LLifeline[] = cx.map((lx) => ({ x: lx, y1: bY + L.actorBoxH, y2: llBot, - })); - const totalW = rightEdge + L.padding; - const totalH = llBot + L.padding; + })) + const totalW = rightEdge + L.padding + const totalH = llBot + L.padding return { w: totalW, @@ -368,32 +364,28 @@ export function doLayout(p: ParsedDiagram): Layout { notes, blocks, msgCount: totalMsgs, - }; + } } export function estW(text: string, fontSize: number): number { - return text.length * fontSize * 0.6; + return text.length * fontSize * 0.6 } -export function wrapText( - text: string, - maxW: number, - fontSize: number, -): string[] { - const words = text.split(/\s+/); - const lines: string[] = []; - let cur = ""; +export function wrapText(text: string, maxW: number, fontSize: number): string[] { + const words = text.split(/\s+/) + const lines: string[] = [] + let cur = '' for (const word of words) { - const test = cur ? `${cur} ${word}` : word; + const test = cur ? `${cur} ${word}` : word if (estW(test, fontSize) > maxW && cur) { - lines.push(cur); - cur = word; + lines.push(cur) + cur = word } else { - cur = test; + cur = test } } - if (cur) lines.push(cur); - return lines.length > 0 ? lines : [text]; + if (cur) lines.push(cur) + return lines.length > 0 ? lines : [text] } // --------------------------------------------------------------------------- @@ -401,27 +393,27 @@ export function wrapText( // --------------------------------------------------------------------------- export function render(lo: Layout, th: ThemeColors): string { - const L = LAYOUT; - const o: string[] = []; - const sz = L.arrowSize; - const br = L.badgeR; + const L = LAYOUT + const o: string[] = [] + const sz = L.arrowSize + const br = L.badgeR o.push( '', - ); - o.push(``); + ) + o.push(``) // Gradient for last (success) message line — use userSpaceOnUse to avoid // zero-height bounding box issues on horizontal elements. - const lastMsg = lo.messages.find((m) => m.isLast); + const lastMsg = lo.messages.find((m) => m.isLast) if (lastMsg) { o.push( '', - ); + ) } // Lifelines @@ -452,12 +444,12 @@ export function render(lo: Layout, th: ThemeColors): string { '" stroke-width="' + L.lifelineStroke + '" stroke-dasharray="6 4"/>', - ); + ) } // Blocks for (const b of lo.blocks) { - const tw = estW(b.label, L.blockLabelFontSize) + 20; + const tw = estW(b.label, L.blockLabelFontSize) + 20 o.push( '', - ); + ) o.push( '', - ); + ) o.push( '' + esc(b.label) + - "", - ); + '', + ) } // Actors @@ -517,7 +509,7 @@ export function render(lo: Layout, th: ThemeColors): string { '" stroke="' + th.actorStroke + '" stroke-width="1"/>', - ); + ) o.push( '' + esc(a.label) + - "", - ); + '', + ) } // Messages for (const m of lo.messages) { - const da = m.dashed ? ' stroke-dasharray="6 4"' : ""; - const goingRight = m.x2 > m.x1; - const lineEndX = goingRight ? m.x2 - sz : m.x2 + sz; - const lineStroke = m.isLast ? "url(#grad-success)" : th.line; + const da = m.dashed ? ' stroke-dasharray="6 4"' : '' + const goingRight = m.x2 > m.x1 + const lineEndX = goingRight ? m.x2 - sz : m.x2 + sz + const lineStroke = m.isLast ? 'url(#grad-success)' : th.line // Solid arrows (->>): filled triangle; dashed arrows (-->>): outline triangle const arrowFill = m.isLast ? m.dashed @@ -548,8 +540,8 @@ export function render(lo: Layout, th: ThemeColors): string { : th.successArrow : m.dashed ? th.actorFill - : th.line; - const arrowStroke = m.isLast ? th.successArrow : th.line; + : th.line + const arrowStroke = m.isLast ? th.successArrow : th.line // Line o.push( @@ -569,43 +561,43 @@ export function render(lo: Layout, th: ThemeColors): string { L.messageStroke + '"' + da + - "/>", - ); + '/>', + ) // Arrow - const tipX = m.x2; - const baseX = goingRight ? tipX - sz : tipX + sz; + const tipX = m.x2 + const baseX = goingRight ? tipX - sz : tipX + sz o.push( '', - ); + ) // Compute label text width to place badge to its left - const labelW = estW(m.label, L.labelFontSize); - const totalLabelW = labelW + (m.num ? br * 2 + 6 : 0); - const groupLeft = m.labelX - totalLabelW / 2; + const labelW = estW(m.label, L.labelFontSize) + const totalLabelW = labelW + (m.num ? br * 2 + 6 : 0) + const groupLeft = m.labelX - totalLabelW / 2 // Badge (subtle bg color, not blue) if (m.num) { - const bcx = groupLeft + br; - const bcy = m.labelY; + const bcx = groupLeft + br + const bcy = m.labelY o.push( '', - ); + ) o.push( '' + m.num + - "", - ); + '', + ) } // Label text (to the right of badge) - const textX = m.num ? groupLeft + br * 2 + 6 + labelW / 2 : m.labelX; + const textX = m.num ? groupLeft + br * 2 + 6 + labelW / 2 : m.labelX o.push( '' + highlightLabel(m.label, th) + - "", - ); + '', + ) } // Notes — rounded box with wrapped text, no italic for (const nt of lo.notes) { - const lineH = L.noteFontSize + 4; + const lineH = L.noteFontSize + 4 // Recenter box now that boxW includes badge space - const centeredBoxX = nt.x - nt.boxW / 2; - const textStartY = nt.boxY + L.noteBoxPadY + L.noteFontSize; + const centeredBoxX = nt.x - nt.boxW / 2 + const textStartY = nt.boxY + L.noteBoxPadY + L.noteFontSize o.push( '', - ); + ) if (nt.num) { - const bx = centeredBoxX + L.noteBoxPadX + br; - const by = nt.boxY + nt.boxH / 2; + const bx = centeredBoxX + L.noteBoxPadX + br + const by = nt.boxY + nt.boxH / 2 o.push( '', - ); + ) o.push( '' + nt.num + - "", - ); + '', + ) } - const textX = nt.num - ? centeredBoxX + L.noteBoxPadX + br * 2 + 6 - : centeredBoxX + L.noteBoxPadX; + const textX = nt.num ? centeredBoxX + L.noteBoxPadX + br * 2 + 6 : centeredBoxX + L.noteBoxPadX for (let li = 0; li < nt.lines.length; li++) { o.push( '' + esc(nt.lines[li]) + - "", - ); + '', + ) } } - o.push(""); - return o.join("\n"); + o.push('') + return o.join('\n') } // Syntax highlight HTTP codes and methods in labels export function highlightLabel(label: string, th: ThemeColors): string { // Tokenize: split label into segments with optional color overrides - const re = - /(GET|POST|PUT|DELETE|PATCH|\b[45]\d{2}\b|\b2\d{2}\s*OK\b|\b2\d{2}\b)/g; - let lastIdx = 0; - let result = ""; - let match: RegExpExecArray | null = re.exec(label); + const re = /(GET|POST|PUT|DELETE|PATCH|\b[45]\d{2}\b|\b2\d{2}\s*OK\b|\b2\d{2}\b)/g + let lastIdx = 0 + let result = '' + let match: RegExpExecArray | null = re.exec(label) while (match !== null) { // Text before match if (match.index > lastIdx) { - result += esc(label.slice(lastIdx, match.index)); + result += esc(label.slice(lastIdx, match.index)) } - const tok = match[0]; - let color = th.textMuted; - if (/^(GET|POST|PUT|DELETE|PATCH)$/.test(tok)) color = th.arrow; - else if (/^[45]\d{2}$/.test(tok)) color = th.errorCode; - else if (/^2\d{2}/.test(tok)) color = th.successArrow; - result += `${esc(tok)}`; - lastIdx = match.index + tok.length; - match = re.exec(label); + const tok = match[0] + let color = th.textMuted + if (/^(GET|POST|PUT|DELETE|PATCH)$/.test(tok)) color = th.arrow + else if (/^[45]\d{2}$/.test(tok)) color = th.errorCode + else if (/^2\d{2}/.test(tok)) color = th.successArrow + result += `${esc(tok)}` + lastIdx = match.index + tok.length + match = re.exec(label) } // Remaining text if (lastIdx < label.length) { - result += esc(label.slice(lastIdx)); + result += esc(label.slice(lastIdx)) } - return result; + return result } export function esc(s: string): string { return s - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """); + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') } // --------------------------------------------------------------------------- @@ -785,17 +774,17 @@ export function esc(s: string): string { // --------------------------------------------------------------------------- export interface AnimationHandle { - skipToEnd: () => void; + skipToEnd: () => void } export function showAllItems(svg: SVGSVGElement) { - svg.style.opacity = "1"; + svg.style.opacity = '1' for (const el of svg.querySelectorAll( - "[data-step],[data-step-arrow],[data-step-label],[data-step-note]", + '[data-step],[data-step-arrow],[data-step-label],[data-step-note]', )) { - el.style.transition = "none"; - el.style.opacity = "1"; - el.style.strokeDashoffset = "0"; + el.style.transition = 'none' + el.style.opacity = '1' + el.style.strokeDashoffset = '0' } } @@ -805,126 +794,123 @@ export function animate( onStart: () => void, ): AnimationHandle { type Item = { - si: number; - draw?: SVGElement; - arrow?: SVGElement; - fade: SVGElement[]; - isNote: boolean; - }; - const map = new Map(); + si: number + draw?: SVGElement + arrow?: SVGElement + fade: SVGElement[] + isNote: boolean + } + const map = new Map() const get = (i: number, isNote = false) => { - if (!map.has(i)) map.set(i, { si: i, fade: [], isNote }); - return map.get(i)!; - }; - - svg.querySelectorAll("[data-step]").forEach((el) => { - get(+(el.dataset.step ?? 0)).draw = el; - }); - svg.querySelectorAll("[data-step-arrow]").forEach((el) => { - get(+(el.dataset.stepArrow ?? 0)).arrow = el; - }); - svg.querySelectorAll("[data-step-label]").forEach((el) => { - get(+(el.dataset.stepLabel ?? 0)).fade.push(el); - }); - svg.querySelectorAll("[data-step-note]").forEach((el) => { - const item = get(+(el.dataset.stepNote ?? 0), true); - item.isNote = true; - item.fade.push(el); - }); - - const timeline = Array.from(map.values()).sort((a, b) => a.si - b.si); - let skipped = false; - const timers: ReturnType[] = []; + if (!map.has(i)) map.set(i, { si: i, fade: [], isNote }) + return map.get(i)! + } + + svg.querySelectorAll('[data-step]').forEach((el) => { + get(+(el.dataset.step ?? 0)).draw = el + }) + svg.querySelectorAll('[data-step-arrow]').forEach((el) => { + get(+(el.dataset.stepArrow ?? 0)).arrow = el + }) + svg.querySelectorAll('[data-step-label]').forEach((el) => { + get(+(el.dataset.stepLabel ?? 0)).fade.push(el) + }) + svg.querySelectorAll('[data-step-note]').forEach((el) => { + const item = get(+(el.dataset.stepNote ?? 0), true) + item.isNote = true + item.fade.push(el) + }) + + const timeline = Array.from(map.values()).sort((a, b) => a.si - b.si) + let skipped = false + const timers: ReturnType[] = [] const handle: AnimationHandle = { skipToEnd() { - if (skipped) return; - skipped = true; - for (const t of timers) clearTimeout(t); - showAllItems(svg); - onComplete(); + if (skipped) return + skipped = true + for (const t of timers) clearTimeout(t) + showAllItems(svg) + onComplete() }, - }; + } if (!timeline.length) { - svg.style.opacity = "1"; - onComplete(); - return handle; + svg.style.opacity = '1' + onComplete() + return handle } - svg.style.opacity = "1"; + svg.style.opacity = '1' for (const item of timeline) { if (item.draw) { - const len = lineLen(item.draw); - item.draw.style.strokeDasharray = `${len}`; - item.draw.style.strokeDashoffset = `${len}`; - item.draw.style.opacity = "0"; + const len = lineLen(item.draw) + item.draw.style.strokeDasharray = `${len}` + item.draw.style.strokeDashoffset = `${len}` + item.draw.style.opacity = '0' } - if (item.arrow) item.arrow.style.opacity = "0"; - for (const el of item.fade) el.style.opacity = "0"; + if (item.arrow) item.arrow.style.opacity = '0' + for (const el of item.fade) el.style.opacity = '0' } const obs = new IntersectionObserver( ([e]) => { - if (!e.isIntersecting) return; - obs.disconnect(); - onStart(); - let lastDelay = 0; - let cumDelay = 800; + if (!e.isIntersecting) return + obs.disconnect() + onStart() + let lastDelay = 0 + let cumDelay = 800 for (let i = 0; i < timeline.length; i++) { - const item = timeline[i]; - const delay = cumDelay; - if (delay > lastDelay) lastDelay = delay; - cumDelay += item.isNote ? 2000 : 1200; + const item = timeline[i] + const delay = cumDelay + if (delay > lastDelay) lastDelay = delay + cumDelay += item.isNote ? 2000 : 1200 - const drawEl = item.draw; - const arrowEl = item.arrow; + const drawEl = item.draw + const arrowEl = item.arrow timers.push( setTimeout(() => { - if (skipped) return; + if (skipped) return if (drawEl) { - drawEl.style.transition = - "opacity 0.3s ease, stroke-dashoffset 1.2s ease-out"; - drawEl.style.opacity = "1"; - drawEl.style.strokeDashoffset = "0"; + drawEl.style.transition = 'opacity 0.3s ease, stroke-dashoffset 1.2s ease-out' + drawEl.style.opacity = '1' + drawEl.style.strokeDashoffset = '0' } for (const el of item.fade) { - el.style.transition = drawEl - ? "opacity 0.6s ease" - : "opacity 0.8s ease"; - el.style.opacity = "1"; + el.style.transition = drawEl ? 'opacity 0.6s ease' : 'opacity 0.8s ease' + el.style.opacity = '1' } if (arrowEl) { timers.push( setTimeout(() => { - if (skipped) return; - arrowEl.style.transition = "opacity 0.3s ease"; - arrowEl.style.opacity = "1"; + if (skipped) return + arrowEl.style.transition = 'opacity 0.3s ease' + arrowEl.style.opacity = '1' }, 1000), - ); + ) } }, delay), - ); + ) } timers.push( setTimeout(() => { - if (!skipped) onComplete(); + if (!skipped) onComplete() }, lastDelay + 1800), - ); + ) }, { threshold: 0.15 }, - ); - obs.observe(svg); + ) + obs.observe(svg) - return handle; + return handle } export function lineLen(el: SVGElement): number { - const x1 = +(el.getAttribute("x1") || 0); - const x2 = +(el.getAttribute("x2") || 0); - const y1 = +(el.getAttribute("y1") || 0); - const y2 = +(el.getAttribute("y2") || 0); - return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + const x1 = +(el.getAttribute('x1') || 0) + const x2 = +(el.getAttribute('x2') || 0) + const y1 = +(el.getAttribute('y1') || 0) + const y2 = +(el.getAttribute('y2') || 0) + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) } // --------------------------------------------------------------------------- @@ -932,113 +918,113 @@ export function lineLen(el: SVGElement): number { // --------------------------------------------------------------------------- export function MermaidDiagram({ chart }: { chart: string }) { - const wrapperRef = useRef(null); - const svgRef = useRef(null); - const animRef = useRef(null); - const [isDark, setIsDark] = useState(false); - const [phase, setPhase] = useState<"idle" | "playing" | "done">("idle"); + const wrapperRef = useRef(null) + const svgRef = useRef(null) + const animRef = useRef(null) + const [isDark, setIsDark] = useState(false) + const [phase, setPhase] = useState<'idle' | 'playing' | 'done'>('idle') useEffect(() => { const check = () => setIsDark( - document.documentElement.style.colorScheme === "dark" || - document.documentElement.classList.contains("dark"), - ); - check(); - const obs = new MutationObserver(check); + document.documentElement.style.colorScheme === 'dark' || + document.documentElement.classList.contains('dark'), + ) + check() + const obs = new MutationObserver(check) obs.observe(document.documentElement, { attributes: true, - attributeFilter: ["class", "style"], - }); - return () => obs.disconnect(); - }, []); + attributeFilter: ['class', 'style'], + }) + return () => obs.disconnect() + }, []) const renderDiagram = useCallback(() => { - const el = svgRef.current; - if (!el || !el.isConnected) return; - setPhase("idle"); + const el = svgRef.current + if (!el || !el.isConnected) return + setPhase('idle') try { - const parsed = parse(chart); - const lo = doLayout(parsed); - const th = isDark ? THEMES.dark : THEMES.light; - el.innerHTML = render(lo, th); - const svg = el.querySelector("svg"); - if (!svg) return; - svg.style.maxWidth = "100%"; - svg.style.height = "auto"; - svg.style.display = "block"; - svg.style.margin = "0 auto"; + const parsed = parse(chart) + const lo = doLayout(parsed) + const th = isDark ? THEMES.dark : THEMES.light + el.innerHTML = render(lo, th) + const svg = el.querySelector('svg') + if (!svg) return + svg.style.maxWidth = '100%' + svg.style.height = 'auto' + svg.style.display = 'block' + svg.style.margin = '0 auto' animRef.current = animate( svg, - () => setPhase("done"), - () => setPhase("playing"), - ); + () => setPhase('done'), + () => setPhase('playing'), + ) } catch (err) { - console.error("MermaidDiagram:", err); + console.error('MermaidDiagram:', err) } - }, [chart, isDark]); + }, [chart, isDark]) useEffect(() => { - const el = svgRef.current; - if (!el) return; - let dead = false; + const el = svgRef.current + if (!el) return + let dead = false const raf = requestAnimationFrame(() => { - if (!dead) renderDiagram(); - }); + if (!dead) renderDiagram() + }) return () => { - dead = true; - cancelAnimationFrame(raf); - el.innerHTML = ""; - }; - }, [renderDiagram]); + dead = true + cancelAnimationFrame(raf) + el.innerHTML = '' + } + }, [renderDiagram]) - const th = isDark ? THEMES.dark : THEMES.light; + const th = isDark ? THEMES.dark : THEMES.light const btnStyle: React.CSSProperties = { - position: "absolute", + position: 'absolute', top: 12, right: 12, width: 28, height: 28, - borderRadius: "50%", + borderRadius: '50%', border: `1px solid ${th.actorStroke}`, background: th.actorFill, color: th.textMuted, - display: "flex", - alignItems: "center", - justifyContent: "center", - cursor: "pointer", + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + cursor: 'pointer', padding: 0, opacity: 0.7, - transition: "opacity 0.2s", - }; + transition: 'opacity 0.2s', + } return (
- {phase === "playing" && ( + {phase === 'playing' && ( )} - {phase === "done" && ( + {phase === 'done' && (
- ); + ) } diff --git a/src/components/TerminalDemo.tsx b/src/components/TerminalDemo.tsx index 68440bcf..32e1a76a 100644 --- a/src/components/TerminalDemo.tsx +++ b/src/components/TerminalDemo.tsx @@ -1,24 +1,24 @@ -"use client"; +'use client' -import { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from 'react' // --------------------------------------------------------------------------- // Constants & helpers // --------------------------------------------------------------------------- -const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'] function randomHex(bytes: number) { - const arr = crypto.getRandomValues(new Uint8Array(bytes)); - return `0x${Array.from(arr, (b) => b.toString(16).padStart(2, "0")).join("")}`; + const arr = crypto.getRandomValues(new Uint8Array(bytes)) + return `0x${Array.from(arr, (b) => b.toString(16).padStart(2, '0')).join('')}` } function randomAddress() { - return randomHex(20); + return randomHex(20) } function randomTxHash() { - return randomHex(32); + return randomHex(32) } // --------------------------------------------------------------------------- @@ -26,17 +26,12 @@ function randomTxHash() { // --------------------------------------------------------------------------- function Spinner() { - const [frame, setFrame] = useState(0); + const [frame, setFrame] = useState(0) useEffect(() => { - const timer = setInterval( - () => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), - 80, - ); - return () => clearInterval(timer); - }, []); - return ( - {SPINNER_FRAMES[frame]} - ); + const timer = setInterval(() => setFrame((f) => (f + 1) % SPINNER_FRAMES.length), 80) + return () => clearInterval(timer) + }, []) + return {SPINNER_FRAMES[frame]} } // biome-ignore format: contains unicode ✔︎ @@ -53,7 +48,7 @@ function StepIcon({ spinning }: { spinning: boolean }) { } function BlankLine() { - return
; + return
} function TruncatedHex({ hash }: { hash: string }) { @@ -64,7 +59,7 @@ function TruncatedHex({ hash }: { hash: string }) { {hash} - ); + ) } // --------------------------------------------------------------------------- @@ -72,39 +67,36 @@ function TruncatedHex({ hash }: { hash: string }) { // --------------------------------------------------------------------------- function PhotoOutput({ url }: { url: string }) { - const [loaded, setLoaded] = useState(false); + const [loaded, setLoaded] = useState(false) return (
{!loaded && ( -
+
)} Generated setLoaded(true)} - className="absolute inset-0 w-full h-full object-cover" + className="absolute inset-0 h-full w-full object-cover" style={{ - transition: "opacity 0.5s", + transition: 'opacity 0.5s', opacity: loaded ? 1 : 0, }} />
- ); + ) } // --------------------------------------------------------------------------- @@ -117,101 +109,102 @@ function ChargeSteps({ address, onDone, }: { - endpoint: string; - output: string; - address: string; - onDone: () => void; + endpoint: string + output: string + address: string + onDone: () => void }) { - const txHash = useMemo(() => randomTxHash(), []); - const doneCalled = useRef(false); + const txHash = useMemo(() => randomTxHash(), []) + const doneCalled = useRef(false) const steps = useMemo( () => [ - { key: "wallet", delay: 600 }, - { key: "fund", delay: 1500 }, - { key: "req402", delay: 500 }, - { key: "pay", delay: 500 }, - { key: "req200", delay: 500 }, + { key: 'wallet', delay: 600 }, + { key: 'fund', delay: 1500 }, + { key: 'req402', delay: 500 }, + { key: 'pay', delay: 500 }, + { key: 'req200', delay: 500 }, ], [], - ); + ) - const [step, setStep] = useState(0); - const currentKey = steps[step]?.key ?? "done"; + const [step, setStep] = useState(0) + const currentKey = steps[step]?.key ?? 'done' const pastStep = (key: string) => { - const idx = steps.findIndex((s) => s.key === key); - return idx !== -1 && step > idx; - }; + const idx = steps.findIndex((s) => s.key === key) + return idx !== -1 && step > idx + } const atOrPast = (key: string) => { - const idx = steps.findIndex((s) => s.key === key); - return idx !== -1 && step >= idx; - }; - const atStep = (key: string) => currentKey === key; + const idx = steps.findIndex((s) => s.key === key) + return idx !== -1 && step >= idx + } + const atStep = (key: string) => currentKey === key useEffect(() => { - if (currentKey === "done") { + if (currentKey === 'done') { if (!doneCalled.current) { - doneCalled.current = true; - onDone(); + doneCalled.current = true + onDone() } - return; + return } - const delay = steps[step].delay; - const timer = setTimeout(() => setStep((s) => s + 1), delay); - return () => clearTimeout(timer); - }, [step, currentKey, steps, onDone]); + const delay = steps[step].delay + const timer = setTimeout(() => setStep((s) => s + 1), delay) + return () => clearTimeout(timer) + }, [step, currentKey, steps, onDone]) return (
- {atOrPast("wallet") && ( -

- Create a wallet{" "} - {" "} + {atOrPast('wallet') && ( +

+ Create a wallet{' '} + {' '}

)} - {atOrPast("fund") && ( -

- Add test funds{" "} - {" "} - 100 USD + {atOrPast('fund') && ( +

+ Add test funds{' '} + {' '} + 100 USD

)} {/* biome-ignore format: contains unicode → */} - {atOrPast("req402") && ( -

- Call {endpoint} - {pastStep("req402") && ( + {atOrPast('req402') && ( +

+ Call {endpoint} + {pastStep('req402') && ( <> - {" "}→ 402{" "} - (payment required) + {' '} + → 402{' '} + (payment required) )}

)} - {atOrPast("pay") && ( -

- Fulfill payment - {pastStep("pay") && ( + {atOrPast('pay') && ( +

+ Fulfill payment + {pastStep('pay') && ( <> - {" "} - {" "} + {' '} + {' '} {txHash.slice(0, 6)}…{txHash.slice(-4)} @@ -220,18 +213,19 @@ function ChargeSteps({

)} {/* biome-ignore format: contains unicode → */} - {atOrPast("req200") && ( -

- Call {endpoint} - {pastStep("req200") && ( + {atOrPast('req200') && ( +

+ Call {endpoint} + {pastStep('req200') && ( <> - {" "}→ 200{" "} - (success) + {' '} + → 200{' '} + (success) )}

)} - {pastStep("req200") && ( + {pastStep('req200') && ( <> @@ -239,7 +233,7 @@ function ChargeSteps({ )}
- ); + ) } // --------------------------------------------------------------------------- @@ -250,16 +244,16 @@ function CssTriangle() { return ( - ); + ) } // --------------------------------------------------------------------------- @@ -267,100 +261,100 @@ function CssTriangle() { // --------------------------------------------------------------------------- export function TerminalDemo({ className }: { className?: string }) { - const [started, setStarted] = useState(false); - const [done, setDone] = useState(false); - const [key, setKey] = useState(0); - const [address] = useState(() => randomAddress()); - const [photoSeed, setPhotoSeed] = useState(() => Math.random().toString(36).slice(2)); - const photoUrl = `https://picsum.photos/seed/${photoSeed}/400/400`; + const [started, setStarted] = useState(false) + const [done, setDone] = useState(false) + const [key, setKey] = useState(0) + const [address] = useState(() => randomAddress()) + const [photoSeed, setPhotoSeed] = useState(() => Math.random().toString(36).slice(2)) + const photoUrl = `https://picsum.photos/seed/${photoSeed}/400/400` - const scrollRef = useRef(null); - const contentRef = useRef(null); + const scrollRef = useRef(null) + const contentRef = useRef(null) // Auto-scroll when content grows useEffect(() => { - const scrollEl = scrollRef.current; - const contentEl = contentRef.current; - if (!scrollEl || !contentEl) return; + const scrollEl = scrollRef.current + const contentEl = contentRef.current + if (!scrollEl || !contentEl) return const observer = new ResizeObserver(() => { scrollEl.scrollTo({ top: scrollEl.scrollHeight - scrollEl.clientHeight, - behavior: "smooth", - }); - }); - observer.observe(contentEl); - return () => observer.disconnect(); - }, []); + behavior: 'smooth', + }) + }) + observer.observe(contentEl) + return () => observer.disconnect() + }, []) const restart = () => { - setStarted(false); - setDone(false); - setPhotoSeed(Math.random().toString(36).slice(2)); - setKey((k) => k + 1); - }; + setStarted(false) + setDone(false) + setPhotoSeed(Math.random().toString(36).slice(2)) + setKey((k) => k + 1) + } return (
{/* Title bar */}
-

- Press Enter or click to start -

+

Press Enter or click to start

)} @@ -424,7 +416,7 @@ export function TerminalDemo({ className }: { className?: string }) {
- ); + ) }