From 48e3bcb1d2474793c9e6fdf5deb037566af6259e Mon Sep 17 00:00:00 2001 From: Haardik Date: Wed, 30 Sep 2026 11:13:58 -0400 Subject: [PATCH 1/5] feat(vibenet): add 200ms reflex demo; updates agents.md Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a0f2c2-6770-7506-9d76-d1fb352c1208 --- AGENTS.md | 1 + app/analytics/events.ts | 6 + app/sitemap.ts | 1 + app/vibenet/demos/200/block-runner/layout.tsx | 12 + app/vibenet/demos/200/block-runner/page.tsx | 20 ++ app/vibenet/demos/200/layout.tsx | 8 +- app/vibenet/demos/200/page.tsx | 80 ++++- app/vibenet/demos/200/reflex/ReflexGame.tsx | 312 ++++++++++++++++++ app/vibenet/demos/200/reflex/layout.tsx | 11 + app/vibenet/demos/200/reflex/lib/game.test.ts | 15 + app/vibenet/demos/200/reflex/lib/game.ts | 27 ++ app/vibenet/demos/200/reflex/page.tsx | 5 + .../demos/account/useAccountEngine.tsx | 26 +- app/vibenet/demos/catalogue.test.ts | 18 + app/vibenet/demos/catalogue.ts | 38 ++- llms.config.mjs | 2 +- 16 files changed, 549 insertions(+), 33 deletions(-) create mode 100644 app/vibenet/demos/200/block-runner/layout.tsx create mode 100644 app/vibenet/demos/200/block-runner/page.tsx create mode 100644 app/vibenet/demos/200/reflex/ReflexGame.tsx create mode 100644 app/vibenet/demos/200/reflex/layout.tsx create mode 100644 app/vibenet/demos/200/reflex/lib/game.test.ts create mode 100644 app/vibenet/demos/200/reflex/lib/game.ts create mode 100644 app/vibenet/demos/200/reflex/page.tsx diff --git a/AGENTS.md b/AGENTS.md index 19daf88a..70d48e89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,6 +126,7 @@ This app uses Vercel Web Analytics. Two things must stay in place: | `trackExplorerActiveBlockJump(chain, jump)` | `app/internal-explorer/components/ActiveBlockButton.tsx` — zeronet latest/previous active block | | `trackValidityOrder(side, status)` | `app/vibenet/demos/validity/ValidityDemo.tsx` — conditional swap submit / include / expiry / replace | | `trackValidityRace(attempt, status)` | `app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx` — validity/manual comparison attempts | + | `trackReflexRun(status)` | `app/vibenet/demos/200/reflex/ReflexGame.tsx` — reaction-game starts and outcomes | Add a helper (and a row here) for a new key journey; remove the helper if you remove its surface. Confirm the wiring with `grep -rn "analytics/events" app`. diff --git a/app/analytics/events.ts b/app/analytics/events.ts index b65d81da..ccbd7fcd 100644 --- a/app/analytics/events.ts +++ b/app/analytics/events.ts @@ -91,3 +91,9 @@ export function trackValidityRace( ): void { track('validity_race', { attempt, status }); } + +// --- 200ms Reflex demo --- + +export function trackReflexRun(status: 'started' | 'human' | 'block' | 'tie' | 'error'): void { + track('reflex_run', { status }); +} diff --git a/app/sitemap.ts b/app/sitemap.ts index 090ae7db..6069e809 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -21,6 +21,7 @@ export default function sitemap(): MetadataRoute.Sitemap { { path: '/vibenet/explorer', priority: 0.6, changeFrequency: 'daily' }, { path: '/vibenet/faucet', priority: 0.5, changeFrequency: 'monthly' }, { path: '/vibenet/demos/200', priority: 0.5, changeFrequency: 'weekly' }, + { path: '/vibenet/demos/200/reflex', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/account', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/b20', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/validity', priority: 0.5, changeFrequency: 'weekly' }, diff --git a/app/vibenet/demos/200/block-runner/layout.tsx b/app/vibenet/demos/200/block-runner/layout.tsx new file mode 100644 index 00000000..e03122c9 --- /dev/null +++ b/app/vibenet/demos/200/block-runner/layout.tsx @@ -0,0 +1,12 @@ +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; + +export const metadata: Metadata = { + title: 'Block Runner · 200ms Blocks', + description: 'A pixel runner paced by Vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', + robots: { index: false, follow: false }, +}; + +export default function BlockRunnerLayout({ children }: { children: ReactNode }) { + return <>{children}; +} diff --git a/app/vibenet/demos/200/block-runner/page.tsx b/app/vibenet/demos/200/block-runner/page.tsx new file mode 100644 index 00000000..6bdac241 --- /dev/null +++ b/app/vibenet/demos/200/block-runner/page.tsx @@ -0,0 +1,20 @@ +import { Text } from '../../../../components/ui/Text'; +import { BlockRunner } from '../BlockRunner'; + +export default function BlockRunnerPage() { + return ( +
+
+ + Vibenet · 200 ms blocks + + Block Runner + + Every obstacle is a real Vibenet block, pushed to the screen the moment the chain seals it. Tap to bite a + block, read its number and slot, and see how long you can keep up. + +
+ +
+ ); +} diff --git a/app/vibenet/demos/200/layout.tsx b/app/vibenet/demos/200/layout.tsx index 182f20a5..093afbd0 100644 --- a/app/vibenet/demos/200/layout.tsx +++ b/app/vibenet/demos/200/layout.tsx @@ -1,13 +1,11 @@ import type { Metadata } from 'next'; import type { ReactNode } from 'react'; -// Unlisted: not in the catalogue, nav, or sitemap, and asked not to be indexed. export const metadata: Metadata = { - title: 'Block Runner · Vibenet', - description: 'A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', - robots: { index: false, follow: false }, + title: '200ms Blocks · Vibenet', + description: 'Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade.', }; -export default function BlockRunnerLayout({ children }: { children: ReactNode }) { +export default function Blocks200Layout({ children }: { children: ReactNode }) { return <>{children}; } diff --git a/app/vibenet/demos/200/page.tsx b/app/vibenet/demos/200/page.tsx index a1e4a404..730bb233 100644 --- a/app/vibenet/demos/200/page.tsx +++ b/app/vibenet/demos/200/page.tsx @@ -1,23 +1,69 @@ -import { BlockRunner } from './BlockRunner'; +import { Button } from '../../../components/ui/Button'; import { Text } from '../../../components/ui/Text'; +import { FeatureCard } from '../../components/FeatureCard'; +import type { VibenetFeature } from '../../library/types'; +import { FeatureGridCard } from '../_shared/FeatureGridCard'; +import { demoForPath } from '../catalogue'; + +const BLOCKS_PATH = '/vibenet/demos/200'; + +const BLOCKS_FEATURE: VibenetFeature = { + id: '200ms-blocks', + title: '200ms Native Blocks', + summary: + 'Denim replaces two-second blocks and incremental Flashblocks with five complete canonical blocks every second. Each 200 ms block has its own number, hash, state root, receipts, and lifecycle.', + status: 'live', + availability: 'Coming soon in ', + availabilityLabel: 'Base Denim', + availabilityHref: { + label: 'Denim 200ms blocks', + href: 'https://docs.base.org/upgrades/denim/200ms-blocks', + external: true, + }, + highlights: [ + { title: 'Five Blocks Per Second', detail: 'A complete canonical block is produced every 200 milliseconds.' }, + { title: 'Not A Preconfirmation', detail: 'Every update is a block with its own hash, state root, and receipts.' }, + { title: 'Standard RPC', detail: 'Use canonical block responses and newHeads subscriptions instead of Flashblocks streams.' }, + { title: 'Live On Vibenet', detail: 'Build against the native cadence today before Denim reaches production networks.' }, + ], +}; + +function DemoIcon({ reflex }: { reflex: boolean }) { + return reflex ? ( + + ) : ( + + ); +} + +export default function Blocks200Page() { + const group = demoForPath(BLOCKS_PATH); + if (!group) return null; -export default function BlockRunnerPage() { return ( -
-
- - Vibenet · 200 ms blocks - - - Block Runner - - - Base's Cobalt upgrade mints a block every 200 ms. Block Runner is a one-button game built on that - cadence: every obstacle is a real vibenet block, pushed to the screen the moment the chain seals it. Tap to - bite a block, read its number, and see how long you can keep up. - -
- +
+ + + Demos +
+ {group.children?.filter((demo) => demo.listed !== false).map((demo) => ( + } + title={demo.title} + description={demo.summary} + > + + + ))} +
); } diff --git a/app/vibenet/demos/200/reflex/ReflexGame.tsx b/app/vibenet/demos/200/reflex/ReflexGame.tsx new file mode 100644 index 00000000..90bc6525 --- /dev/null +++ b/app/vibenet/demos/200/reflex/ReflexGame.tsx @@ -0,0 +1,312 @@ +'use client'; + +import { useEffect, useRef, useState, type MouseEvent } from 'react'; +import type { Hex } from 'viem'; + +import { trackReflexRun } from '../../../../analytics/events'; +import { Button } from '../../../../components/ui/Button'; +import { Card } from '../../../../components/ui/Card'; +import { cn } from '../../../../components/ui/cn'; +import { Text } from '../../../../components/ui/Text'; +import { VIBENET_EXPLORER_PATH } from '../../../library/config'; +import { walletErrorMessage } from '../../../library/wallet'; +import { AccountDemoShell } from '../../_components/AccountDemoShell'; +import { ViewTransactionButton } from '../../_shared/ViewTransactionButton'; +import type { Inclusion } from '../../_shared/inclusion'; +import { AccountEngineProvider, useAccountEngine } from '../../account/useAccountEngine'; +import { outcomeCopy, reflexOutcome, type ReflexOutcome } from './lib/game'; + +const TARGETS = Array.from({ length: 9 }, (_, index) => index); + +type Phase = 'idle' | 'preparing' | 'countdown' | 'racing' | 'result' | 'error'; + +type Result = { + outcome: ReflexOutcome; + reactionMs: number | null; + inclusionMs: number; + hash: Hex; + inclusion?: Inclusion; +}; + +const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms)); + +function BaseTarget({ active }: { active: boolean }) { + return ( + + ); +} + +function ReflexGameInner() { + const engine = useAccountEngine(); + const [phase, setPhase] = useState('idle'); + const [countdown, setCountdown] = useState<3 | 2 | 1 | 'GO' | null>(null); + const [target, setTarget] = useState(null); + const [elapsed, setElapsed] = useState(0); + const [hasReacted, setHasReacted] = useState(false); + const [result, setResult] = useState(null); + const [error, setError] = useState(''); + const startedAt = useRef(0); + const reactionMs = useRef(null); + const transactionHash = useRef(null); + + useEffect(() => { + if (phase !== 'racing') return; + let frame = 0; + const update = () => { + setElapsed(Math.round(performance.now() - startedAt.current)); + frame = window.requestAnimationFrame(update); + }; + frame = window.requestAnimationFrame(update); + return () => window.cancelAnimationFrame(frame); + }, [phase]); + + const reset = () => { + setPhase('idle'); + setCountdown(null); + setTarget(null); + setElapsed(0); + setHasReacted(false); + setResult(null); + setError(''); + startedAt.current = 0; + reactionMs.current = null; + transactionHash.current = null; + }; + + const finish = () => { + const inclusionMs = Math.max(0, Math.round(performance.now() - startedAt.current)); + const reaction = reactionMs.current; + const outcome = reaction === null ? 'block' : reflexOutcome(reaction, inclusionMs); + const hash = transactionHash.current; + if (!hash) return; + setElapsed(inclusionMs); + setTarget(null); + setResult({ outcome, reactionMs: reaction, inclusionMs, hash }); + setPhase('result'); + trackReflexRun(outcome); + }; + + const startRun = async () => { + if (!engine.acct || phase !== 'idle' && phase !== 'result' && phase !== 'error') return; + setPhase('preparing'); + setError(''); + setResult(null); + setElapsed(0); + setHasReacted(false); + reactionMs.current = null; + transactionHash.current = null; + trackReflexRun('started'); + + try { + const account = engine.acct; + const response = await engine.sendActiveCalls({ + calls: [{ to: account.address, data: '0x', value: '0' }], + metadata: '200ms Reflex', + beforeBroadcast: async () => { + setPhase('countdown'); + for (const value of [3, 2, 1] as const) { + setCountdown(value); + await sleep(1_000); + } + setCountdown('GO'); + }, + onBroadcast: (hash) => { + transactionHash.current = hash; + startedAt.current = performance.now(); + setTarget(Math.floor(Math.random() * TARGETS.length)); + setCountdown(null); + setPhase('racing'); + }, + onReceipt: finish, + }); + + setResult((current) => current ? { ...current, inclusion: response.inclusion } : current); + engine.pushActivity({ + kind: 'transact', + title: '200ms Reflex run', + detail: reactionMs.current === null ? 'The block won' : `Reaction: ${reactionMs.current} ms`, + txHash: response.hash, + serialized: response.serialized, + network: engine.chain.name, + mode: engine.chain.mode, + account: account.address, + }); + } catch (cause) { + setTarget(null); + setCountdown(null); + setError(walletErrorMessage(cause)); + setPhase('error'); + trackReflexRun('error'); + } + }; + + const hitTarget = (index: number, event: MouseEvent) => { + if (phase !== 'racing' || index !== target || reactionMs.current !== null) return; + const reaction = Math.max(0, Math.round(event.timeStamp - startedAt.current)); + reactionMs.current = reaction; + setElapsed(reaction); + setHasReacted(true); + setTarget(null); + }; + + const outcome = result ? outcomeCopy(result.outcome) : null; + const busy = phase === 'preparing' || phase === 'countdown' || phase === 'racing'; + const arenaStatus = + phase === 'racing' + ? hasReacted + ? 'Waiting for the block…' + : 'Hit the lit target' + : phase === 'result' + ? 'Race complete' + : phase === 'error' + ? 'Ready to try again' + : 'Move your cursor into the arena'; + + return ( + +
+ Vibenet · 200ms Blocks + Can you beat a block? + + After a 3–2–1 countdown, a real transaction is broadcast and one target lights up. Hit it before Vibenet + seals the transaction in a complete native block. + +
+ +
+ +
+
+ Reaction arena + {arenaStatus} +
+
+ {phase === 'racing' ? `${elapsed} ms` : phase === 'result' ? 'Done' : '— ms'} +
+
+ +
+ {TARGETS.map((index) => { + const active = phase === 'racing' && target === index; + return ( + + ); + })} + + {phase === 'preparing' || phase === 'countdown' ? ( +
+ {phase === 'preparing' ? 'Preparing transaction' : 'Get ready'} +
+ {phase === 'preparing' ? '…' : countdown} +
+
+ ) : null} +
+
+ +
+ + The race + {result && outcome ? ( + <> + {outcome.title} + {outcome.detail} +
+
+ You + + {result.reactionMs === null ? 'Missed' : `${result.reactionMs} ms`} + +
+
+ Txn inclusion + {result.inclusionMs} ms +
+
+ + Measured from RPC acceptance until this page observed the receipt. + +
+ + +
+ + ) : phase === 'error' ? ( + <> + Run interrupted + {error} + + + ) : ( + <> +
+ 200 + ms +
+ + One full block. Signing happens first, then the countdown starts immediately before broadcast. + + + + )} +
+ + + How it works +
    +
  1. 01 Sign and prepare the transaction.
  2. +
  3. 02 Position your cursor during 3–2–1.
  4. +
  5. 03 Hit the target before the receipt arrives.
  6. +
+
+
+
+
+ ); +} + +export function ReflexGame() { + return ( + + + + ); +} diff --git a/app/vibenet/demos/200/reflex/layout.tsx b/app/vibenet/demos/200/reflex/layout.tsx new file mode 100644 index 00000000..3d8239d0 --- /dev/null +++ b/app/vibenet/demos/200/reflex/layout.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from 'next'; +import type { ReactNode } from 'react'; + +export const metadata: Metadata = { + title: 'Reflex · 200ms Blocks', + description: 'Race your reaction time against a real transaction landing in a native 200 ms Vibenet block.', +}; + +export default function ReflexLayout({ children }: { children: ReactNode }) { + return <>{children}; +} diff --git a/app/vibenet/demos/200/reflex/lib/game.test.ts b/app/vibenet/demos/200/reflex/lib/game.test.ts new file mode 100644 index 00000000..a756618c --- /dev/null +++ b/app/vibenet/demos/200/reflex/lib/game.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; + +import { outcomeCopy, reflexOutcome } from './game'; + +describe('reflexOutcome', () => { + it('awards the run to the human only when the click is earlier', () => { + expect(reflexOutcome(199, 200)).toBe('human'); + expect(reflexOutcome(201, 200)).toBe('block'); + }); + + it('calls an exact same-millisecond finish a tie', () => { + expect(reflexOutcome(200, 200)).toBe('tie'); + expect(outcomeCopy('tie').title).toBe('Dead heat.'); + }); +}); diff --git a/app/vibenet/demos/200/reflex/lib/game.ts b/app/vibenet/demos/200/reflex/lib/game.ts new file mode 100644 index 00000000..4c5b787f --- /dev/null +++ b/app/vibenet/demos/200/reflex/lib/game.ts @@ -0,0 +1,27 @@ +export type ReflexOutcome = 'human' | 'block' | 'tie'; + +/** Compare two durations measured from the same broadcast callback. */ +export function reflexOutcome(reactionMs: number, inclusionMs: number): ReflexOutcome { + if (reactionMs < inclusionMs) return 'human'; + if (reactionMs > inclusionMs) return 'block'; + return 'tie'; +} + +export function outcomeCopy(outcome: ReflexOutcome): { title: string; detail: string } { + if (outcome === 'human') { + return { + title: 'You beat the block.', + detail: 'You hit the target before this page observed the transaction receipt.', + }; + } + if (outcome === 'tie') { + return { + title: 'Dead heat.', + detail: 'Your click and the transaction receipt arrived in the same millisecond.', + }; + } + return { + title: 'The block beat you.', + detail: 'Vibenet included the transaction before you hit the target.', + }; +} diff --git a/app/vibenet/demos/200/reflex/page.tsx b/app/vibenet/demos/200/reflex/page.tsx new file mode 100644 index 00000000..86e7902d --- /dev/null +++ b/app/vibenet/demos/200/reflex/page.tsx @@ -0,0 +1,5 @@ +import { ReflexGame } from './ReflexGame'; + +export default function ReflexPage() { + return ; +} diff --git a/app/vibenet/demos/account/useAccountEngine.tsx b/app/vibenet/demos/account/useAccountEngine.tsx index f637a127..7db42520 100644 --- a/app/vibenet/demos/account/useAccountEngine.tsx +++ b/app/vibenet/demos/account/useAccountEngine.tsx @@ -853,7 +853,7 @@ function useAccountEngineCore() { // Wait for a broadcast tx to be included and check that it — and every 8130 // phase in it — succeeded. Throws TxPendingError if it is still not included // when the timeout runs out, a plain Error if anything reverted. - const awaitInclusion = async (txHash: Hex, timeout = 30_000): Promise => { + const awaitInclusion = async (txHash: Hex, timeout = 30_000, onReceipt?: () => void): Promise => { const client = makeRpcClient(); const w = ensureWatcher(); // The receipt arrives on the socket the moment its block is sealed; over @@ -865,6 +865,7 @@ function useAccountEngineCore() { if (err instanceof ReceiptTimeoutError) throw new TxPendingError(txHash); throw err; } + onReceipt?.(); // Status and 8130 phase results come from the account RPC, whose replica // can trail the socket's node by a block: a few short retries, then the // pushed receipt itself. @@ -896,9 +897,16 @@ function useAccountEngineCore() { // Broadcast a signed 8130 tx and wait for inclusion. Throws TxPendingError on // timeout (submitted but unconfirmed), a plain Error if any phase reverts. - const broadcast8130 = async (signedTx: Hex, onStatus?: (s: 'submitting' | 'confirming') => void): Promise => { + const broadcast8130 = async ( + signedTx: Hex, + onStatus?: (s: 'submitting' | 'confirming') => void, + onBroadcast?: (hash: Hex) => void, + beforeBroadcast?: () => void | Promise, + onReceipt?: () => void, + ): Promise => { const client = makeRpcClient(); const w = ensureWatcher(); + await beforeBroadcast?.(); onStatus?.('submitting'); // The newest block this page has seen is the anchor the inclusion is // measured from, on the chain's clock: read it before the send leaves. @@ -913,8 +921,9 @@ function useAccountEngineCore() { const now = Date.now(); for (const [hash, mark] of sendAnchors.current) if (now - mark.at > 300_000) sendAnchors.current.delete(hash); sendAnchors.current.set(txHash, { anchor, at: now }); + onBroadcast?.(txHash); onStatus?.('confirming'); - return awaitInclusion(txHash); + return awaitInclusion(txHash, 30_000, onReceipt); }; // Live EIP-8130 state used while preparing a transaction. This is the only @@ -1201,11 +1210,20 @@ function useAccountEngineCore() { calls, tokenGas, metadata, + onBroadcast, + beforeBroadcast, + onReceipt, }: { calls: { to: Address; data: Hex; value?: string }[]; tokenGas?: { token: Address; decimals: number; payer: Signer; fee: bigint }; /** Optional top-level signed app data attached to the transaction. */ metadata?: string; + /** Called as soon as the RPC accepts the signed transaction, before inclusion. */ + onBroadcast?: (hash: Hex) => void; + /** Runs after signing and immediately before the transaction is sent to the RPC. */ + beforeBroadcast?: () => void | Promise; + /** Called when the receipt first arrives, before follow-up receipt parsing. */ + onReceipt?: () => void; }): Promise<{ hash: Hex; serialized: Hex; mode: 'self' | 'token'; inclusion?: Inclusion }> => { if (!acct) throw new Error('Select an account before you continue.'); if (!calls.length) throw new Error('No calls to send.'); @@ -1239,7 +1257,7 @@ function useAccountEngineCore() { undefined, payerOpt, ); - const hash = await broadcast8130(serialized); + const hash = await broadcast8130(serialized, undefined, onBroadcast, beforeBroadcast, onReceipt); applyLandedBundle(acct, nextSeq, bundle); return { hash, serialized, mode: tokenGas ? 'token' : 'self', inclusion: inclusionFor(hash) }; }; diff --git a/app/vibenet/demos/catalogue.test.ts b/app/vibenet/demos/catalogue.test.ts index 6c434352..7a521162 100644 --- a/app/vibenet/demos/catalogue.test.ts +++ b/app/vibenet/demos/catalogue.test.ts @@ -51,6 +51,14 @@ describe('demoForPath', () => { expect(demoForPath('/vibenet/demos/validity/race-the-agent')?.title).toBe('Race the Agent'); expect(listedDemos().some((demo) => demo.title === 'Conditional Swaps')).toBe(false); }); + + it('groups both 200ms games under the 200ms blocks catalogue entry', () => { + const group = demoForPath('/vibenet/demos/200'); + expect(group?.children?.map((demo) => demo.title)).toEqual(['Reflex', 'Block Runner']); + expect(group?.children?.filter((demo) => demo.listed !== false).map((demo) => demo.title)).toEqual(['Reflex']); + expect(demoForPath('/vibenet/demos/200/reflex')?.title).toBe('Reflex'); + expect(demoForPath('/vibenet/demos/200/block-runner')).toMatchObject({ title: 'Block Runner', listed: false }); + }); }); describe('demoBreadcrumb', () => { @@ -80,6 +88,16 @@ describe('demoBreadcrumb', () => { }); }); + it('resolves a 200ms game through the 200ms Blocks group', () => { + expect(demoBreadcrumb('/vibenet/demos/200/reflex')).toEqual({ + middle: { + label: '200ms Blocks', + href: '/vibenet/demos/200', + }, + childLabel: 'Reflex', + }); + }); + it('falls back to readable labels for unregistered nested routes', () => { expect(demoBreadcrumb('/vibenet/demos/trading/stop-loss')).toEqual({ middle: { label: 'Trading', href: '/vibenet/demos/trading' }, diff --git a/app/vibenet/demos/catalogue.ts b/app/vibenet/demos/catalogue.ts index 7559df68..0303ebd4 100644 --- a/app/vibenet/demos/catalogue.ts +++ b/app/vibenet/demos/catalogue.ts @@ -101,14 +101,40 @@ export const DEMOS: DemoEntry[] = [ ], }, { - // Secret route: /demos/200 (redirects here). Off the grid on purpose. href: '/vibenet/demos/200', - title: 'Block Runner', - shortTitle: 'Block Runner', - summary: 'A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', - points: ['One block every 200 ms', 'Block height follows gas used', 'One button: tap to bite'], + title: '200ms Blocks', + shortTitle: '200ms Blocks', + icon: '/denim-illo.svg', + summary: + 'Experience complete native blocks every 200 milliseconds on Vibenet before the Denim upgrade reaches Base Sepolia and Mainnet.', + points: [ + 'Five canonical blocks every second', + 'Every block has its own hash, state root, and receipts', + 'Race a live transaction against chain inclusion', + ], available: true, - listed: false, + children: [ + { + href: '/vibenet/demos/200/reflex', + title: 'Reflex', + summary: + 'Send a real transaction from your browser account, then race to hit the target before Vibenet seals it in a 200 ms block.', + points: [ + 'Starts on transaction broadcast', + 'Compares your reaction with real inclusion', + 'Uses your existing Vibenet account', + ], + available: true, + }, + { + href: '/vibenet/demos/200/block-runner', + title: 'Block Runner', + summary: 'A pixel runner paced by Vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', + points: ['One block every 200 ms', 'Block height follows gas used', 'One button: tap to bite'], + available: true, + listed: false, + }, + ], }, ]; diff --git a/llms.config.mjs b/llms.config.mjs index c7e6cc5f..05385ed6 100644 --- a/llms.config.mjs +++ b/llms.config.mjs @@ -8,5 +8,5 @@ import { disabledRouteGlobs } from './deploy.config.mjs'; export default { - exclude: disabledRouteGlobs(), + exclude: [...disabledRouteGlobs(), '/vibenet/demos/200/block-runner'], }; From 2b2ba246651f0aea385cad552176ebe7c6b411b9 Mon Sep 17 00:00:00 2001 From: Haardik Date: Wed, 30 Sep 2026 11:16:47 -0400 Subject: [PATCH 2/5] docs: refresh generated agent indexes Amp-Thread-ID: https://ampcode.com/threads/T-01a0f2c2-6770-7506-9d76-d1fb352c1208 Co-authored-by: Amp --- public/AGENTS.md | 5 +++-- public/llms-full.txt | 3 ++- public/llms.txt | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/public/AGENTS.md b/public/AGENTS.md index 334201ed..824bbc03 100644 --- a/public/AGENTS.md +++ b/public/AGENTS.md @@ -25,7 +25,7 @@ Machine-readable entry point for agents working with Base Chain network state. | /vibenet/explorer | daily | re-fetch every session; never cache across sessions | | /vibenet/faucet | monthly | stable within a session | | /api/snapshots | daily | re-fetch every session; never cache across sessions | -| /, /vibenet, /vibenet/demos/200, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity, /vibenet/demos/validity/conditional-swaps, /vibenet/demos/validity/race-the-agent | infrequent | stable within a session | +| /, /vibenet, /vibenet/demos/200, /vibenet/demos/200/reflex, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity, /vibenet/demos/validity/conditional-swaps, /vibenet/demos/validity/race-the-agent | infrequent | stable within a session | ## Machine-readable endpoints @@ -82,7 +82,8 @@ Discovered from the Next.js app directory. **Vibenet developer network** - [/vibenet](https://chain.base.org/vibenet) — Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [/vibenet/demos/200](https://chain.base.org/vibenet/demos/200) — A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. +- [/vibenet/demos/200](https://chain.base.org/vibenet/demos/200) — Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. +- [/vibenet/demos/200/reflex](https://chain.base.org/vibenet/demos/200/reflex) — Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. - [/vibenet/demos/account](https://chain.base.org/vibenet/demos/account) — Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [/vibenet/demos/b20](https://chain.base.org/vibenet/demos/b20) — Explore, configure, and issue Base-native B20 tokens on Vibenet. - [/vibenet/demos/validity](https://chain.base.org/vibenet/demos/validity) — Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. diff --git a/public/llms-full.txt b/public/llms-full.txt index f4011209..f8d11f95 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -17,7 +17,8 @@ ## Vibenet developer network - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [Block Runner · Vibenet](https://chain.base.org/vibenet/demos/200): A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. +- [200ms Blocks · Vibenet](https://chain.base.org/vibenet/demos/200): Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. +- [Reflex · 200ms Blocks](https://chain.base.org/vibenet/demos/200/reflex): Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. - [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. diff --git a/public/llms.txt b/public/llms.txt index 79737649..8d083813 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -14,7 +14,8 @@ Freshness: /snapshots and /vibenet/explorer change daily. /vibenet/faucet change ## Vibenet developer network - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [Block Runner · Vibenet](https://chain.base.org/vibenet/demos/200): A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. +- [200ms Blocks · Vibenet](https://chain.base.org/vibenet/demos/200): Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. +- [Reflex · 200ms Blocks](https://chain.base.org/vibenet/demos/200/reflex): Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. - [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. From d875dc9d5cf3358acadfab66de27e416dfc7dbf7 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Fri, 2 Oct 2026 16:37:31 -0400 Subject: [PATCH 3/5] refactor(vibenet): make Reflex an easter egg linked from tx popups Drop the 200ms Blocks category and restore Block Runner's unlisted /vibenet/demos/200 route as on main. Reflex moves to an unlisted, noindex /vibenet/demos/reflex, reached from a footnote under the inclusion line in transaction popups. --- AGENTS.md | 2 +- app/sitemap.ts | 1 - app/vibenet/demos/200/block-runner/layout.tsx | 12 --- app/vibenet/demos/200/block-runner/page.tsx | 20 ----- app/vibenet/demos/200/layout.tsx | 8 +- app/vibenet/demos/200/page.tsx | 80 ++++--------------- app/vibenet/demos/_shared/InclusionBadge.tsx | 25 ++++-- app/vibenet/demos/catalogue.test.ts | 18 ----- app/vibenet/demos/catalogue.ts | 46 ++++------- .../demos/{200 => }/reflex/ReflexGame.tsx | 22 ++--- app/vibenet/demos/{200 => }/reflex/layout.tsx | 5 +- .../demos/{200 => }/reflex/lib/game.test.ts | 0 .../demos/{200 => }/reflex/lib/game.ts | 0 app/vibenet/demos/{200 => }/reflex/page.tsx | 0 llms.config.mjs | 3 +- 15 files changed, 76 insertions(+), 166 deletions(-) delete mode 100644 app/vibenet/demos/200/block-runner/layout.tsx delete mode 100644 app/vibenet/demos/200/block-runner/page.tsx rename app/vibenet/demos/{200 => }/reflex/ReflexGame.tsx (94%) rename app/vibenet/demos/{200 => }/reflex/layout.tsx (64%) rename app/vibenet/demos/{200 => }/reflex/lib/game.test.ts (100%) rename app/vibenet/demos/{200 => }/reflex/lib/game.ts (100%) rename app/vibenet/demos/{200 => }/reflex/page.tsx (100%) diff --git a/AGENTS.md b/AGENTS.md index 70d48e89..d76aca7b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,7 +126,7 @@ This app uses Vercel Web Analytics. Two things must stay in place: | `trackExplorerActiveBlockJump(chain, jump)` | `app/internal-explorer/components/ActiveBlockButton.tsx` — zeronet latest/previous active block | | `trackValidityOrder(side, status)` | `app/vibenet/demos/validity/ValidityDemo.tsx` — conditional swap submit / include / expiry / replace | | `trackValidityRace(attempt, status)` | `app/vibenet/demos/validity/race-the-agent/RaceTheAgentDemo.tsx` — validity/manual comparison attempts | - | `trackReflexRun(status)` | `app/vibenet/demos/200/reflex/ReflexGame.tsx` — reaction-game starts and outcomes | + | `trackReflexRun(status)` | `app/vibenet/demos/reflex/ReflexGame.tsx` — reaction-game starts and outcomes | Add a helper (and a row here) for a new key journey; remove the helper if you remove its surface. Confirm the wiring with `grep -rn "analytics/events" app`. diff --git a/app/sitemap.ts b/app/sitemap.ts index 6069e809..090ae7db 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -21,7 +21,6 @@ export default function sitemap(): MetadataRoute.Sitemap { { path: '/vibenet/explorer', priority: 0.6, changeFrequency: 'daily' }, { path: '/vibenet/faucet', priority: 0.5, changeFrequency: 'monthly' }, { path: '/vibenet/demos/200', priority: 0.5, changeFrequency: 'weekly' }, - { path: '/vibenet/demos/200/reflex', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/account', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/b20', priority: 0.5, changeFrequency: 'weekly' }, { path: '/vibenet/demos/validity', priority: 0.5, changeFrequency: 'weekly' }, diff --git a/app/vibenet/demos/200/block-runner/layout.tsx b/app/vibenet/demos/200/block-runner/layout.tsx deleted file mode 100644 index e03122c9..00000000 --- a/app/vibenet/demos/200/block-runner/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import type { Metadata } from 'next'; -import type { ReactNode } from 'react'; - -export const metadata: Metadata = { - title: 'Block Runner · 200ms Blocks', - description: 'A pixel runner paced by Vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', - robots: { index: false, follow: false }, -}; - -export default function BlockRunnerLayout({ children }: { children: ReactNode }) { - return <>{children}; -} diff --git a/app/vibenet/demos/200/block-runner/page.tsx b/app/vibenet/demos/200/block-runner/page.tsx deleted file mode 100644 index 6bdac241..00000000 --- a/app/vibenet/demos/200/block-runner/page.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Text } from '../../../../components/ui/Text'; -import { BlockRunner } from '../BlockRunner'; - -export default function BlockRunnerPage() { - return ( -
-
- - Vibenet · 200 ms blocks - - Block Runner - - Every obstacle is a real Vibenet block, pushed to the screen the moment the chain seals it. Tap to bite a - block, read its number and slot, and see how long you can keep up. - -
- -
- ); -} diff --git a/app/vibenet/demos/200/layout.tsx b/app/vibenet/demos/200/layout.tsx index 093afbd0..182f20a5 100644 --- a/app/vibenet/demos/200/layout.tsx +++ b/app/vibenet/demos/200/layout.tsx @@ -1,11 +1,13 @@ import type { Metadata } from 'next'; import type { ReactNode } from 'react'; +// Unlisted: not in the catalogue, nav, or sitemap, and asked not to be indexed. export const metadata: Metadata = { - title: '200ms Blocks · Vibenet', - description: 'Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade.', + title: 'Block Runner · Vibenet', + description: 'A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', + robots: { index: false, follow: false }, }; -export default function Blocks200Layout({ children }: { children: ReactNode }) { +export default function BlockRunnerLayout({ children }: { children: ReactNode }) { return <>{children}; } diff --git a/app/vibenet/demos/200/page.tsx b/app/vibenet/demos/200/page.tsx index 730bb233..a1e4a404 100644 --- a/app/vibenet/demos/200/page.tsx +++ b/app/vibenet/demos/200/page.tsx @@ -1,69 +1,23 @@ -import { Button } from '../../../components/ui/Button'; +import { BlockRunner } from './BlockRunner'; import { Text } from '../../../components/ui/Text'; -import { FeatureCard } from '../../components/FeatureCard'; -import type { VibenetFeature } from '../../library/types'; -import { FeatureGridCard } from '../_shared/FeatureGridCard'; -import { demoForPath } from '../catalogue'; - -const BLOCKS_PATH = '/vibenet/demos/200'; - -const BLOCKS_FEATURE: VibenetFeature = { - id: '200ms-blocks', - title: '200ms Native Blocks', - summary: - 'Denim replaces two-second blocks and incremental Flashblocks with five complete canonical blocks every second. Each 200 ms block has its own number, hash, state root, receipts, and lifecycle.', - status: 'live', - availability: 'Coming soon in ', - availabilityLabel: 'Base Denim', - availabilityHref: { - label: 'Denim 200ms blocks', - href: 'https://docs.base.org/upgrades/denim/200ms-blocks', - external: true, - }, - highlights: [ - { title: 'Five Blocks Per Second', detail: 'A complete canonical block is produced every 200 milliseconds.' }, - { title: 'Not A Preconfirmation', detail: 'Every update is a block with its own hash, state root, and receipts.' }, - { title: 'Standard RPC', detail: 'Use canonical block responses and newHeads subscriptions instead of Flashblocks streams.' }, - { title: 'Live On Vibenet', detail: 'Build against the native cadence today before Denim reaches production networks.' }, - ], -}; - -function DemoIcon({ reflex }: { reflex: boolean }) { - return reflex ? ( - - ) : ( - - ); -} - -export default function Blocks200Page() { - const group = demoForPath(BLOCKS_PATH); - if (!group) return null; +export default function BlockRunnerPage() { return ( -
- - - Demos -
- {group.children?.filter((demo) => demo.listed !== false).map((demo) => ( - } - title={demo.title} - description={demo.summary} - > - - - ))} -
+
+
+ + Vibenet · 200 ms blocks + + + Block Runner + + + Base's Cobalt upgrade mints a block every 200 ms. Block Runner is a one-button game built on that + cadence: every obstacle is a real vibenet block, pushed to the screen the moment the chain seals it. Tap to + bite a block, read its number, and see how long you can keep up. + +
+
); } diff --git a/app/vibenet/demos/_shared/InclusionBadge.tsx b/app/vibenet/demos/_shared/InclusionBadge.tsx index d9bf28e3..f937cb8e 100644 --- a/app/vibenet/demos/_shared/InclusionBadge.tsx +++ b/app/vibenet/demos/_shared/InclusionBadge.tsx @@ -5,22 +5,35 @@ // timestamp minus that of the newest block the page had seen at broadcast. // Vibenet runs Cobalt today; on chains without it the slot is omitted. +import Link from 'next/link'; + import { cn } from '../../../components/ui/cn'; import { Text } from '../../../components/ui/Text'; import { blocksAfterSendLabel, type Inclusion, latencyLabel, slotLabel } from './inclusion'; +/** Unlisted reaction game; this footnote is the only way in. */ +const REFLEX_PATH = '/vibenet/demos/reflex'; + /** - * The line under the badge in a transaction result: the claim, in words. Only - * for blocks that carry Cobalt's millisecond timestamp, so it never shows on a - * chain without 200 ms blocks. + * The line under the badge in a transaction result: the claim, in words, plus + * a footnote linking the Reflex easter egg. Only for blocks that carry Cobalt's + * millisecond timestamp, so it never shows on a chain without 200 ms blocks. */ export function InclusionTagline({ inclusion }: { inclusion: Inclusion }) { if (inclusion.blockTimestampMs === null) return null; const blocks = blocksAfterSendLabel(inclusion); return ( - - {blocks ? `Sealed ${blocks} after broadcast. ` : ''}Brought to you by 200 ms blocks on Base. - + <> + + {blocks ? `Sealed ${blocks} after broadcast. ` : ''}Brought to you by 200 ms blocks on Base. + + + Think you're faster?{' '} + + Race a block + + + ); } diff --git a/app/vibenet/demos/catalogue.test.ts b/app/vibenet/demos/catalogue.test.ts index 7a521162..6c434352 100644 --- a/app/vibenet/demos/catalogue.test.ts +++ b/app/vibenet/demos/catalogue.test.ts @@ -51,14 +51,6 @@ describe('demoForPath', () => { expect(demoForPath('/vibenet/demos/validity/race-the-agent')?.title).toBe('Race the Agent'); expect(listedDemos().some((demo) => demo.title === 'Conditional Swaps')).toBe(false); }); - - it('groups both 200ms games under the 200ms blocks catalogue entry', () => { - const group = demoForPath('/vibenet/demos/200'); - expect(group?.children?.map((demo) => demo.title)).toEqual(['Reflex', 'Block Runner']); - expect(group?.children?.filter((demo) => demo.listed !== false).map((demo) => demo.title)).toEqual(['Reflex']); - expect(demoForPath('/vibenet/demos/200/reflex')?.title).toBe('Reflex'); - expect(demoForPath('/vibenet/demos/200/block-runner')).toMatchObject({ title: 'Block Runner', listed: false }); - }); }); describe('demoBreadcrumb', () => { @@ -88,16 +80,6 @@ describe('demoBreadcrumb', () => { }); }); - it('resolves a 200ms game through the 200ms Blocks group', () => { - expect(demoBreadcrumb('/vibenet/demos/200/reflex')).toEqual({ - middle: { - label: '200ms Blocks', - href: '/vibenet/demos/200', - }, - childLabel: 'Reflex', - }); - }); - it('falls back to readable labels for unregistered nested routes', () => { expect(demoBreadcrumb('/vibenet/demos/trading/stop-loss')).toEqual({ middle: { label: 'Trading', href: '/vibenet/demos/trading' }, diff --git a/app/vibenet/demos/catalogue.ts b/app/vibenet/demos/catalogue.ts index 7ba7c5f2..36523364 100644 --- a/app/vibenet/demos/catalogue.ts +++ b/app/vibenet/demos/catalogue.ts @@ -101,40 +101,28 @@ export const DEMOS: DemoEntry[] = [ ], }, { + // Secret route: /demos/200 (redirects here). Off the grid on purpose. href: '/vibenet/demos/200', - title: '200ms Blocks', - shortTitle: '200ms Blocks', - icon: '/denim-illo.svg', + title: 'Block Runner', + shortTitle: 'Block Runner', + summary: 'A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', + points: ['One block every 200 ms', 'Block height follows gas used', 'One button: tap to bite'], + available: true, + listed: false, + }, + { + // Easter egg: linked only from the inclusion line in transaction popups. + href: '/vibenet/demos/reflex', + title: 'Reflex', summary: - 'Experience complete native blocks every 200 milliseconds on Vibenet before the Denim upgrade reaches Base Sepolia and Mainnet.', + 'Send a real transaction from your browser account, then race to hit the target before Vibenet seals it in a 200 ms block.', points: [ - 'Five canonical blocks every second', - 'Every block has its own hash, state root, and receipts', - 'Race a live transaction against chain inclusion', + 'Starts on transaction broadcast', + 'Compares your reaction with real inclusion', + 'Uses your existing Vibenet account', ], available: true, - children: [ - { - href: '/vibenet/demos/200/reflex', - title: 'Reflex', - summary: - 'Send a real transaction from your browser account, then race to hit the target before Vibenet seals it in a 200 ms block.', - points: [ - 'Starts on transaction broadcast', - 'Compares your reaction with real inclusion', - 'Uses your existing Vibenet account', - ], - available: true, - }, - { - href: '/vibenet/demos/200/block-runner', - title: 'Block Runner', - summary: 'A pixel runner paced by Vibenet’s 200 ms blocks. Swallow a block to read its number and slot.', - points: ['One block every 200 ms', 'Block height follows gas used', 'One button: tap to bite'], - available: true, - listed: false, - }, - ], + listed: false, }, ]; diff --git a/app/vibenet/demos/200/reflex/ReflexGame.tsx b/app/vibenet/demos/reflex/ReflexGame.tsx similarity index 94% rename from app/vibenet/demos/200/reflex/ReflexGame.tsx rename to app/vibenet/demos/reflex/ReflexGame.tsx index 90bc6525..bac8a800 100644 --- a/app/vibenet/demos/200/reflex/ReflexGame.tsx +++ b/app/vibenet/demos/reflex/ReflexGame.tsx @@ -3,17 +3,17 @@ import { useEffect, useRef, useState, type MouseEvent } from 'react'; import type { Hex } from 'viem'; -import { trackReflexRun } from '../../../../analytics/events'; -import { Button } from '../../../../components/ui/Button'; -import { Card } from '../../../../components/ui/Card'; -import { cn } from '../../../../components/ui/cn'; -import { Text } from '../../../../components/ui/Text'; -import { VIBENET_EXPLORER_PATH } from '../../../library/config'; -import { walletErrorMessage } from '../../../library/wallet'; -import { AccountDemoShell } from '../../_components/AccountDemoShell'; -import { ViewTransactionButton } from '../../_shared/ViewTransactionButton'; -import type { Inclusion } from '../../_shared/inclusion'; -import { AccountEngineProvider, useAccountEngine } from '../../account/useAccountEngine'; +import { trackReflexRun } from '../../../analytics/events'; +import { Button } from '../../../components/ui/Button'; +import { Card } from '../../../components/ui/Card'; +import { cn } from '../../../components/ui/cn'; +import { Text } from '../../../components/ui/Text'; +import { VIBENET_EXPLORER_PATH } from '../../library/config'; +import { walletErrorMessage } from '../../library/wallet'; +import { AccountDemoShell } from '../_components/AccountDemoShell'; +import { ViewTransactionButton } from '../_shared/ViewTransactionButton'; +import type { Inclusion } from '../_shared/inclusion'; +import { AccountEngineProvider, useAccountEngine } from '../account/useAccountEngine'; import { outcomeCopy, reflexOutcome, type ReflexOutcome } from './lib/game'; const TARGETS = Array.from({ length: 9 }, (_, index) => index); diff --git a/app/vibenet/demos/200/reflex/layout.tsx b/app/vibenet/demos/reflex/layout.tsx similarity index 64% rename from app/vibenet/demos/200/reflex/layout.tsx rename to app/vibenet/demos/reflex/layout.tsx index 3d8239d0..f1c6a646 100644 --- a/app/vibenet/demos/200/reflex/layout.tsx +++ b/app/vibenet/demos/reflex/layout.tsx @@ -1,9 +1,12 @@ import type { Metadata } from 'next'; import type { ReactNode } from 'react'; +// Unlisted: reached from the inclusion line in transaction popups, not the +// catalogue grid, nav, or sitemap. export const metadata: Metadata = { - title: 'Reflex · 200ms Blocks', + title: 'Reflex · Vibenet', description: 'Race your reaction time against a real transaction landing in a native 200 ms Vibenet block.', + robots: { index: false, follow: false }, }; export default function ReflexLayout({ children }: { children: ReactNode }) { diff --git a/app/vibenet/demos/200/reflex/lib/game.test.ts b/app/vibenet/demos/reflex/lib/game.test.ts similarity index 100% rename from app/vibenet/demos/200/reflex/lib/game.test.ts rename to app/vibenet/demos/reflex/lib/game.test.ts diff --git a/app/vibenet/demos/200/reflex/lib/game.ts b/app/vibenet/demos/reflex/lib/game.ts similarity index 100% rename from app/vibenet/demos/200/reflex/lib/game.ts rename to app/vibenet/demos/reflex/lib/game.ts diff --git a/app/vibenet/demos/200/reflex/page.tsx b/app/vibenet/demos/reflex/page.tsx similarity index 100% rename from app/vibenet/demos/200/reflex/page.tsx rename to app/vibenet/demos/reflex/page.tsx diff --git a/llms.config.mjs b/llms.config.mjs index 05385ed6..268690fe 100644 --- a/llms.config.mjs +++ b/llms.config.mjs @@ -8,5 +8,6 @@ import { disabledRouteGlobs } from './deploy.config.mjs'; export default { - exclude: [...disabledRouteGlobs(), '/vibenet/demos/200/block-runner'], + // Reflex is an easter egg reached from transaction popups, so keep it out of the indexes. + exclude: [...disabledRouteGlobs(), '/vibenet/demos/reflex'], }; From 0134103989e284b34b0344cf84cade44bd350758 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Fri, 2 Oct 2026 16:37:31 -0400 Subject: [PATCH 4/5] docs: refresh generated agent indexes --- public/AGENTS.md | 5 ++--- public/llms-full.txt | 3 +-- public/llms.txt | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/public/AGENTS.md b/public/AGENTS.md index 824bbc03..334201ed 100644 --- a/public/AGENTS.md +++ b/public/AGENTS.md @@ -25,7 +25,7 @@ Machine-readable entry point for agents working with Base Chain network state. | /vibenet/explorer | daily | re-fetch every session; never cache across sessions | | /vibenet/faucet | monthly | stable within a session | | /api/snapshots | daily | re-fetch every session; never cache across sessions | -| /, /vibenet, /vibenet/demos/200, /vibenet/demos/200/reflex, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity, /vibenet/demos/validity/conditional-swaps, /vibenet/demos/validity/race-the-agent | infrequent | stable within a session | +| /, /vibenet, /vibenet/demos/200, /vibenet/demos/account, /vibenet/demos/b20, /vibenet/demos/validity, /vibenet/demos/validity/conditional-swaps, /vibenet/demos/validity/race-the-agent | infrequent | stable within a session | ## Machine-readable endpoints @@ -82,8 +82,7 @@ Discovered from the Next.js app directory. **Vibenet developer network** - [/vibenet](https://chain.base.org/vibenet) — Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [/vibenet/demos/200](https://chain.base.org/vibenet/demos/200) — Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. -- [/vibenet/demos/200/reflex](https://chain.base.org/vibenet/demos/200/reflex) — Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. +- [/vibenet/demos/200](https://chain.base.org/vibenet/demos/200) — A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. - [/vibenet/demos/account](https://chain.base.org/vibenet/demos/account) — Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [/vibenet/demos/b20](https://chain.base.org/vibenet/demos/b20) — Explore, configure, and issue Base-native B20 tokens on Vibenet. - [/vibenet/demos/validity](https://chain.base.org/vibenet/demos/validity) — Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. diff --git a/public/llms-full.txt b/public/llms-full.txt index f8d11f95..f4011209 100644 --- a/public/llms-full.txt +++ b/public/llms-full.txt @@ -17,8 +17,7 @@ ## Vibenet developer network - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [200ms Blocks · Vibenet](https://chain.base.org/vibenet/demos/200): Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. -- [Reflex · 200ms Blocks](https://chain.base.org/vibenet/demos/200/reflex): Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. +- [Block Runner · Vibenet](https://chain.base.org/vibenet/demos/200): A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. - [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. diff --git a/public/llms.txt b/public/llms.txt index 8d083813..79737649 100644 --- a/public/llms.txt +++ b/public/llms.txt @@ -14,8 +14,7 @@ Freshness: /snapshots and /vibenet/explorer change daily. /vibenet/faucet change ## Vibenet developer network - [Vibenet · Base Chain](https://chain.base.org/vibenet): Explore Vibenet, the Base devnet for testing in-flight protocol features. -- [200ms Blocks · Vibenet](https://chain.base.org/vibenet/demos/200): Experience Vibenet’s native 200 ms blocks through interactive demos built for the Base Denim upgrade. -- [Reflex · 200ms Blocks](https://chain.base.org/vibenet/demos/200/reflex): Race your reaction time against a real transaction landing in a native 200 ms Vibenet block. +- [Block Runner · Vibenet](https://chain.base.org/vibenet/demos/200): A pixel runner paced by vibenet’s 200 ms blocks. Swallow a block to read its number and slot. - [Accounts · Vibenet](https://chain.base.org/vibenet/demos/account): Create native account abstraction accounts from in-browser keys, fund them from the faucet, and inspect balances on Vibenet. - [Tokens · Vibenet](https://chain.base.org/vibenet/demos/b20): Explore, configure, and issue Base-native B20 tokens on Vibenet. - [Validity Transactions · Vibenet](https://chain.base.org/vibenet/demos/validity): Explore Vibenet demos built with transactions that execute only while their onchain validity conditions hold. From 12c1b866041baf1593cd07d795f84ba813c58778 Mon Sep 17 00:00:00 2001 From: Haardik H Date: Fri, 2 Oct 2026 16:51:59 -0400 Subject: [PATCH 5/5] fix(vibenet): top up unfunded Reflex accounts before racing Accounts persist in the browser across Vibenet resets but their ETH does not, and Reflex self-pays gas. The node rejected those sends with -32003 (insufficient funds), which viem shows as the generic "Transaction creation failed." after the full countdown. Check the balance before signing and drip from the faucet when it is low, and show the node's rejection reason instead of viem's short message. --- .../demos/account/useAccountEngine.tsx | 8 ++++++-- app/vibenet/demos/reflex/ReflexGame.tsx | 17 ++++++++++++---- app/vibenet/demos/reflex/lib/game.test.ts | 20 ++++++++++++++++++- app/vibenet/demos/reflex/lib/game.ts | 20 +++++++++++++++++++ 4 files changed, 58 insertions(+), 7 deletions(-) diff --git a/app/vibenet/demos/account/useAccountEngine.tsx b/app/vibenet/demos/account/useAccountEngine.tsx index 6365c943..b5af2506 100644 --- a/app/vibenet/demos/account/useAccountEngine.tsx +++ b/app/vibenet/demos/account/useAccountEngine.tsx @@ -287,8 +287,9 @@ function useAccountEngineCore() { return vibenetApi.account.balances(acct.address, 'vibenet').catch(() => null); }; - const requestFaucet = async () => { - if (!acct) return; + /** Drip ETH + USDV to the active account and wait for the ETH to land. Resolves true once credited. */ + const requestFaucet = async (): Promise => { + if (!acct) return false; setFaucetBusy('eth+usdv'); try { // Capture the pre-drip balance as the baseline for the "did it credit?" poll. @@ -310,8 +311,10 @@ function useAccountEngineCore() { } else { toast.error("Top up didn't go through — Vibenet may be down for maintenance. Please try again shortly."); } + return credited; } catch { toast.error('Top up failed'); + return false; } finally { setFaucetBusy(null); } @@ -661,6 +664,7 @@ function useAccountEngineCore() { // Faucet faucetBusy, requestFaucet, + refreshVibenetBalances, // Key + account-building primitives (used by CreateAccountModal) usedSignerIds, diff --git a/app/vibenet/demos/reflex/ReflexGame.tsx b/app/vibenet/demos/reflex/ReflexGame.tsx index bac8a800..cf185167 100644 --- a/app/vibenet/demos/reflex/ReflexGame.tsx +++ b/app/vibenet/demos/reflex/ReflexGame.tsx @@ -9,12 +9,11 @@ import { Card } from '../../../components/ui/Card'; import { cn } from '../../../components/ui/cn'; import { Text } from '../../../components/ui/Text'; import { VIBENET_EXPLORER_PATH } from '../../library/config'; -import { walletErrorMessage } from '../../library/wallet'; import { AccountDemoShell } from '../_components/AccountDemoShell'; import { ViewTransactionButton } from '../_shared/ViewTransactionButton'; import type { Inclusion } from '../_shared/inclusion'; import { AccountEngineProvider, useAccountEngine } from '../account/useAccountEngine'; -import { outcomeCopy, reflexOutcome, type ReflexOutcome } from './lib/game'; +import { MIN_RACE_BALANCE_WEI, outcomeCopy, raceErrorMessage, reflexOutcome, type ReflexOutcome } from './lib/game'; const TARGETS = Array.from({ length: 9 }, (_, index) => index); @@ -58,6 +57,7 @@ function ReflexGameInner() { const [hasReacted, setHasReacted] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(''); + const [toppingUp, setToppingUp] = useState(false); const startedAt = useRef(0); const reactionMs = useRef(null); const transactionHash = useRef(null); @@ -112,6 +112,15 @@ function ReflexGameInner() { try { const account = engine.acct; + // Accounts persist in the browser across Vibenet resets, but their ETH + // does not, and Reflex pays its own gas. Top up before signing instead of + // letting the node reject the send after the countdown. + const balances = await engine.refreshVibenetBalances(); + if (balances && BigInt(balances.eth_wei ?? '0') < MIN_RACE_BALANCE_WEI) { + setToppingUp(true); + const credited = await engine.requestFaucet().finally(() => setToppingUp(false)); + if (!credited) throw new Error('Could not top up this account from the faucet. Try again shortly.'); + } const response = await engine.sendActiveCalls({ calls: [{ to: account.address, data: '0x', value: '0' }], metadata: '200ms Reflex', @@ -147,7 +156,7 @@ function ReflexGameInner() { } catch (cause) { setTarget(null); setCountdown(null); - setError(walletErrorMessage(cause)); + setError(raceErrorMessage(cause)); setPhase('error'); trackReflexRun('error'); } @@ -226,7 +235,7 @@ function ReflexGameInner() { {phase === 'preparing' || phase === 'countdown' ? (
- {phase === 'preparing' ? 'Preparing transaction' : 'Get ready'} + {phase === 'countdown' ? 'Get ready' : toppingUp ? 'Topping up account from the faucet' : 'Preparing transaction'}
{phase === 'preparing' ? '…' : countdown}
diff --git a/app/vibenet/demos/reflex/lib/game.test.ts b/app/vibenet/demos/reflex/lib/game.test.ts index a756618c..24c257c3 100644 --- a/app/vibenet/demos/reflex/lib/game.test.ts +++ b/app/vibenet/demos/reflex/lib/game.test.ts @@ -1,6 +1,7 @@ +import { TransactionRejectedRpcError } from 'viem'; import { describe, expect, it } from 'vitest'; -import { outcomeCopy, reflexOutcome } from './game'; +import { outcomeCopy, raceErrorMessage, reflexOutcome } from './game'; describe('reflexOutcome', () => { it('awards the run to the human only when the click is earlier', () => { @@ -13,3 +14,20 @@ describe('reflexOutcome', () => { expect(outcomeCopy('tie').title).toBe('Dead heat.'); }); }); + +describe('raceErrorMessage', () => { + // What the node returns when a stored account has no ETH, e.g. after a reset. + const rejected = (reason: string) => new TransactionRejectedRpcError(new Error(reason)); + + it('explains an unfunded account instead of the generic -32003 text', () => { + const message = raceErrorMessage(rejected('insufficient funds for gas * price + value: have 0 want 67000000000000')); + expect(message).not.toBe('Transaction creation failed.'); + expect(message).toMatch(/no ETH/); + }); + + it("passes through the node's reason for other rejections", () => { + expect(raceErrorMessage(rejected('nonce too low: next nonce 4, tx nonce 3'))).toBe( + 'nonce too low: next nonce 4, tx nonce 3', + ); + }); +}); diff --git a/app/vibenet/demos/reflex/lib/game.ts b/app/vibenet/demos/reflex/lib/game.ts index 4c5b787f..21e9d424 100644 --- a/app/vibenet/demos/reflex/lib/game.ts +++ b/app/vibenet/demos/reflex/lib/game.ts @@ -25,3 +25,23 @@ export function outcomeCopy(outcome: ReflexOutcome): { title: string; detail: st detail: 'Vibenet included the transaction before you hit the target.', }; } + +/** + * Below this, top the account up before a race. A Reflex transaction costs + * about 67k gas at 1 gwei (~0.000067 ETH); the faucet drips 0.1 ETH. + */ +export const MIN_RACE_BALANCE_WEI = 1_000_000_000_000_000n; // 0.001 ETH + +/** + * The node reports every txpool rejection as JSON-RPC -32003, which viem + * renders as the generic "Transaction creation failed." The node's reason is + * in `details`; translate the one a player can act on and surface the rest. + */ +export function raceErrorMessage(cause: unknown): string { + const e = cause as { shortMessage?: string; details?: string; message?: string }; + const reason = e?.details || e?.shortMessage || e?.message || String(cause); + if (/insufficient funds/i.test(reason)) { + return 'This account has no ETH on Vibenet to pay for gas (the network may have been reset). Try again to top it up from the faucet.'; + } + return reason; +}