Fetch-native HTTP handlers, SSE helpers, a canonical route plan, and the shared entrypoint authorization gate for Lucid Agents.
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
const agent = await createAgent({ name: 'echo', version: '1.0.0' })
.use(http({ basePath: '/api/agent', servicePage: { preset: 'dossier' } }))
.addEntrypoint({
key: 'echo',
handler: async ({ input }) => ({ output: input }),
})
.build();
agent.http.basePath; // '/api/agent'
agent.http.handlers; // transport-neutral Fetch handlers
agent.http.routes; // canonical route/capability planbuildServicePageModel(card, options) converts a public Agent Card and health
response into the canonical storefront model used by generated UIs:
import { buildServicePageModel } from '@lucid-agents/http';
const card = await agent.http.handlers
.manifest(new Request('https://agent.example/.well-known/agent-card.json'))
.then(response => response.json());
const service = buildServicePageModel(card, {
health: { ok: true, version: card.version },
});The model preserves public identity/provider metadata, protocol interfaces, health, trust, offerings, schemas, examples, modes, security, prices, payment and SIWX requirements, skills, task support, endpoints, and extension descriptors. It does not accept or expose private runtime configuration.
The default Hono and Express landing page renders the same model as a minimal, portable endpoint directory. It contains no client JavaScript, invoke controls, schemas, or raw Agent Card JSON. Every invoke and stream operation appears once with its HTTP path, payment method, and price. Priced operations receive a high-contrast price treatment, while a compact Resolved Core mark identifies the quiet "Powered by Lucid Agents" footer attribution.
Choose one of three built-in designs and optionally override bounded semantic tokens:
import { defineServiceUi } from '@lucid-agents/http/service-ui';
export default defineServiceUi({
preset: 'folio', // dossier | folio | console
tokens: {
colors: { accent: '#2B302C' },
fonts: {
body: ['Instrument Sans', 'Avenir Next', 'sans-serif'],
stylesheetUrl: '/fonts/service-ui.css',
},
},
});Pass the config to http({ servicePage }). Use servicePage: false for an
API-only runtime. landingPage remains as a deprecated compatibility switch.
Unknown keys, unsafe fonts/URLs, invalid hex values, and inaccessible primary
color combinations fail during runtime construction.
Install domain extensions such as payments(), mpp(), identity(), and
a2a() before HTTP when possible. The kernel also honors HTTP's ordering
constraints if call-site order differs.
agent.http.routes is the only route definition used by the server adapters.
Each item contains a stable ID, method, path, path parameter names, and a
handle(Request, params) function.
With no base path, the plan includes:
GET /healthandGET /entrypoints;POST /entrypoints/:key/invokeand/stream;GET /.well-known/agent-card.jsonand the legacyagent.jsonalias;GET /.well-known/oasf-record.jsonand/favicon.svg;- the landing route, unless disabled;
- task routes only when
a2a()is installed.
basePath prefixes every capability consistently and is written into the agent
card's HTTP interface URL.
An adapter can bind the route plan without depending on its internals:
for (const route of agent.http.routes) {
framework.route(route.method, route.path, request =>
route.handle(request, framework.params(request))
);
}Hono and Express already do this. Generated Next and TanStack route modules call the same handler contract.
Invoke, stream, and task creation all call one authorization transaction. It:
- resolves one payment rail for the canonical entrypoint;
- verifies SIWX, x402, or MPP credentials in the owning extension;
- wins the target-side idempotency claim for invokes;
- admits the request by evaluating and reserving incoming policy capacity;
- executes an invoke or admits stream/task work;
- finalizes settlement/recording or releases reservations on a pre-admission failure.
Invoke finalization follows the handler result. Streaming and task requests finalize when their asynchronous work is successfully admitted, since their HTTP response is already live or accepted at that point.
Task creation has an additional ordering guarantee. An initial MPP challenge
does not reserve task capacity; a credential-bearing MPP request reserves its
durable task before verification can commit. HTTP then holds a renewable
prepared execution claim while finalizing x402 or MPP settlement and
atomically activates it before starting the handler or its timeout.
Uncommitted failures terminalize reserved state and release payment policy
capacity. Once a rail commits, any later execution-start or accounting failure
returns the receipt with a queryable terminal task capability.
Paid task creation requires a task store that truthfully declares
durability: 'durable'; the default process-local A2A store is accepted only
for free tasks. It also requires a caller-supplied Task-Access-Token, ensuring
the payer knows the recovery capability before authorization begins; the A2A
sendMessage() client supplies one automatically. These checks happen before
payment authorization. The client also sends an Idempotency-Key, returns
settlement headers on success, and exposes both keys plus any task/settlement
evidence through TaskCreationError when a response is interrupted or
non-successful.
The two-phase task transaction covers all HTTP response paths in a running
worker. It cannot atomically commit an external payment provider and a task
database across abrupt process loss. Production payment verifiers must
deduplicate settlement with the caller's Idempotency-Key and provide receipt
reconciliation; multi-worker hosts must persist invocation data needed to
resume an expired active task lease.
Task creation is not target-side idempotent. The idempotency header is a settlement-deduplication input for payment verifiers; clients must list or otherwise reconcile durable task state before retrying a creation request.
If both x402 and MPP are installed, every priced entrypoint must set
paymentProtocol: 'x402' | 'mpp'. An auth-only entrypoint requires an enabled
SIWX payments runtime. These conditions are validated when the entrypoint is
registered, including dynamic registration after build.
authorizeEntrypointRequest is exported for a custom transport that needs the
same gate. After successful verification, call authorization.admit(). Only an
admitted request may execute; call the admission's finalize(response) exactly
once for every admitted request, including error responses. Call abort() if
the transport abandons admitted work before it has a response.
Handlers accept standard Web Request objects and return Response objects:
const response = await agent.http.handlers.invoke(
new Request('https://example.test/api/agent/entrypoints/echo/invoke', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ input: { value: 1 } }),
}),
{ key: 'echo' }
);The manifest handler builds by request origin, so generated compatibility routes
must delegate to it rather than cache runtime.manifest.build() independently.
Target-side invoke idempotency is enabled by default. Send a stable 20–256
character Idempotency-Key when retrying an operation:
await fetch('https://agent.example/entrypoints/charge/invoke', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': 'charge:customer-42:invoice-2026-07',
},
body: JSON.stringify({ input: { amount: 10 } }),
});The key is scoped to the entrypoint and bound to the request body, ambient
authorization/cookie context, and a stable identity derived from the freshly
verified SIWX or payment credential. Verification happens before the claim;
policy reservation and settlement happen only after a new claim is won. A
completed 2xx response is replayed only for the same verified subject, with
Idempotency-Replayed: true; a concurrent duplicate or reuse with different
input returns 409. Authorization challenges and application failures release
the claim so they can be retried. If storing a completed response fails after
execution, HTTP returns 503 and retains the active claim to prevent duplicate
execution or settlement.
The default store is bounded and process-local. Multi-instance deployments must inject an atomic durable store:
http({
idempotency: {
store: durableIdempotencyStore,
inProgressTtlMs: 15 * 60_000,
retentionMs: 24 * 60 * 60_000,
},
});Set idempotency: false only when the application intentionally provides an
equivalent deduplication boundary elsewhere.
Streaming entrypoints use SSE envelopes. A stream handler receives an emit
function as its second argument and returns an optional final result:
.addEntrypoint({
key: 'tokens',
stream: async ({ input }, emit) => {
await emit({ kind: 'text', text: String(input) });
return { output: { done: true }, status: 'succeeded' };
},
})Low-level createSSEStream and writeSSE helpers are available for custom
transports. Envelope and route contracts are defined in
@lucid-agents/types/http.
createSSEStream() exposes asynchronous ready(), write(), and close()
operations. Awaiting them applies response-consumer backpressure. Its runner
signal aborts when either the request aborts or the response reader cancels,
so generators can stop external work promptly.
An MPP Tempo session-backed stream meters each handler emit() as one
configured unit. HTTP waits for downstream capacity, atomically charges the
verified session meter, and only then writes the envelope. Session control
frames are protocol-native SSE events:
event: payment-need-voucher
data: {"channelId":"0x...","requiredCumulative":"20",...}
event: payment-receipt
data: {"method":"tempo","intent":"session","spent":"20","units":2,...}
The stream waits for a live top-up according to the configured session policy.
If funding remains unavailable, it emits the standard session Problem Details
document as an error event and terminates with a failed run-end. Consumer
cancellation aborts the wait and releases only uncommitted meter work. Shared
payment policies reserve the verified maximum before streaming and finalize
exactly once with deliveredUnits * unitAmount after completion, failure, or
disconnect, so resumed and simultaneous streams do not double-count cumulative
channel spend.
invoke, invokeHandler, and stream are exported for integration code. They
operate on the canonical runtime/entrypoint definitions and perform Zod input
and output validation. Prefer the completed HTTP runtime unless you are writing
an adapter.