Lab kit
The shared pieces for the Lab pages. Everything here is simulated except the figures marked Live, which follow real Base blocks.
Brackets
Four 10 px L corners, 1.5 px, ink at 50%. The frame for stages and cards; the parent is position: relative.
Content sits inside the corners.
CodePane · single file
Server-highlighted (paper in light, espresso in dark). The verify range stays lit; the button pulses the pay range.
import { getPaymentStatus, pay } from '@base-org/account'const MERCHANT = '0xYourMerchantAddress'// In the browser: one call opens Base Pay. No account, no card form.export async function checkout(orderId: string) { const payment = await pay({ amount: '3.50', to: MERCHANT, testnet: false }) await fetch('/api/orders/paid', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ orderId, paymentId: payment.id }), })}// On the server: check the payment against your own order record, then fulfil.// Make fulfil() idempotent, so one payment id never ships twice.export async function confirm(paymentId: string, order: { amount: string; fulfil: () => Promise<void> }) { const status = await getPaymentStatus({ id: paymentId, expectedPayment: { amount: order.amount, recipient: MERCHANT }, testnet: false, }) if (status.status === 'completed') await order.fulfil() return status.status}CodePane · tabs
Three files, one pane. Tabs are a tablist: Left, Right, Home and End move between them. Each file keeps its own lit range.
import { createPublicClient, createWalletClient, custom, erc20Abi, http, parseUnits, type Address } from 'viem'import { base } from 'viem/chains'const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // native USDC on Base, 6 decimalsconst wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) })const client = createPublicClient({ chain: base, transport: http() })export async function send(to: Address, dollars: string) { const [account] = await wallet.requestAddresses() // Dry-run the transfer first: it fails here, not onchain, if the balance is short. const { request } = await client.simulateContract({ account, address: USDC, abi: erc20Abi, functionName: 'transfer', args: [to, parseUnits(dollars, 6)], }) // The wallet asks the sender to confirm, then broadcasts. const hash = await wallet.writeContract(request) // Preconfirmed in about 200 ms, included in a 2 s block. return client.waitForTransactionReceipt({ hash })}CodePane · collapse
swap.ts is 38 lines, so it opens with 16 and a footer. A lit range below the fold opens the block itself.
import { createPublicClient, createWalletClient, custom, erc20Abi, http, parseAbi, parseUnits } from 'viem'import { base } from 'viem/chains'const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'const WETH = '0x4200000000000000000000000000000000000006'const QUOTER = '0x3d4e44Eb1374240CE5F1B871ab261CD16335B76a' // Uniswap v3 QuoterV2const ROUTER = '0x2626664c2603336E57B271c5C0b26F421741e481' // Uniswap v3 SwapRouter02const quoterAbi = parseAbi([ 'function quoteExactInputSingle((address tokenIn, address tokenOut, uint256 amountIn, uint24 fee, uint160 sqrtPriceLimitX96) params) returns (uint256 amountOut, uint160 sqrtPriceX96After, uint32 initializedTicksCrossed, uint256 gasEstimate)',])const routerAbi = parseAbi([ 'function exactInputSingle((address tokenIn, address tokenOut, uint24 fee, address recipient, uint256 amountIn, uint256 amountOutMinimum, uint160 sqrtPriceLimitX96) params) payable returns (uint256 amountOut)',])// Connect a wallet on Base and pick an amount: 1,000 USDC, which has 6 decimals.const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) })const client = createPublicClient({ chain: base, transport: http() })const [account] = await wallet.requestAddresses()const amountIn = parseUnits('1000', 6)// Ask the 0.05% pool what that buys right now. The Quoter simulates the swap; nothing is sent.const { result: [quoted] } = await client.simulateContract({ address: QUOTER, abi: quoterAbi, functionName: 'quoteExactInputSingle', args: [{ tokenIn: USDC, tokenOut: WETH, amountIn, fee: 500, sqrtPriceLimitX96: 0n }],})// Let the router pull the USDC.const approval = await wallet.writeContract({ account, address: USDC, abi: erc20Abi, functionName: 'approve', args: [ROUTER, amountIn],})await client.waitForTransactionReceipt({ hash: approval })// Swap, and refuse to fill for less than 99.5% of the quote.const swap = await wallet.writeContract({ account, address: ROUTER, abi: routerAbi, functionName: 'exactInputSingle', args: [{ tokenIn: USDC, tokenOut: WETH, fee: 500, recipient: account, amountIn, amountOutMinimum: quoted * 9950n / 10000n, sqrtPriceLimitX96: 0n }],})await client.waitForTransactionReceipt({ hash: swap })import { createPublicClient, createWalletClient, custom, erc20Abi, http, parseAbi, parseUnits } from 'viem'import { base } from 'viem/chains'const USDC = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'const VAULT = '0xBEEFE94c8aD530842bfE7d8B397938fFc1cb83b2' // Steakhouse Prime USDC, on Morphoconst vaultAbi = parseAbi([ 'function deposit(uint256 assets, address receiver) returns (uint256 shares)', 'function balanceOf(address owner) view returns (uint256)', 'function convertToAssets(uint256 shares) view returns (uint256)',])// Connect a wallet on Base and pick an amount: 100 USDC, which has 6 decimals.const wallet = createWalletClient({ chain: base, transport: custom(window.ethereum!) })const client = createPublicClient({ chain: base, transport: http() })const [account] = await wallet.requestAddresses()const amount = parseUnits('100', 6)// Let the vault pull the USDC, then deposit it in exchange for vault shares.const approval = await wallet.writeContract({ account, address: USDC, abi: erc20Abi, functionName: 'approve', args: [VAULT, amount],})await client.waitForTransactionReceipt({ hash: approval })const deposit = await wallet.writeContract({ account, address: VAULT, abi: vaultAbi, functionName: 'deposit', args: [amount, account],})await client.waitForTransactionReceipt({ hash: deposit })// The shares grow in value every block. This is what they're worth right now.const shares = await client.readContract({ address: VAULT, abi: vaultAbi, functionName: 'balanceOf', args: [account] })const value = await client.readContract({ address: VAULT, abi: vaultAbi, functionName: 'convertToAssets', args: [shares] })CodeFrame · plain lines (JSON)
Built on the client, not by Shiki, through the same chrome. The passing checks are lit.
{ "tx": "0x3b1c…9e04", "checks": { "signature": true, "nonce": true, "balance": true, "gasLimit": false }, "valid": false}CodePane · no gutter, no wrap
A one-line shell command, as a static block.
npm install viem @base-org/accountFlourish · FallingPixel and Stamp
A pixel that falls into a changed figure, and a mono stamp that rises under it. Parent is position: relative.
Readout
Label + tags, value + unit, as-of. minCh reserves the digits' width so an odometer never shifts layout. hero seats it in the page's one well.
Panel · KV rows
A titled group with one provenance footer; KV rows with dotted leaders, read together.
Fee receipt
Sim- Price impact
- 0.06%
- Pool fee
- $23.00
- Price after
- +0.13%
- Gas
- $0.0112
- Deposited
- $10,000.00
- Earned
- $36.18
- Withdrawn
- $2,500.00
- Balance
- $7,536.18
Stat group
2–4 equal figures side by side; auto-fit minmax(140px, 1fr).
Ledger
Rows that are appended or compared. The newest row carries the rail; past 8 rows the body scrolls under a top fade.
Swap receipt
Sim| Who | Before | After | Change |
|---|---|---|---|
| me · USDC | 10,000.00 | 9,900.00 | −100.00 |
| pool · USDC | 25,899.18 | 25,999.18 | +100.00 |
| me · WETH | 4.20 | 5.19 | +0.99 |
| pool · WETH | 1,204.11 | 1,203.12 | −0.99 |
Interest per block
Sim| Block | Interest |
|---|---|
| 52,511,751 | +$0.0000012 |
| 52,511,750 | +$0.0000012 |
| 52,511,749 | +$0.0000012 |
| 52,511,748 | +$0.0000012 |
| 52,511,747 | +$0.0000012 |
| 52,511,746 | +$0.0000012 |
| 52,511,745 | +$0.0000012 |
| 52,511,744 | +$0.0000012 |
| 52,511,743 | +$0.0000012 |
| 52,511,742 | +$0.0000012 |
| 52,511,741 | +$0.0000012 |
| 52,511,740 | +$0.0000012 |
Unavailable
The pending well: hatched texture, one line, WAITING — never LIVE.
Pool
WaitingPool unavailable at block 52,511,752 · retrying on the next block
Instrument next to an article card
Editorial: serif prose, hover ripple. Instrument: mono, hairlines, tabular numbers, still.
Pool
SimLiveTag
Live reads Base; Sim is simulated; Waiting marks an unavailable or empty well and never reads as Live.
Odometer
Changed digits roll 8 px over 300 ms. Cells key by digit place, so the value can grow without jitter.
PhoneFrame
Solid bezel, island, status row, home indicator. By default the screen is fixed and scrolls; fit lets the device grow with its content.
- Coffee−$4.20
- Transfer from Ana+$120.00
- Groceries−$38.15
- Coffee−$4.20
- Transfer from Ana+$120.00
- Groceries−$38.15
- Rent share−$640.00
- Refund+$12.99
- Train pass−$54.00
- Lunch−$11.40
- Payout+$310.00
- Books−$22.80
- Gym−$29.00
- Transfer to Sam−$45.00
- Interest+$0.84
- Phone bill−$35.00
- Market−$17.60
- Coffee−$4.20
- Transfer from Ana+$120.00
- Coffee−$4.20
- Transfer from Ana+$120.00
- Groceries−$38.15
- Rent share−$640.00
- Refund+$12.99
- Train pass−$54.00
- Lunch−$11.40
- Payout+$310.00
- Books−$22.80
- Gym−$29.00
- Transfer to Sam−$45.00
- Interest+$0.84
- Phone bill−$35.00
- Market−$17.60
Scrubber · log $100 – $1,000,000
A native range under a drawn track. Keyboard steps are a share of the track, so a log scale moves in log steps.
Segmented
A tablist with a sliding indicator; selection follows focus. The badge is a 5 px square after a tab's label: something changed there.
Selected: day
Ink for a change the reader made, blue for one the chain made. The selected tab never shows one.
Flash
A tint behind the changed value, fading over 600 ms. Ink for what the reader did, --chain for what the chain did. Replay with the button.
Delta chip
The signed change beside a value: + and a real minus sign (U+2212). Enters, holds 1.2 s, fades. Never for a zero change or a block tick.
LabStage
One action, three views: app, code, chain. Signals flash each pane where a change landed, 60 ms apart; on mobile a hidden pane's tab gains a badge.
Balance
$1,000.00
Each tap moves the balance and lands in a block. Next block changes the chain only.
import { parseUnits } from 'viem'
// USDC has 6 decimals
const assets = parseUnits('1000.00', 6)Signals: app 0 · code 0 · chain 0
Tray
Prose behind a disclosure. The one sanctioned height animation, out of its trigger.