This guide helps AI coding agents understand and work with the lucid-agents monorepo effectively.
This is a TypeScript/Bun monorepo for building, monetizing, and verifying AI agents. It provides:
- @lucid-agents/core - Protocol-agnostic agent runtime with extension system
- @lucid-agents/http - HTTP extension for request/response handling
- @lucid-agents/identity - ERC-8004 identity and trust layer
- @lucid-agents/payments - x402 payment utilities
- @lucid-agents/wallet - Wallet SDK for agent and developer wallets
- @lucid-agents/a2a - A2A Protocol client for agent-to-agent communication
- @lucid-agents/ap2 - AP2 (Agent Payments Protocol) extension
- @lucid-agents/hono - Hono HTTP server adapter
- @lucid-agents/express - Express HTTP server adapter
- @lucid-agents/tanstack - TanStack Start adapter
- @lucid-agents/cli - CLI for scaffolding new agent projects
Tech Stack:
- Runtime: Bun (Node.js 20+ compatible)
- Language: TypeScript (ESM, strict mode)
- Build: tsup
- Package Manager: Bun workspaces
- Versioning: Changesets
cli (scaffolding tool)
↓ scaffolds projects using
hono OR express OR tanstack (adapters)
↓ both use
core (protocol-agnostic runtime)
↓ uses extensions
http, identity, payments, wallet, a2a, ap2 (extensions)
The framework uses an extension-based architecture where features are added via composable extensions:
- http (
@lucid-agents/http) - HTTP request/response handling, streaming, SSE - wallets (
@lucid-agents/wallet) - Wallet management for agents - payments (
@lucid-agents/payments) - x402 payment verification and pricing - identity (
@lucid-agents/identity) - ERC-8004 on-chain identity and trust - a2a (
@lucid-agents/a2a) - Agent-to-agent communication protocol - ap2 (
@lucid-agents/ap2) - Agent Payments Protocol extension
The framework supports multiple runtime adapters:
- Hono (
@lucid-agents/hono) - Traditional HTTP server - Express (
@lucid-agents/express) - Node.js/Express server with x402 middleware - TanStack Start (
@lucid-agents/tanstack) - Full-stack React with dashboard (UI) or API-only (headless)
Templates are adapter-agnostic and work with any compatible adapter.
HTTP Request
↓
Adapter Router (Hono, Express, or TanStack)
↓
x402 Paywall Middleware (if configured)
↓
Runtime Handler (core)
↓
Entrypoint Handler
↓
Response (JSON or SSE stream)
- Multi-adapter support - Same agent logic works with different frameworks
- Template-based scaffolding - Templates use
.templateextensions for clean code generation - Zod for validation - Schema-first approach for input/output
- Server-Sent Events for streaming - Standard SSE for real-time responses
- ERC-8004 for identity - On-chain agent identity and reputation
- x402 for payments - HTTP-native payment protocol supporting both EVM and Solana networks
The framework supports payment receiving on multiple blockchain networks:
EVM Networks:
base- Base mainnet (L2, low cost)base-sepolia- Base Sepolia testnetethereum- Ethereum mainnetsepolia- Ethereum Sepolia testnet
Solana Networks:
solana- Solana mainnet (high throughput, low fees)solana-devnet- Solana devnet
Key Differences:
- EVM: EIP-712 signatures, ERC-20 tokens (USDC), 0x-prefixed addresses
- Solana: Ed25519 signatures, SPL tokens (USDC), Base58 addresses
- Transaction finality: Solana (~400ms) vs EVM (12s-12min)
- Gas costs: Solana (~$0.0001) vs EVM ($0.01-$10)
Identity vs Payments:
- Identity registration (ERC-8004): EVM-only (smart contract on Ethereum chains)
- Payment receiving: Any supported network (EVM or Solana)
- These are independent: register identity on Base, receive payments on Solana
These principles guide how we organize and structure code across the monorepo. Follow them when writing new code or refactoring existing code.
One type definition per concept. Avoid duplicate types like PaymentsRuntimeInternal vs PaymentsRuntime. If you need variations, use type composition or generics, not separate type definitions.
Bad:
// Internal type
type PaymentsRuntimeInternal = { config: PaymentsConfig | undefined; activate: ... };
// Public type
type PaymentsRuntime = { config: PaymentsConfig; requirements: ... };Good:
// One type definition
type PaymentsRuntime = {
config: PaymentsConfig;
isActive: boolean;
requirements: ...;
activate: ...;
};Domain complexity belongs in the owning package. The payments package should handle all payments-related complexity. The core runtime should use it directly without transformation layers.
Bad:
// In core runtime - wrapping payments runtime
const paymentsRuntime = payments.config ? {
get config() { return payments.config!; },
get isActive() { return payments.isActive; },
requirements(...) { return evaluatePaymentRequirement(...); }
} : undefined;Good:
// In payments package - returns complete runtime
export function createPaymentsRuntime(...): PaymentsRuntime | undefined {
return {
config,
isActive,
requirements(...) { ... },
activate(...) { ... }
};
}
// In core runtime - use directly
const payments = createPaymentsRuntime(...);
return { payments };Expose runtimes directly without unnecessary wrappers. If the type matches what's needed, pass it through. Don't create intermediate objects or getters unless there's a clear need.
Bad:
return {
get payments() {
return payments.config ? { ...wrappedObject } : undefined;
},
};Good:
return {
wallets,
payments,
};Similar concepts should follow the same pattern. If wallets is exposed directly, payments should be too. Consistency reduces cognitive load and makes the codebase easier to understand.
Example:
// Both follow the same pattern
const wallets = createWalletsRuntime(config);
const payments = createPaymentsRuntime(opts?.payments, config);
return {
wallets, // Direct exposure
payments, // Direct exposure
};If something needs to be used by consumers, include it in the public type. Don't hide methods or use type casts. The public API should be complete and type-safe.
Bad:
// Internal method not in public type
payments.activate(def); // Type error or requires castGood:
// Public type includes all needed methods
type PaymentsRuntime = {
config: PaymentsConfig;
isActive: boolean;
requirements: ...;
activate: (entrypoint: EntrypointDef) => void; // Public method
};Avoid unnecessary getters, wrappers, and intermediate objects. Prefer straightforward code. Add complexity only when there's a clear benefit.
Bad:
// Unnecessary wrapper
const paymentsRuntime = {
get config() { return payments.config!; },
get isActive() { return payments.isActive; },
requirements(...) { return evaluate(...); }
};Good:
// Direct use
payments.requirements(...);Each package should own its complexity. The payments package creates and returns a complete PaymentsRuntime with all its methods. Consumers use it as-is without transformation.
Principle: Each package should return what consumers need, and consumers should use it directly without transformation layers.
Avoid layers like sync(), resolvedConfig vs activeConfig, etc. Keep it simple until you actually need the complexity. YAGNI (You Aren't Gonna Need It) applies.
Bad:
// Multiple config states
type PaymentsRuntime = {
config: PaymentsConfig | undefined;
resolvedConfig: PaymentsConfig | undefined;
activeConfig: PaymentsConfig | undefined;
sync: (agent: AgentCore) => void;
};Good:
// Single config state
type PaymentsRuntime = {
config: PaymentsConfig;
isActive: boolean;
activate: (entrypoint: EntrypointDef) => void;
};/
├── packages/
│ ├── core/ # Protocol-agnostic runtime
│ │ ├── src/
│ │ │ ├── core/ # AgentCore, entrypoint management
│ │ │ ├── extensions/ # AgentBuilder, extension system
│ │ │ ├── config/ # Config management
│ │ │ └── utils/ # Helper utilities
│ │ └── AGENTS.md # Package-specific guide
│ │
│ ├── http/ # HTTP extension
│ │ ├── src/
│ │ │ ├── extension.ts # HTTP extension definition
│ │ │ ├── invoke.ts # HTTP invocation logic
│ │ │ ├── stream.ts # HTTP streaming logic
│ │ │ └── sse.ts # Server-Sent Events
│ │ └── examples/
│ │
│ ├── wallet/ # Wallet SDK
│ │ ├── src/
│ │ │ └── env.ts # Wallet config from env
│ │ └── examples/
│ │
│ ├── payments/ # x402 payment utilities
│ │ ├── src/
│ │ │ └── extension.ts # Payments extension
│ │ └── examples/
│ │
│ ├── identity/ # ERC-8004 identity
│ │ ├── src/
│ │ │ ├── init.ts # createAgentIdentity()
│ │ │ ├── extension.ts # Identity extension
│ │ │ ├── registries/ # Registry clients
│ │ │ └── utils/ # Identity utilities
│ │ └── examples/
│ │
│ ├── a2a/ # A2A Protocol client
│ │ ├── src/
│ │ │ ├── extension.ts # A2A extension
│ │ │ └── client.ts # A2A client
│ │ └── examples/
│ │
│ ├── ap2/ # AP2 extension
│ │ └── src/
│ │ └── extension.ts # AP2 extension
│ │
│ ├── hono/ # Hono adapter
│ │ ├── src/
│ │ │ ├── app.ts # createAgentApp() for Hono
│ │ │ └── paywall.ts # x402 Hono middleware
│ │ └── examples/
│ │
│ ├── express/ # Express adapter
│ │ ├── src/
│ │ │ ├── app.ts # createAgentApp() for Express
│ │ │ └── paywall.ts # x402 Express middleware
│ │ └── __tests__/
│ │
│ ├── tanstack/ # TanStack adapter
│ │ ├── src/
│ │ │ ├── runtime.ts # createTanStackRuntime()
│ │ │ └── paywall.ts # x402 TanStack middleware
│ │ └── examples/
│ │
│ └── cli/ # CLI scaffolding tool
│ ├── src/
│ │ ├── index.ts # CLI implementation
│ │ └── adapters.ts # Adapter definitions
│ └── templates/ # Project templates
│ ├── blank/ # Minimal agent
│ ├── identity/ # Identity-enabled agent
│ ├── trading-data-agent/ # Trading data merchant
│ └── trading-recommendation-agent/ # Trading shopper
│
├── scripts/
│ ├── build-packages.ts # Build script
│ └── changeset-publish.ts # Publish script
│
└── package.json # Workspace config
# Install all dependencies
bun install
# Build all packages
bun run build
# or
bun run build:packages
# Version packages (for release)
bun run changeset
bun run release:version
# Publish packages
bun run release:publish
# Full release flow
bun run release# Work in a specific package
cd packages/core
# Build this package
bun run build
# Run tests
bun test
# Type check
bunx tsc --noEmit
# Watch mode (if configured)
bun run devcreateAgentApp(runtimeOrBuilder)
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { payments } from '@lucid-agents/payments';
import { paymentsFromEnv } from '@lucid-agents/payments';
import { createAgentApp } from '@lucid-agents/hono';
const agent = await createAgent({
name: 'my-agent',
version: '0.1.0',
description: 'Agent description',
})
.use(http())
.use(payments({ config: paymentsFromEnv() }))
.build();
const { app, addEntrypoint } = await createAgentApp(agent);createAgentApp(runtimeOrBuilder)
import { createAgent } from '@lucid-agents/core';
import { http } from '@lucid-agents/http';
import { createAgentApp } from '@lucid-agents/express';
const agent = await createAgent({
name: 'my-agent',
version: '0.1.0',
description: 'Agent description',
})
.use(http())
.build();
const { app, addEntrypoint } = await createAgentApp(agent);
// Express apps need to listen on a port
const server = app.listen(process.env.PORT ?? 3000);createTanStackRuntime(runtimeOrBuilder)
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: '0.1.0',
description: 'Agent description',
})
.use(http())
.build();
const { runtime: tanStackRuntime, handlers } = await createTanStackRuntime(agent);
// Use runtime.addEntrypoint() instead of addEntrypoint()
tanStackRuntime.addEntrypoint({ ... });
// Export for TanStack routes
export { runtime: tanStackRuntime, handlers };addEntrypoint(definition)
addEntrypoint({
key: 'echo',
description: 'Echo back input',
input: z.object({ text: z.string() }),
output: z.object({ text: z.string() }),
price: '1000', // Optional x402 price
handler: async ctx => {
return {
output: { text: ctx.input.text },
usage: { total_tokens: 0 },
};
},
});paymentsFromEnv()
import { paymentsFromEnv } from '@lucid-agents/payments';
const payments = paymentsFromEnv();
// Returns PaymentsConfig or undefinedcreateAgentIdentity(options)
import { createAgent } from '@lucid-agents/core';
import { wallets } from '@lucid-agents/wallet';
import { walletsFromEnv } from '@lucid-agents/wallet';
import { createAgentIdentity } from '@lucid-agents/identity';
const agent = await createAgent({
name: 'my-agent',
version: '1.0.0',
})
.use(wallets({ config: walletsFromEnv() }))
.build();
const identity = await createAgentIdentity({
runtime: agent,
domain: 'agent.example.com',
autoRegister: true, // Register if not exists
});
// Returns:
// - identity.record (if registered)
// - identity.clients (registry clients)
// - identity.trust (trust config)
// - identity.didRegister (whether just registered)getTrustConfig(identity)
import { getTrustConfig } from '@lucid-agents/identity';
const trustConfig = getTrustConfig(identity);
// Returns TrustConfig for agent manifestInteractive Mode
bunx @lucid-agents/cli my-agentWith Adapter Selection
# Hono adapter (traditional HTTP server)
bunx @lucid-agents/cli my-agent --adapter=hono
# Express adapter (Node-style HTTP server)
bunx @lucid-agents/cli my-agent --adapter=express
# TanStack UI (full dashboard)
bunx @lucid-agents/cli my-agent --adapter=tanstack-ui
# TanStack Headless (API only)
bunx @lucid-agents/cli my-agent --adapter=tanstack-headlessNon-Interactive Mode
bunx @lucid-agents/cli my-agent \
--adapter=tanstack-ui \
--template=identity \
--non-interactive \
--AGENT_DESCRIPTION="My custom agent" \
--PAYMENTS_RECEIVABLE_ADDRESS="0x..."Each template in packages/cli/templates/ contains:
template-name/
├── src/
│ ├── agent.ts # Agent definition
│ └── index.ts # Server setup
├── package.json # Dependencies
├── tsconfig.json # TypeScript config
├── template.json # Wizard configuration
├── template.schema.json # JSON Schema for arguments
├── AGENTS.md # Template-specific guide
└── README.md # User-facing docs
- Parse CLI args - Extract flags like
--template=identity --KEY=value - Load templates - Scan
templates/directory - Resolve template - Match
--templateflag or prompt user - Collect wizard answers:
- Use pre-supplied
--KEY=valueflags if present - Otherwise, prompt user (or use defaults in
--non-interactive)
- Use pre-supplied
- Copy template - Copy entire template directory to target
- Transform files:
- Update
package.jsonwith actual package name - Replace
{{AGENT_NAME}}tokens in README - Generate
.envwith wizard answers
- Update
- Remove artifacts - Delete
template.json - Install dependencies - Run
bun installif requested
- Edit files in
packages/cli/templates/[template-name]/ - Update
template.jsonif adding wizard prompts - Update
template.schema.jsonto document new arguments - Update
AGENTS.mdwith examples of new features - Test:
cd packages/cli bun run build cd ../.. bunx ./packages/cli/dist/index.js test-agent --template=[template-name]
- Create directory:
packages/cli/templates/my-template/ - Add required files:
mkdir -p src # Create src/agent.ts, src/index.ts # Copy package.json, tsconfig.json from blank template
- Create
template.json:{ "id": "my-template", "name": "My Template", "description": "Description here", "wizard": { "prompts": [ { "key": "SOME_CONFIG", "type": "input", "message": "Enter config value:", "defaultValue": "default" } ] } } - Create
template.schema.jsondocumenting all arguments - Create
AGENTS.mdwith comprehensive examples - Test the template
- Add to help text in
src/index.ts
Located in src/__tests__/ within each package:
import { describe, expect, test } from 'bun:test';
describe('MyModule', () => {
test('should do something', () => {
expect(something()).toBe(expected);
});
});Run tests:
bun test # All tests
bun test src/__tests__/specific.test.ts # Specific testExample-based testing in examples/ directories:
cd packages/core/examples
bun run client.ts # Test against running agent# Test template generation
cd /tmp
bunx /path/to/lucid-agents/packages/cli/dist/index.js test-agent --template=blank
cd test-agent
bun install
bun run devA pull request is not considered complete unless it includes:
- E2E example smoke test - If the PR adds or modifies SDK surface area (new extensions, entrypoints, or config patterns), add a corresponding smoke test in
packages/examples/src/__tests__/smoke.test.ts. Smoke tests verify that agents build, servers boot, agent cards are valid, and entrypoints respond correctly — all without external services. Runbun test packages/examples/src/__tests__/to confirm. - Documentation - Update relevant documentation:
AGENTS.mdfor architecture/patterns, package-levelREADME.mdfiles for usage, and inline JSDoc for public APIs. If you add a new package or extension, add it to the directory structure and dependency diagram sections.
When you make changes:
bun run changesetThis prompts you for:
- Which packages changed?
- Semver bump type (major/minor/patch)
- Summary of changes
Creates a file in .changeset/ describing the change.
bun run release:versionThis:
- Reads all changeset files
- Updates package.json versions
- Updates CHANGELOG.md files
- Removes processed changeset files
bun run release:publishThis:
- Builds all packages
- Publishes to npm
Full flow:
bun run release # version + publish- No emojis - Do not use emojis in code, comments, or commit messages unless explicitly requested by the user
- Re-exports are banned - Do not re-export types or values from other packages. Define types in the appropriate shared types package (
@lucid-agents/types) or in the package where they are used. Re-exports create unnecessary coupling and make it unclear where types are actually defined.
- ESM only - Use
import/export, notrequire() - Strict mode - All packages use
strict: true - Explicit types - Avoid
any, prefer explicit types orunknown - Type exports - Export types separately:
export type { MyType }
- Source:
kebab-case.ts - Types:
types.tsor inline - Tests:
*.test.tsin__tests__/ - Examples: Descriptive names in
examples/
Package structure:
src/
├── index.ts # Main exports
├── types.ts # Type definitions
├── feature1.ts # Feature implementation
├── feature2.ts
├── utils/
│ └── helpers.ts # Utility functions
└── __tests__/
└── feature1.test.ts
Export patterns:
// index.ts
export { mainFunction } from './feature1';
export { helperFunction } from './utils/helpers';
export type { MyType } from './types';Error handling:
try {
const result = await operation();
return result;
} catch (error) {
throw new Error(`Operation failed: ${(error as Error).message}`);
}Environment variables:
const value = process.env.KEY;
if (!value) {
throw new Error('KEY environment variable required');
}Zod schemas:
import { z } from 'zod';
const schema = z.object({
field: z.string().min(1),
optional: z.number().optional(),
});
type Parsed = z.infer<typeof schema>;// core imports identity types
import type { TrustConfig } from '@lucid-agents/identity';
// core accepts trust config
createAgentApp(meta, {
trust: trustConfig, // From identity
});Templates reference both packages:
// In generated agent.ts
import { createAgentApp } from '@lucid-agents/core';
import { createAgentIdentity } from '@lucid-agents/identity';The CLI doesn't directly import these; it scaffolds code that uses them.
When developing changes to packages and testing them in external projects (e.g., your own agent), use bun's linking feature:
Workflow:
-
Register packages globally - In the lucid-agents monorepo, register the packages you want to link:
cd lucid-agents/packages/types bun link cd ../wallet bun link cd ../identity bun link
-
Update your test project's
package.jsonto use thelink:protocol:{ "dependencies": { "@lucid-agents/identity": "link:@lucid-agents/identity", "@lucid-agents/types": "link:@lucid-agents/types", "@lucid-agents/wallet": "link:@lucid-agents/wallet" } } -
Install dependencies in your test project:
cd my-test-agent bun install -
Make changes in the linked package:
cd lucid-agents/packages/identity # Make your changes to source code bun run build # Build after changes
-
Test immediately - Changes are reflected in your test project automatically
-
Before committing, remember to change back to version references:
{ "dependencies": { "@lucid-agents/identity": "^1.12.0", "@lucid-agents/types": "^1.5.5", "@lucid-agents/wallet": "^0.5.5" } }
How it works:
bun linkregisters a package globally by name- The
link:@package-nameprotocol in package.json references the globally registered package - Any changes you make and build in the linked package are immediately available
- No need to publish to npm or reinstall dependencies
- Perfect for rapid iteration and testing
- Create implementation in
packages/core/src/feature.ts - Add types to
types.tsor inline - Export from
index.ts - Add tests in
__tests__/feature.test.ts - Update
README.mdwith examples - Update
AGENTS.mdwith AI-focused guide - Create changeset:
bun run changeset
- Update
EntrypointDeftype intypes.ts - Update manifest generation in
manifest.ts - Update routing in
app.ts - Add examples showing the new type
- Update template files if relevant
- Edit
packages/cli/src/index.ts - Build:
cd packages/cli && bun run build - Test locally:
bunx ./dist/index.js test-agent - Update help text and README
- Create changeset
Ensure:
- All packages are built:
bun run build:packages - Dependencies are installed:
bun install - Using correct import paths (e.g.,
@lucid-agents/core/types)
Templates use the built packages:
- Build packages first
- Check that template
package.jsonreferences correct versions - Run
bunx tsc --noEmitin template directory
Ensure:
- You're in the repo root
- Changes are committed to git
.changesetdirectory exists- Run
bunx changesetnotbun run changesetif workspace command fails
Check:
- TypeScript version matches across packages
- All imports are resolvable
- No circular dependencies
- Run
bun installagain
Core HTTP runtime that adapters wrap. Handles manifest building, entrypoint registry, streaming helpers, and payment evaluation.
Hono-specific createAgentApp() implementation. Wires Fetch handlers to Hono routes and installs the x402 middleware.
Express-specific createAgentApp() implementation. Bridges Node requests/responses to the Fetch-based runtime and installs the x402 Express middleware.
Generates AgentCard and manifest JSON. Includes A2A skills, payments metadata, trust registrations.
x402 payment middleware. Checks payment headers, validates with facilitator, enforces pricing.
Core type definitions: EntrypointDef, AgentContext, AgentMeta, PaymentsConfig, etc.
Main createAgentIdentity() function. Bootstraps ERC-8004 identity, handles auto-registration.
Registry client implementations for Identity, Reputation, and Validation registries.
CLI implementation. Handles argument parsing, wizard prompts, template copying, file transformation.
- CONTRIBUTING.md - Contribution guidelines
- agents.md - AGENTS.md standard documentation
- ERC-8004 Specification
- Hono Documentation
- Express Documentation
- Bun Documentation
- x402 Protocol
When working on this codebase:
- Check package READMEs - Each package has detailed documentation
- Check AGENTS.md files - Package-specific guides for AI agents
- Look at examples - All packages have
examples/directories - Review tests - Tests show expected behavior
- Check changesets - Recent changes documented in
.changeset/