Compose a server
Every piece is a function of a Request, so composing them is routing.
apps/reference/server.ts
is the whole thing in 45 lines; this is the shape.
The pieces
Section titled “The pieces”import { createActionRoutes } from '@initlabs/vibekit/actions'import { createAgentHandler } from '@initlabs/vibekit/agent'import { createMcpHttpHandler } from '@initlabs/vibekit/mcp/http'import { createPaywall, memoryStore } from '@initlabs/vibekit/pay'import { createHost, defaultPlugins, defaultTools } from '@initlabs/vibekit/preset'import { createRestHandler } from '@initlabs/vibekit/rest'
const deployment = { network: 'testnet', mode: 'compose', tools: defaultTools, plugins: defaultPlugins() } as const
const agent = createAgentHandler({ ...deployment, model: { provider: 'zerosignal', model: 'qwen3:8b' } })const rest = createRestHandler(deployment)const mcp = createMcpHttpHandler({ name: 'mine', ...deployment })const actions = createActionRoutes({ hostFor: () => host })const host = createHost('testnet')const paywall = createPaywall({ chain: 'testnet', payTo: HOUSE, store: memoryStore() })The same deployment object feeds the agent, REST, and MCP: one tool list,
one network, one mode. createHost builds its own over the stock tools for
the action routes; createActionRoutes needs an ActionHost that can also
broadcast and poll, which is what a host is.
The routing
Section titled “The routing”Bun.serve({ async fetch(request) { const { pathname } = new URL(request.url) if (pathname === '/buy') return paywall.buy(request) if (pathname === '/api/actions') return actions(request) // free: approval flow steps if (pathname === '/api/tools') return Response.json({ tools: rest.catalogue() }) // Everything below costs a turn: free ones by IP first, then a bought pack, else 402. const charge = await paywall.charge(request) if (!charge.ok) return charge.response if (pathname === '/api/agent') return agent.fetch(request) if (pathname === '/api/mcp') return mcp.fetch(request) const tool = pathname.match(/^\/api\/tools\/([a-z0-9_]+)$/)?.[1] if (tool) return rest.call(tool, request) return new Response('Not found', { status: 404 }) },})Three decisions are visible in the routing and nowhere else:
- What costs a turn. The paywall is a line in front of a route, not a
property of the handler. The reference app charges nothing; the web agent
charges the agent, REST, and MCP
tools/call, and leaves the browser’s own reads and approval steps free. - What can sign. Nothing. Every handler was built in compose mode; the
action routes verify a wallet’s bytes against the draft and broadcast. To
sign server-side, a deployment takes
mode: 'execute'and aresolveSigner— see swap the signer — and you put auth in front of it. - Where the model is. One option on one handler. See swap the model.
Run an action to see what the action routes are serving; REST and x402 for the paywall’s store and credit rules.