@lucid-agents/tanstack
TanStack Start handlers for a completed Lucid HTTP runtime.
The TanStack adapter exposes Fetch-native Lucid HTTP handlers in the context shape expected by TanStack Start route modules. It delegates to the completed runtime.http.handlers; it does not create a second registry, execute handlers directly, or install an adapter-local paywall.
Installation
bun add @lucid-agents/core @lucid-agents/http @lucid-agents/tanstack @tanstack/react-startBasic usage
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { createTanStackRuntime } from '@lucid-agents/tanstack';
const agent = await createAgent({
name: 'my-agent',
version: '1.0.0',
})
.use(http({ basePath: '/api/agent' }))
.build();
export const { runtime, handlers, routes } = await createTanStackRuntime(agent);Always install http() before creating the TanStack wrapper. Call await runtime.close() during server shutdown.
API reference
createTanStackRuntime(runtime)
Returns the original runtime, adapted handlers, and the canonical route plan.
type TanStackRuntime<TRuntime> = {
runtime: TRuntime;
handlers: TanStackHandlers;
routes: readonly AgentHttpRoute[];
};| Property | Description |
|---|---|
runtime | The same completed runtime passed to the adapter. |
handlers | TanStack-context wrappers around runtime.http.handlers. |
routes | The same route paths, methods, and capability checks used by Hono and Express. |
createTanStackHandlers(runtime)
Creates just the adapted handler collection. createTanStackRuntime() calls this function for you.
type TanStackRequestHandler = (context: {
request: Request;
}) => Promise<Response>;
type TanStackRouteHandler<Params> = (context: {
request: Request;
params: Params;
}) => Promise<Response>;TanStackHandlers includes:
| Handler | Parameters | Purpose |
|---|---|---|
health | { request } | Health response. |
entrypoints | { request } | Entrypoint discovery. |
manifest | { request } | Agent Card. |
oasf | { request } | OASF record or 404. |
favicon | { request } | SVG favicon. |
landing | { request } | Optional landing page. |
invoke | { request, params: { key } } | Canonical invoke flow. |
stream | { request, params: { key } } | Canonical SSE flow. |
tasks | { request } | Create a task. |
listTasks | { request } | List owned tasks. |
getTask | { request, params: { taskId } } | Read an owned task. |
cancelTask | { request, params: { taskId } } | Cancel an owned task. |
subscribeTask | { request, params: { taskId } } | Stream task updates. |
Route modules
TanStack route files should pass the original request and validated route parameters to the returned handler. The handler already performs body parsing, schema validation, authorization, idempotency, execution, and response formatting.
import { createFileRoute } from '@tanstack/react-router';
import { handlers } from '@/lib/agent';
export const Route = createFileRoute('/api/agent/entrypoints/$key/invoke')({
server: {
handlers: {
POST: async ({ request, params }) => {
const key = params.key;
if (typeof key !== 'string') {
return new Response('Missing or invalid key parameter', {
status: 400,
});
}
return handlers.invoke({ request, params: { key } });
},
},
},
});Streaming uses the same delegation; do not build a second ReadableStream or rewrite SSE headers:
export const Route = createFileRoute('/api/agent/entrypoints/$key/stream')({
server: {
handlers: {
POST: ({ request, params }) =>
handlers.stream({ request, params: { key: params.key } }),
},
},
});Discovery routes delegate in the same way:
export const Route = createFileRoute('/.well-known/agent-card.json')({
server: {
handlers: {
GET: ({ request }) => handlers.manifest({ request }),
},
},
});Root discovery routes may delegate to a runtime configured with /api/agent because the handler itself is transport-neutral. routes reports the canonical base-path-prefixed plan for inspection and tooling.
Payments and authentication
Payments are configured as a runtime extension. TanStack routes need no payment wrapper:
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { payments, paymentsFromEnv } from '@lucid-agents/payments';
import { createTanStackRuntime } from '@lucid-agents/tanstack';
const agent = await createAgent({
name: 'paid-agent',
version: '1.0.0',
})
.use(payments({ config: paymentsFromEnv() }))
.use(http({ basePath: '/api/agent' }))
.addEntrypoint({
key: 'premium',
price: '0.01',
async handler() {
return { output: { result: 'premium content' } };
},
})
.build();
export const { runtime, handlers, routes } = await createTanStackRuntime(agent);The HTTP runtime uses one authorization path for invoke, stream, and task creation. x402, MPP, SIWX entitlements, payment-policy admission, settlement, and idempotency therefore match Hono and Express. If both x402 and MPP are installed, each priced entrypoint must set paymentProtocol: 'x402' | 'mpp'.
Tasks
The canonical route plan includes task routes only when a2a() is installed. A generated route module can still call a task handler without A2A, but it receives a structured 404 capability response.
Task creation accepts a 20–256 character Task-Access-Token header or generates a token and returns it with the task. List, get, cancel, and subscribe operations require the same token.
CLI templates
The CLI supplies two composable TanStack Start shells:
tanstack-headlessprovides the runtime route modules without a storefront.tanstack-uilayers a minimal read-only endpoint table over the same headless route modules.
bunx @lucid-agents/cli my-agent --adapter=tanstack-headless
bunx @lucid-agents/cli my-agent --adapter=tanstack-uiExports
export {
createTanStackRuntime,
createTanStackHandlers,
} from '@lucid-agents/tanstack';
export type {
TanStackHandlers,
TanStackRequestHandler,
TanStackRouteHandler,
TanStackRuntime,
} from '@lucid-agents/tanstack';