-
+
{hint && !error && {hint}}
{error && {error}}
diff --git a/frontend/components/SpendingPolicy.tsx b/frontend/components/SpendingPolicy.tsx
index 94fca690..4e063b9d 100644
--- a/frontend/components/SpendingPolicy.tsx
+++ b/frontend/components/SpendingPolicy.tsx
@@ -99,7 +99,7 @@ export default function SpendingPolicyDisplay({ policy, walletAddress, agentOwne
}
const dailyUsed = Number(
- BigInt(policy.daily_spent_stroops) * 100n /
+ BigInt(policy.daily_spent_stroops || policy.spent_today_stroops || '0') * 100n /
BigInt(policy.max_per_day_stroops === '0' ? '1' : policy.max_per_day_stroops),
);
@@ -224,7 +224,7 @@ export default function SpendingPolicyDisplay({ policy, walletAddress, agentOwne
Daily spend used
- ${stroopsToUsdc(policy.daily_spent_stroops)} / ${stroopsToUsdc(policy.max_per_day_stroops)} USDC
+ ${stroopsToUsdc(policy.daily_spent_stroops || policy.spent_today_stroops || '0')} / ${stroopsToUsdc(policy.max_per_day_stroops)} USDC
diff --git a/frontend/components/StatsBar.tsx b/frontend/components/StatsBar.tsx
index de478368..52e882c5 100644
--- a/frontend/components/StatsBar.tsx
+++ b/frontend/components/StatsBar.tsx
@@ -40,15 +40,15 @@ export default function StatsBar() {
);
diff --git a/frontend/components/ThemeProvider.tsx b/frontend/components/ThemeProvider.tsx
index 21772090..fe5ab70d 100644
--- a/frontend/components/ThemeProvider.tsx
+++ b/frontend/components/ThemeProvider.tsx
@@ -43,10 +43,6 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
- if (!mounted) {
- return <>{children}>;
- }
-
return (
{children}
@@ -57,7 +53,7 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
- throw new Error('useTheme must be used within a ThemeProvider');
+ return { theme: 'light' as Theme, toggleTheme: () => {} };
}
return context;
}
diff --git a/frontend/jest.config.js b/frontend/jest.config.js
index c43b5a2b..c086d52c 100644
--- a/frontend/jest.config.js
+++ b/frontend/jest.config.js
@@ -8,6 +8,7 @@ const customJestConfig = {
setupFilesAfterEnv: ['/jest.setup.js'],
testEnvironment: 'jest-environment-jsdom',
moduleNameMapper: {
+ '^@lodestar/client$': '/../packages/client/index.js',
'^@/(.*)$': '/$1',
},
transformIgnorePatterns: ['/node_modules/(?!(next|@next|@creit-tech)/)'],
diff --git a/frontend/lib/contract.ts b/frontend/lib/contract.ts
index 6a7bde8c..5e80f3a4 100644
--- a/frontend/lib/contract.ts
+++ b/frontend/lib/contract.ts
@@ -1,53 +1,42 @@
-import type {
- ServiceEntry,
- StatsResponse,
- ServicesResponse,
- ReputationResponse,
- Category,
- AgentEntry,
- SpendingPolicy,
- AgentStats,
- AgentsResponse,
- AgentEligibilityResponse,
- AgentSpendCheckResponse,
- AgentSortOption,
-} from './types';
+import {
+ LodestarClient,
+ type ServiceEntry,
+ type StatsResponse,
+ type ServicesResponse,
+ type ReputationResponse,
+ type Category,
+ type AgentEntry,
+ type SpendingPolicy,
+ type AgentStats,
+ type AgentsResponse,
+ type AgentEligibilityResponse,
+ type AgentSpendCheckResponse,
+ type AgentSortOption,
+} from '../../packages/client/index.js';
import { PAGE_SIZE } from './pagination';
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:3001';
-async function apiFetch(path: string, init?: RequestInit): Promise {
- const res = await fetch(`${API_URL}${path}`, {
- headers: { 'Content-Type': 'application/json' },
- // 60s timeout to handle Render cold start (~50s wake time)
- signal: AbortSignal.timeout(60000),
- ...init,
- });
- if (!res.ok) {
- const body = (await res.json().catch(() => ({}))) as { error?: string };
- throw new Error(body.error ?? `Request failed: ${res.status}`);
- }
- return res.json() as Promise;
-}
+export const apiClient = new LodestarClient({
+ baseUrl: API_URL,
+ timeoutMs: 60000,
+});
export async function fetchServices(category?: Category): Promise {
- const query = category ? `?category=${category}` : '';
- const data = await apiFetch(`/api/services${query}`);
+ const data = await apiClient.getServices({ category });
return data.services;
}
export async function fetchStats(): Promise {
- return apiFetch('/api/stats');
+ return apiClient.getStats();
}
export async function fetchServiceById(id: number): Promise {
- return apiFetch(`/api/services/${id}`);
+ return apiClient.getServiceById(id);
}
export async function fetchServicesByProvider(address: string): Promise {
- const data = await apiFetch(
- `/api/registry/by-provider/${encodeURIComponent(address)}`
- );
+ const data = await apiClient.getServicesByProvider(address);
return data.services;
}
@@ -66,10 +55,7 @@ export async function submitReputation(
'No voting agent configured. Set NEXT_PUBLIC_DEMO_AGENT_ADDRESS to a registered demo agent.'
);
}
- return apiFetch(`/api/reputation/${id}`, {
- method: 'POST',
- body: JSON.stringify({ positive, agent }),
- });
+ return apiClient.submitReputation(id, { positive, agent });
}
export interface RegisterFormData {
@@ -80,38 +66,24 @@ export interface RegisterFormData {
category: Category;
}
-interface PreparedRegistryTxResponse {
- xdr: string;
- submitToken: string;
-}
-
-interface SubmittedRegistryTxResponse {
- success: boolean;
- hash: string;
- id: number | null;
-}
-
export async function registerService(
formData: RegisterFormData,
walletAddress: string
): Promise<{ txHash: string; id: number }> {
const { kitSignTransaction: signTx } = await import('./wallet');
- const prepared = await apiFetch('/api/registry/prepare-register', {
- method: 'POST',
- body: JSON.stringify({
- name: formData.name,
- description: formData.description,
- endpoint: formData.endpoint,
- priceUsdc: formData.price_usdc,
- category: formData.category,
- providerAddress: walletAddress,
- }),
+ const prepared = await apiClient.prepareRegisterService({
+ name: formData.name,
+ description: formData.description,
+ endpoint: formData.endpoint,
+ priceUsdc: formData.price_usdc,
+ category: formData.category,
+ providerAddress: walletAddress,
});
const signedXdr = await signTx(prepared.xdr);
- const result = await apiFetch('/api/registry/submit-signed-tx', {
- method: 'POST',
- body: JSON.stringify({ signedXdr, submitToken: prepared.submitToken }),
+ const result = await apiClient.submitSignedRegistryTx({
+ signedXdr,
+ submitToken: prepared.submitToken,
});
if (!result.success || result.id == null) {
@@ -124,7 +96,7 @@ export async function registerService(
// ── Agent Credit Scoring ──────────────────────────────────────────────────────
// Contract ID for the LodestarAgents on-chain program.
-// All current agent operations flow through the backend API (see apiFetch above).
+// All current agent operations flow through the backend API (see apiClient above).
// Wire this into a direct contract call if/when the frontend needs to invoke
// agent operations without a backend intermediary.
export const AGENTS_CONTRACT_ID = process.env.NEXT_PUBLIC_AGENTS_CONTRACT_ID ?? '';
@@ -134,30 +106,24 @@ export async function fetchAgents(
pageSize = PAGE_SIZE,
sort: AgentSortOption = 'score'
): Promise {
- return apiFetch(
- `/api/agents?page=${page}&pageSize=${pageSize}&sort=${sort}`
- );
+ return apiClient.getAgents({ page, pageSize, sort });
}
export async function fetchAgent(
address: string
): Promise<{ agent: AgentEntry; policy: SpendingPolicy | null }> {
- return apiFetch<{ agent: AgentEntry; policy: SpendingPolicy | null }>(
- `/api/agents/${address}`
- );
+ return apiClient.getAgent(address);
}
export async function fetchAgentStats(): Promise {
- return apiFetch('/api/agents/stats');
+ return apiClient.getAgentStats();
}
export async function fetchAgentEligibility(
address: string,
minScore: number
): Promise {
- return apiFetch(
- `/api/agents/${address}/eligible?min_score=${minScore}`
- );
+ return apiClient.getAgentEligibility(address, minScore);
}
export async function fetchAgentSpendCheck(
@@ -165,7 +131,5 @@ export async function fetchAgentSpendCheck(
amount: string,
category: string
): Promise {
- return apiFetch(
- `/api/agents/${address}/can-spend?amount=${encodeURIComponent(amount)}&category=${encodeURIComponent(category)}`
- );
+ return apiClient.checkAgentCanSpend(address, { amount, category });
}
diff --git a/frontend/lib/sort.ts b/frontend/lib/sort.ts
index 37dd765b..d9ad222d 100644
--- a/frontend/lib/sort.ts
+++ b/frontend/lib/sort.ts
@@ -19,7 +19,7 @@ export function sortServices(
return parseFloat(a.price_usdc) - parseFloat(b.price_usdc);
}
// 'newest' - highest registered_at first
- return b.registered_at - a.registered_at;
+ return (b.registered_at ?? 0) - (a.registered_at ?? 0);
});
}
@@ -42,7 +42,7 @@ export function sortAgents(
return Number(b.total_payments) - Number(a.total_payments);
}
// 'newest' - highest registered_at first
- return Number(b.registered_at) - Number(a.registered_at);
+ return Number(b.registered_at ?? 0) - Number(a.registered_at ?? 0);
});
}
@@ -67,7 +67,7 @@ export function sortServicesWithTieBreaker(
} else if (sort === 'price') {
result = parseFloat(a.price_usdc) - parseFloat(b.price_usdc);
} else {
- result = b.registered_at - a.registered_at;
+ result = (b.registered_at ?? 0) - (a.registered_at ?? 0);
}
if (result === 0 && tieBreaker) {
return tieBreaker(a, b);
@@ -96,7 +96,7 @@ export function sortAgentsWithTieBreaker(
} else if (sort === 'payments') {
result = Number(b.total_payments) - Number(a.total_payments);
} else {
- result = Number(b.registered_at) - Number(a.registered_at);
+ result = Number(b.registered_at ?? 0) - Number(a.registered_at ?? 0);
}
if (result === 0 && tieBreaker) {
return tieBreaker(a, b);
diff --git a/frontend/lib/types.ts b/frontend/lib/types.ts
index 41a8335f..804f2569 100644
--- a/frontend/lib/types.ts
+++ b/frontend/lib/types.ts
@@ -1,18 +1,17 @@
-export type Category = 'search' | 'weather' | 'finance' | 'ai' | 'data' | 'compute';
-
-export interface ServiceEntry {
- id: number;
- name: string;
- description: string;
- endpoint: string;
- price_usdc: string;
- category: Category;
- provider: string;
- reputation: number;
- active: boolean;
- registered_at: number;
- ttl_warning?: boolean;
-}
+export type {
+ Category,
+ ServiceEntry,
+ ServicesResponse,
+ StatsResponse,
+ ReputationResponse,
+ AgentEntry,
+ SpendingPolicy,
+ AgentSortOption,
+ AgentsResponse,
+ AgentStats,
+ AgentEligibilityResponse,
+ AgentSpendCheckResponse,
+} from '../../packages/client/index.js';
export interface ApiResponse {
data?: T;
@@ -20,23 +19,6 @@ export interface ApiResponse {
code?: string;
}
-export interface StatsResponse {
- totalServices: number;
- categories: Category[];
- latestService: ServiceEntry | null;
-}
-
-export interface ServicesResponse {
- services: ServiceEntry[];
- count: number;
-}
-
-export interface ReputationResponse {
- success: boolean;
- newReputation: number;
- txHash?: string;
-}
-
export interface ActivityEntry {
timestamp: string;
agent: string;
@@ -84,63 +66,8 @@ export const TIER_COLORS: Record = {
elite: 'text-amber-600 bg-amber-50',
};
-export interface AgentEntry {
- address: string;
- name: string;
- description: string;
- owner: string;
- score: number;
- total_payments: string;
- successful_payments: string;
- failed_payments: string;
- total_volume_stroops: string;
- registered_at: string;
- last_active: string;
- active: boolean;
- flagged: boolean;
- flag_reason: string;
-}
-
-export interface SpendingPolicy {
- agent_address: string;
- max_per_tx_stroops: string;
- max_per_day_stroops: string;
- allowed_categories: string[];
- min_score_to_earn: number;
- daily_spent_stroops: string;
- last_reset_ledger: string;
-}
-
-export type AgentSortOption = 'score' | 'payments' | 'newest';
-
-export interface AgentsResponse {
- agents: AgentEntry[];
- total: number;
- page: number;
- pageSize: number;
-}
-
export interface AgentRegisterRequest {
agentAddress: string;
name: string;
description: string;
}
-
-export interface AgentStats {
- totalAgents: number;
- avgScore: number;
- topAgent: AgentEntry | null;
- totalVolume: string;
- totalVolumeStroops: string;
-}
-
-export interface AgentEligibilityResponse {
- eligible: boolean;
- score: number;
- required: number;
-}
-
-export interface AgentSpendCheckResponse {
- allowed: boolean;
- reason: string;
-}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 095674f6..10ce7e8e 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -15,7 +15,10 @@
"incremental": true,
"types": ["jest", "node", "@testing-library/jest-dom"],
"plugins": [{ "name": "next" }],
- "paths": { "@/*": ["./*"] }
+ "paths": {
+ "@/*": ["./*"],
+ "@lodestar/client": ["../packages/client/index.js"]
+ }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx"]
diff --git a/packages/client/README.md b/packages/client/README.md
new file mode 100644
index 00000000..1603d638
--- /dev/null
+++ b/packages/client/README.md
@@ -0,0 +1,104 @@
+# @lodestar/client
+
+Typed OpenAPI client for the [Lodestar](https://github.com/Stellar-Ecosystem/lodestar) backend API (#832).
+
+Lodestar is the discovery and credit scoring protocol for x402 AI agents on Stellar. `@lodestar/client` provides a typed, promise-based API client for interacting with Lodestar registry and agent credit scoring endpoints.
+
+## Installation
+
+```bash
+npm install @lodestar/client
+```
+
+## Quickstart
+
+### JavaScript / ESM
+
+```javascript
+import { LodestarClient, createClient } from '@lodestar/client';
+
+const client = createClient({
+ baseUrl: process.env.LODESTAR_API_URL || 'http://localhost:3001',
+ timeoutMs: 30_000,
+});
+
+// Discover active services in the 'weather' category
+const { services } = await client.getServices({ category: 'weather' });
+
+// Query an agent's credit score and spending policy
+const { agent, policy } = await client.getAgent('GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAA');
+console.log(`Agent ${agent.name} score: ${agent.score}`);
+```
+
+### TypeScript
+
+```typescript
+import { LodestarClient, ServiceEntry, AgentEntry } from '@lodestar/client';
+
+const client = new LodestarClient({
+ baseUrl: 'https://api.lodestar.example',
+});
+
+// Fully typed response objects
+const services: ServiceEntry[] = (await client.getServices({ category: 'ai' })).services;
+```
+
+## API Methods
+
+### System & Health
+
+- `client.getHealth()`: Check process liveness, uptime, and transaction queues.
+- `client.getReadiness()`: Check dependency connectivity (Soroban RPC, Redis).
+
+### Service Registry & Reputation
+
+- `client.getStats()`: Aggregate registry and agent metrics.
+- `client.getServices({ category })`: List active registered services (optionally filtered by category).
+- `client.getServiceById(id)`: Fetch details for a specific service.
+- `client.getServicesByProvider(address)`: Fetch all services registered by a provider address.
+- `client.prepareRegisterService(data)`: Generate unsigned Soroban transaction XDR for registering a new service.
+- `client.submitSignedRegistryTx({ signedXdr, submitToken })`: Submit a wallet-signed registration transaction.
+- `client.submitReputation(id, { positive, agent })`: Cast an on-chain reputation vote for a service.
+
+### Agent Scoring & Policy
+
+- `client.getAgents({ page, pageSize, sort })`: Paginated list of agents sorted by score, payments, or registration date.
+- `client.getAgentStats()`: Aggregate scoring statistics and tier distribution (unrated, bronze, silver, gold, platinum).
+- `client.getAgent(address)`: Get an agent's profile, credit score, and spending policy.
+- `client.registerAgent({ name, description, address, endpoint })`: Register an AI agent on-chain.
+- `client.getAgentEligibility(address, minScore)`: Verify if an agent meets the minimum score threshold for a tier.
+- `client.checkAgentCanSpend(address, { amount, category })`: Validate transaction against daily and per-tx spending limits.
+- `client.recordAgentPayment(address, { success, stroops, txHash })`: Record payment execution and update score.
+- `client.buildAgentTx(address, data, callerAddress)`: Build unsigned transaction for owner policy updates.
+- `client.submitSignedAgentTx(address, { signedXdr })`: Submit signed agent policy transaction.
+
+### Activity
+
+- `client.getActivity({ page, limit })`: Paginated event stream of on-chain and off-chain protocol activity.
+- `client.getDemoActivity()`: Recent demo runs and service invocations.
+
+## Error Handling
+
+All failed HTTP responses throw `LodestarApiError`:
+
+```javascript
+import { LodestarApiError } from '@lodestar/client';
+
+try {
+ await client.getAgent('INVALID_ADDRESS');
+} catch (err) {
+ if (err instanceof LodestarApiError) {
+ console.error(`API Error (${err.status}): ${err.message}`);
+ console.error(`Error Code: ${err.code}`);
+ console.error(`Request ID: ${err.requestId}`);
+ }
+}
+```
+
+## OpenAPI Specification
+
+The complete OpenAPI 3.0 specification is bundled with the package:
+
+```javascript
+import spec from '@lodestar/client/openapi.json' assert { type: 'json' };
+```
diff --git a/packages/client/client.test.js b/packages/client/client.test.js
new file mode 100644
index 00000000..9b9c8c1f
--- /dev/null
+++ b/packages/client/client.test.js
@@ -0,0 +1,437 @@
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { LodestarClient, createClient, LodestarApiError } from './index.js';
+
+describe('LodestarClient', () => {
+ let mockFetch;
+ let client;
+
+ beforeEach(() => {
+ mockFetch = vi.fn();
+ client = new LodestarClient({
+ baseUrl: 'http://api.test',
+ timeoutMs: 5000,
+ fetch: mockFetch,
+ });
+ });
+
+ function jsonResponse(data, status = 200, headers = {}) {
+ const normalizedHeaders = Object.fromEntries(
+ Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])
+ );
+ return Promise.resolve({
+ ok: status >= 200 && status < 300,
+ status,
+ headers: {
+ get: (name) => {
+ const lower = name.toLowerCase();
+ if (lower === 'content-type' && !normalizedHeaders['content-type']) {
+ return 'application/json';
+ }
+ return normalizedHeaders[lower] || null;
+ },
+ },
+ json: () => Promise.resolve(data),
+ text: () => Promise.resolve(JSON.stringify(data)),
+ });
+ }
+
+ describe('Initialization and Configuration', () => {
+ it('strips trailing slashes from baseUrl', () => {
+ const c = new LodestarClient({ baseUrl: 'http://localhost:3001///' });
+ expect(c.baseUrl).toBe('http://localhost:3001');
+ });
+
+ it('defaults to localhost:3001 if baseUrl is not provided', () => {
+ const c = createClient();
+ expect(c.baseUrl).toBe('http://localhost:3001');
+ });
+
+ it('sets custom default headers', async () => {
+ const c = new LodestarClient({
+ baseUrl: 'http://api.test',
+ headers: { 'X-Custom-Header': 'CustomValue' },
+ fetch: mockFetch,
+ });
+ mockFetch.mockReturnValue(jsonResponse({ status: 'ok' }));
+
+ await c.getHealth();
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/healthz',
+ expect.objectContaining({
+ headers: expect.objectContaining({
+ 'X-Custom-Header': 'CustomValue',
+ 'Content-Type': 'application/json',
+ }),
+ })
+ );
+ });
+ });
+
+ describe('System endpoints', () => {
+ it('getHealth calls GET /healthz', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ status: 'ok', uptimeSeconds: 123 }));
+
+ const res = await client.getHealth();
+ expect(res).toEqual({ status: 'ok', uptimeSeconds: 123 });
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/healthz',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getReadiness calls GET /readyz', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ ready: true, status: 'ready' }));
+
+ const res = await client.getReadiness();
+ expect(res).toEqual({ ready: true, status: 'ready' });
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/readyz',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+ });
+
+ describe('Registry & Services endpoints', () => {
+ it('getStats calls GET /api/stats', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ total_services: 5, total_categories: 3 }));
+
+ const res = await client.getStats();
+ expect(res.total_services).toBe(5);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/stats',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getServices calls GET /api/services with query params', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ services: [{ id: 1, name: 'Weather API' }] }));
+
+ const res = await client.getServices({ category: 'weather' });
+ expect(res.services).toHaveLength(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/services?category=weather',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getServices ignores "all" category filter', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ services: [] }));
+
+ await client.getServices({ category: 'all' });
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/services',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getServiceById calls GET /api/services/:id', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ id: 42, name: 'Search Service' }));
+
+ const res = await client.getServiceById(42);
+ expect(res.id).toBe(42);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/services/42',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getServicesByProvider calls GET /api/registry/by-provider/:address', async () => {
+ const address = 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAA';
+ mockFetch.mockReturnValue(jsonResponse({ services: [{ id: 1 }] }));
+
+ const res = await client.getServicesByProvider(address);
+ expect(res.services).toHaveLength(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ `http://api.test/api/registry/by-provider/${address}`,
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('prepareRegisterService calls POST /api/registry/prepare-register with JSON body', async () => {
+ const reqData = {
+ name: 'New Service',
+ description: 'Testing',
+ endpoint: 'https://example.com/api',
+ priceUsdc: '0.05',
+ category: 'ai',
+ providerAddress: 'GA7Q...',
+ };
+ mockFetch.mockReturnValue(jsonResponse({ xdr: 'AAAA...', submitToken: 'tok123' }));
+
+ const res = await client.prepareRegisterService(reqData);
+ expect(res.submitToken).toBe('tok123');
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/registry/prepare-register',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify(reqData),
+ })
+ );
+ });
+
+ it('submitSignedRegistryTx calls POST /api/registry/submit-signed-tx', async () => {
+ const reqData = { signedXdr: 'AAAA...', submitToken: 'tok123' };
+ mockFetch.mockReturnValue(jsonResponse({ success: true, hash: 'tx123', id: 5 }));
+
+ const res = await client.submitSignedRegistryTx(reqData);
+ expect(res.success).toBe(true);
+ expect(res.id).toBe(5);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/registry/submit-signed-tx',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify(reqData),
+ })
+ );
+ });
+
+ it('submitReputation calls POST /api/reputation/:id', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ newReputation: 15, txHash: 'txhash1' }));
+
+ const res = await client.submitReputation(10, { positive: true, agent: 'GA7Q...' });
+ expect(res.newReputation).toBe(15);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/reputation/10',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify({ positive: true, agent: 'GA7Q...' }),
+ })
+ );
+ });
+ });
+
+ describe('Agent endpoints', () => {
+ it('getAgents calls GET /api/agents with pagination and sorting params', async () => {
+ mockFetch.mockReturnValue(
+ jsonResponse({ agents: [], total: 0, page: 1, pageSize: 10, totalPages: 0 })
+ );
+
+ const res = await client.getAgents({ page: 1, pageSize: 10, sort: 'payments' });
+ expect(res.page).toBe(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents?page=1&pageSize=10&sort=payments',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getAgentStats calls GET /api/agents/stats', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ total_agents: 10, average_score: 750 }));
+
+ const res = await client.getAgentStats();
+ expect(res.total_agents).toBe(10);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/stats',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getAgent calls GET /api/agents/:address', async () => {
+ const address = 'GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAA';
+ mockFetch.mockReturnValue(
+ jsonResponse({
+ agent: { address, name: 'Agent 1', score: 800 },
+ policy: null,
+ })
+ );
+
+ const res = await client.getAgent(address);
+ expect(res.agent.name).toBe('Agent 1');
+ expect(mockFetch).toHaveBeenCalledWith(
+ `http://api.test/api/agents/${address}`,
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('registerAgent calls POST /api/agents/register', async () => {
+ const reqData = {
+ name: 'Autonomous Agent',
+ description: 'Executes trades',
+ address: 'GA7Q...',
+ };
+ mockFetch.mockReturnValue(jsonResponse({ txHash: 'txhash_reg', agent: reqData }));
+
+ const res = await client.registerAgent(reqData);
+ expect(res.txHash).toBe('txhash_reg');
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/register',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify(reqData),
+ })
+ );
+ });
+
+ it('getAgentEligibility calls GET /api/agents/:address/eligible', async () => {
+ const address = 'GA7Q...';
+ mockFetch.mockReturnValue(jsonResponse({ eligible: true, score: 900, minScore: 500 }));
+
+ const res = await client.getAgentEligibility(address, 500);
+ expect(res.eligible).toBe(true);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/GA7Q.../eligible?min_score=500',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('checkAgentCanSpend calls GET /api/agents/:address/can-spend with parameters', async () => {
+ const address = 'GA7Q...';
+ mockFetch.mockReturnValue(jsonResponse({ allowed: true }));
+
+ const res = await client.checkAgentCanSpend(address, {
+ amount: '0.05',
+ category: 'weather',
+ });
+ expect(res.allowed).toBe(true);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/GA7Q.../can-spend?amount=0.05&category=weather',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('recordAgentPayment calls POST /api/agents/:address/payment', async () => {
+ const address = 'GA7Q...';
+ const paymentData = { success: true, stroops: '500000', txHash: 'txpay1' };
+ mockFetch.mockReturnValue(jsonResponse({ newScore: 820, txHash: 'txrec' }));
+
+ const res = await client.recordAgentPayment(address, paymentData);
+ expect(res.newScore).toBe(820);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/GA7Q.../payment',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify(paymentData),
+ })
+ );
+ });
+
+ it('buildAgentTx includes x-caller-address header', async () => {
+ const address = 'GA7Q...';
+ const caller = 'GCALLER...';
+ const data = { action: 'update_policy', max_per_tx_stroops: '1000' };
+ mockFetch.mockReturnValue(jsonResponse({ xdr: 'AAAA_TX' }));
+
+ const res = await client.buildAgentTx(address, data, caller);
+ expect(res.xdr).toBe('AAAA_TX');
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/GA7Q.../build-tx',
+ expect.objectContaining({
+ method: 'POST',
+ headers: expect.objectContaining({
+ 'x-caller-address': caller,
+ 'Content-Type': 'application/json',
+ }),
+ body: JSON.stringify(data),
+ })
+ );
+ });
+
+ it('submitSignedAgentTx calls POST /api/agents/:address/submit-signed-tx', async () => {
+ const address = 'GA7Q...';
+ mockFetch.mockReturnValue(jsonResponse({ txHash: 'signed_tx_hash' }));
+
+ const res = await client.submitSignedAgentTx(address, { signedXdr: 'AAAA...' });
+ expect(res.txHash).toBe('signed_tx_hash');
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/agents/GA7Q.../submit-signed-tx',
+ expect.objectContaining({
+ method: 'POST',
+ body: JSON.stringify({ signedXdr: 'AAAA...' }),
+ })
+ );
+ });
+ });
+
+ describe('Activity endpoints', () => {
+ it('getActivity calls GET /api/activity with pagination', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ events: [{ id: 'evt1' }] }));
+
+ const res = await client.getActivity({ page: 2, limit: 15 });
+ expect(res.events).toHaveLength(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/api/activity?page=2&limit=15',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+
+ it('getDemoActivity calls GET /demo/activity', async () => {
+ mockFetch.mockReturnValue(jsonResponse({ activity: [{ id: 'demo1' }] }));
+
+ const res = await client.getDemoActivity();
+ expect(res.activity).toHaveLength(1);
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'http://api.test/demo/activity',
+ expect.objectContaining({ method: 'GET' })
+ );
+ });
+ });
+
+ describe('Error handling', () => {
+ it('throws LodestarApiError on HTTP error status with response details', async () => {
+ mockFetch.mockReturnValue(
+ jsonResponse(
+ { error: 'Invalid Stellar address format', code: 'INVALID_ADDRESS', requestId: 'req_123' },
+ 400
+ )
+ );
+
+ await expect(client.getAgent('INVALID')).rejects.toThrow(LodestarApiError);
+
+ try {
+ await client.getAgent('INVALID');
+ } catch (err) {
+ expect(err.status).toBe(400);
+ expect(err.code).toBe('INVALID_ADDRESS');
+ expect(err.requestId).toBe('req_123');
+ expect(err.message).toBe('Invalid Stellar address format');
+ }
+ });
+
+ it('extracts requestId from x-request-id response header if not in JSON body', async () => {
+ mockFetch.mockReturnValue(
+ jsonResponse(
+ { error: 'Internal server error' },
+ 500,
+ { 'X-Request-Id': 'header-req-id-789' }
+ )
+ );
+
+ try {
+ await client.getHealth();
+ expect.unreachable();
+ } catch (err) {
+ expect(err).toBeInstanceOf(LodestarApiError);
+ expect(err.status).toBe(500);
+ expect(err.requestId).toBe('header-req-id-789');
+ }
+ });
+
+ it('handles network failure by throwing LodestarApiError with NETWORK_ERROR', async () => {
+ mockFetch.mockRejectedValue(new Error('Connection refused'));
+
+ try {
+ await client.getHealth();
+ expect.unreachable();
+ } catch (err) {
+ expect(err).toBeInstanceOf(LodestarApiError);
+ expect(err.code).toBe('NETWORK_ERROR');
+ expect(err.message).toBe('Connection refused');
+ }
+ });
+
+ it('handles timeout error by throwing LodestarApiError with TIMEOUT', async () => {
+ const abortErr = new Error('The operation was aborted');
+ abortErr.name = 'TimeoutError';
+ mockFetch.mockRejectedValue(abortErr);
+
+ try {
+ await client.getHealth();
+ expect.unreachable();
+ } catch (err) {
+ expect(err).toBeInstanceOf(LodestarApiError);
+ expect(err.status).toBe(408);
+ expect(err.code).toBe('TIMEOUT');
+ }
+ });
+ });
+});
diff --git a/packages/client/index.d.ts b/packages/client/index.d.ts
new file mode 100644
index 00000000..0d21a24d
--- /dev/null
+++ b/packages/client/index.d.ts
@@ -0,0 +1,303 @@
+export type Category = 'search' | 'weather' | 'finance' | 'ai' | 'data' | 'compute';
+
+export type AgentSortOption = 'score' | 'payments' | 'registered_at' | 'newest';
+
+export interface ServiceEntry {
+ id: number;
+ name: string;
+ description: string;
+ endpoint: string;
+ price_usdc: string;
+ category: Category;
+ provider: string;
+ reputation: number;
+ active: boolean;
+ registered_at?: number | null;
+}
+
+export interface ServicesResponse {
+ services: ServiceEntry[];
+ total?: number;
+ count?: number;
+}
+
+export interface StatsResponse {
+ total_services: number;
+ total_categories: number;
+ active_services: number;
+ top_category: string;
+ total_agents?: number;
+ total_volume_stroops?: string;
+ total_volume_usdc?: string;
+ average_score?: number;
+ totalServices?: number;
+ categories?: Category[];
+ latestService?: ServiceEntry | null;
+}
+
+export interface PrepareRegisterRequest {
+ name: string;
+ description: string;
+ endpoint: string;
+ priceUsdc: string;
+ category: Category;
+ providerAddress: string;
+}
+
+export interface PrepareRegisterResponse {
+ xdr: string;
+ submitToken: string;
+}
+
+export interface SubmitSignedRegistryTxRequest {
+ signedXdr: string;
+ submitToken: string;
+}
+
+export interface SubmitSignedRegistryTxResponse {
+ success: boolean;
+ hash: string;
+ id: number | null;
+}
+
+export interface SubmitReputationRequest {
+ positive: boolean;
+ agent: string;
+}
+
+export interface ReputationResponse {
+ newReputation: number;
+ success?: boolean;
+ txHash?: string | null;
+}
+
+export interface AgentEntry {
+ address: string;
+ name: string;
+ description: string;
+ endpoint?: string;
+ score: number;
+ total_payments: string;
+ successful_payments: string;
+ failed_payments: string;
+ total_volume_stroops: string;
+ active: boolean;
+ flagged: boolean;
+ registered_at?: number | string | null;
+ last_active?: string | null;
+ flag_reason?: string | null;
+ owner?: string;
+}
+
+export interface SpendingPolicy {
+ max_per_tx_stroops: string;
+ max_per_day_stroops: string;
+ spent_today_stroops?: string;
+ last_spend_day?: number;
+ allowed_categories: string[];
+ min_score_to_earn: number;
+ agent_address?: string;
+ daily_spent_stroops?: string;
+ last_reset_ledger?: string;
+}
+
+export interface AgentsResponse {
+ agents: AgentEntry[];
+ total: number;
+ page: number;
+ pageSize: number;
+ totalPages?: number;
+}
+
+export interface TierDistribution {
+ unrated?: number;
+ bronze?: number;
+ silver?: number;
+ gold?: number;
+ platinum?: number;
+}
+
+export interface AgentStats {
+ total_agents?: number;
+ active_agents?: number;
+ flagged_agents?: number;
+ average_score?: number;
+ total_volume_stroops?: string;
+ tier_distribution?: TierDistribution;
+ totalAgents?: number;
+ avgScore?: number;
+ topAgent?: AgentEntry | null;
+ totalVolume?: string;
+ totalVolumeStroops?: string;
+}
+
+export interface AgentProfileResponse {
+ agent: AgentEntry;
+ policy: SpendingPolicy | null;
+}
+
+export interface RegisterAgentRequest {
+ name: string;
+ description: string;
+ address: string;
+ endpoint?: string;
+ agentAddress?: string;
+ maxPerTxUsdc?: string;
+ maxPerDayUsdc?: string;
+ allowedCategories?: string[];
+}
+
+export interface RegisterAgentResponse {
+ txHash?: string | null;
+ agent: AgentEntry;
+}
+
+export interface AgentEligibilityResponse {
+ eligible: boolean;
+ score: number;
+ minScore?: number;
+ required?: number;
+}
+
+export interface AgentSpendCheckResponse {
+ allowed: boolean;
+ reason?: string | null;
+ currentScore?: number | null;
+ dailySpent?: string | null;
+ dailyLimit?: string | null;
+}
+
+export interface RecordPaymentRequest {
+ success: boolean;
+ stroops: string;
+ txHash?: string;
+}
+
+export interface RecordPaymentResponse {
+ newScore: number;
+ txHash?: string | null;
+}
+
+export interface BuildAgentTxRequest {
+ action: string;
+ [key: string]: unknown;
+}
+
+export interface BuildAgentTxResponse {
+ xdr: string;
+}
+
+export interface SubmitSignedAgentTxRequest {
+ signedXdr: string;
+}
+
+export interface SubmitSignedAgentTxResponse {
+ txHash: string;
+}
+
+export interface ActivityEvent {
+ id: string;
+ type: string;
+ timestamp: string;
+ data?: Record;
+}
+
+export interface ActivityResponse {
+ events: ActivityEvent[];
+ pagination?: Record;
+}
+
+export interface DemoActivityEntry {
+ id: string;
+ type: string;
+ timestamp: string;
+ summary: string;
+ agent?: string;
+ service?: string;
+ amount?: string;
+ txHash?: string;
+}
+
+export interface DemoActivityResponse {
+ activity: DemoActivityEntry[];
+}
+
+export interface HealthResponse {
+ status: string;
+ uptimeSeconds: number;
+ queueDepth?: number;
+ pendingTransactions?: number;
+ timestamp: string;
+}
+
+export interface ReadinessResponse {
+ ready: boolean;
+ status: string;
+ rpc?: Record;
+ redis?: Record;
+ timestamp: string;
+}
+
+export interface ErrorResponse {
+ error: string;
+ code?: string;
+ requestId?: string;
+}
+
+export interface ClientOptions {
+ baseUrl?: string;
+ timeoutMs?: number;
+ fetch?: typeof fetch;
+ headers?: Record;
+}
+
+export interface RequestOptions extends Omit {
+ headers?: Record;
+ timeoutMs?: number;
+}
+
+export class LodestarApiError extends Error {
+ readonly status: number;
+ readonly code?: string;
+ readonly body?: unknown;
+ readonly requestId?: string;
+
+ constructor(message: string, status: number, body?: unknown, code?: string, requestId?: string);
+}
+
+export class LodestarClient {
+ readonly baseUrl: string;
+ readonly timeoutMs: number;
+
+ constructor(options?: ClientOptions);
+
+ // System
+ getHealth(options?: RequestOptions): Promise;
+ getReadiness(options?: RequestOptions): Promise;
+
+ // Registry & Services
+ getStats(options?: RequestOptions): Promise;
+ getServices(params?: { category?: Category }, options?: RequestOptions): Promise;
+ getServiceById(id: number, options?: RequestOptions): Promise;
+ getServicesByProvider(address: string, options?: RequestOptions): Promise;
+ prepareRegisterService(data: PrepareRegisterRequest, options?: RequestOptions): Promise;
+ submitSignedRegistryTx(data: SubmitSignedRegistryTxRequest, options?: RequestOptions): Promise;
+ submitReputation(id: number, data: SubmitReputationRequest, options?: RequestOptions): Promise;
+
+ // Agents
+ getAgents(params?: { page?: number; pageSize?: number; sort?: AgentSortOption }, options?: RequestOptions): Promise;
+ getAgentStats(options?: RequestOptions): Promise;
+ getAgent(address: string, options?: RequestOptions): Promise;
+ registerAgent(data: RegisterAgentRequest, options?: RequestOptions): Promise;
+ getAgentEligibility(address: string, minScore: number, options?: RequestOptions): Promise;
+ checkAgentCanSpend(address: string, params: { amount?: string; category?: string; amount_stroops?: string }, options?: RequestOptions): Promise;
+ recordAgentPayment(address: string, data: RecordPaymentRequest, options?: RequestOptions): Promise;
+ buildAgentTx(address: string, data: BuildAgentTxRequest, callerAddress: string, options?: RequestOptions): Promise;
+ submitSignedAgentTx(address: string, data: SubmitSignedAgentTxRequest, options?: RequestOptions): Promise;
+
+ // Activity
+ getActivity(params?: { page?: number; limit?: number }, options?: RequestOptions): Promise;
+ getDemoActivity(options?: RequestOptions): Promise;
+}
+
+export function createClient(options?: ClientOptions): LodestarClient;
diff --git a/packages/client/index.js b/packages/client/index.js
new file mode 100644
index 00000000..630461c8
--- /dev/null
+++ b/packages/client/index.js
@@ -0,0 +1,245 @@
+/**
+ * @lodestar/client
+ * Typed OpenAPI client for the Lodestar backend (#832).
+ */
+
+export class LodestarApiError extends Error {
+ constructor(message, status, body, code, requestId) {
+ super(message);
+ this.name = 'LodestarApiError';
+ this.status = status;
+ this.body = body;
+ this.code = code || (body && typeof body === 'object' ? body.code : undefined);
+ this.requestId = requestId || (body && typeof body === 'object' ? body.requestId : undefined);
+ }
+}
+
+export class LodestarClient {
+ constructor(options = {}) {
+ this.baseUrl = (options.baseUrl || 'http://localhost:3001').replace(/\/+$/, '');
+ this.timeoutMs = options.timeoutMs ?? 60_000;
+ this._customFetch = options.fetch;
+ this.defaultHeaders = options.headers || {};
+ }
+
+ async _request(path, options = {}) {
+ const url = `${this.baseUrl}${path.startsWith('/') ? path : `/${path}`}`;
+ const timeoutMs = options.timeoutMs ?? this.timeoutMs;
+ const fetchFn = options.fetch || this._customFetch || globalThis.fetch;
+ const headers = {
+ 'Content-Type': 'application/json',
+ ...this.defaultHeaders,
+ ...(options.headers || {}),
+ };
+
+ let signal = options.signal;
+ let timeoutId;
+ if (!signal && typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
+ signal = AbortSignal.timeout(timeoutMs);
+ } else if (!signal && typeof AbortController !== 'undefined') {
+ const controller = new AbortController();
+ timeoutId = setTimeout(() => controller.abort(), timeoutMs);
+ signal = controller.signal;
+ }
+
+ try {
+ const res = await fetchFn(url, {
+ ...options,
+ headers,
+ signal,
+ });
+
+ if (timeoutId) clearTimeout(timeoutId);
+
+ let data = null;
+ if (typeof res.json === 'function') {
+ data = await res.json().catch(() => null);
+ } else if (typeof res.text === 'function') {
+ const text = await res.text().catch(() => '');
+ try {
+ data = JSON.parse(text);
+ } catch {
+ data = text;
+ }
+ }
+
+ if (!res.ok) {
+ const message =
+ (data && typeof data === 'object' && (data.error || data.message)) ||
+ `Request failed with status ${res.status}`;
+ const code = data && typeof data === 'object' ? data.code : undefined;
+ const requestId =
+ (data && typeof data === 'object' && data.requestId) ||
+ res.headers?.get?.('x-request-id') ||
+ undefined;
+
+ throw new LodestarApiError(message, res.status, data, code, requestId);
+ }
+
+ return data;
+ } catch (err) {
+ if (timeoutId) clearTimeout(timeoutId);
+ if (err instanceof LodestarApiError) throw err;
+ if (err.name === 'TimeoutError' || err.name === 'AbortError') {
+ throw new LodestarApiError(`Request timed out after ${timeoutMs}ms`, 408, null, 'TIMEOUT');
+ }
+ throw new LodestarApiError(err.message || 'Network request failed', 0, null, 'NETWORK_ERROR');
+ }
+ }
+
+ // ── System ──────────────────────────────────────────────────────────────────
+
+ async getHealth(options) {
+ return this._request('/healthz', { method: 'GET', ...options });
+ }
+
+ async getReadiness(options) {
+ return this._request('/readyz', { method: 'GET', ...options });
+ }
+
+ // ── Registry & Services ────────────────────────────────────────────────────
+
+ async getStats(options) {
+ return this._request('/api/stats', { method: 'GET', ...options });
+ }
+
+ async getServices(params = {}, options) {
+ const query = new URLSearchParams();
+ if (params.category && params.category !== 'all') {
+ query.set('category', params.category);
+ }
+ const qs = query.toString();
+ return this._request(`/api/services${qs ? `?${qs}` : ''}`, { method: 'GET', ...options });
+ }
+
+ async getServiceById(id, options) {
+ return this._request(`/api/services/${encodeURIComponent(id)}`, { method: 'GET', ...options });
+ }
+
+ async getServicesByProvider(address, options) {
+ return this._request(`/api/registry/by-provider/${encodeURIComponent(address)}`, {
+ method: 'GET',
+ ...options,
+ });
+ }
+
+ async prepareRegisterService(data, options) {
+ return this._request('/api/registry/prepare-register', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ async submitSignedRegistryTx(data, options) {
+ return this._request('/api/registry/submit-signed-tx', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ async submitReputation(id, data, options) {
+ return this._request(`/api/reputation/${encodeURIComponent(id)}`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ // ── Agents ──────────────────────────────────────────────────────────────────
+
+ async getAgents(params = {}, options) {
+ const query = new URLSearchParams();
+ if (params.page !== undefined) query.set('page', String(params.page));
+ if (params.pageSize !== undefined) query.set('pageSize', String(params.pageSize));
+ if (params.sort !== undefined) query.set('sort', String(params.sort));
+ const qs = query.toString();
+ return this._request(`/api/agents${qs ? `?${qs}` : ''}`, { method: 'GET', ...options });
+ }
+
+ async getAgentStats(options) {
+ return this._request('/api/agents/stats', { method: 'GET', ...options });
+ }
+
+ async getAgent(address, options) {
+ return this._request(`/api/agents/${encodeURIComponent(address)}`, {
+ method: 'GET',
+ ...options,
+ });
+ }
+
+ async registerAgent(data, options) {
+ return this._request('/api/agents/register', {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ async getAgentEligibility(address, minScore, options) {
+ return this._request(
+ `/api/agents/${encodeURIComponent(address)}/eligible?min_score=${encodeURIComponent(minScore)}`,
+ { method: 'GET', ...options }
+ );
+ }
+
+ async checkAgentCanSpend(address, params = {}, options) {
+ const query = new URLSearchParams();
+ if (params.amount !== undefined) query.set('amount', String(params.amount));
+ if (params.category !== undefined) query.set('category', String(params.category));
+ if (params.amount_stroops !== undefined) query.set('amount_stroops', String(params.amount_stroops));
+ const qs = query.toString();
+ return this._request(
+ `/api/agents/${encodeURIComponent(address)}/can-spend${qs ? `?${qs}` : ''}`,
+ { method: 'GET', ...options }
+ );
+ }
+
+ async recordAgentPayment(address, data, options) {
+ return this._request(`/api/agents/${encodeURIComponent(address)}/payment`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ async buildAgentTx(address, data, callerAddress, options = {}) {
+ const headers = {
+ ...(options.headers || {}),
+ 'x-caller-address': callerAddress,
+ };
+ return this._request(`/api/agents/${encodeURIComponent(address)}/build-tx`, {
+ method: 'POST',
+ headers,
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ async submitSignedAgentTx(address, data, options) {
+ return this._request(`/api/agents/${encodeURIComponent(address)}/submit-signed-tx`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ ...options,
+ });
+ }
+
+ // ── Activity ────────────────────────────────────────────────────────────────
+
+ async getActivity(params = {}, options) {
+ const query = new URLSearchParams();
+ if (params.page !== undefined) query.set('page', String(params.page));
+ if (params.limit !== undefined) query.set('limit', String(params.limit));
+ const qs = query.toString();
+ return this._request(`/api/activity${qs ? `?${qs}` : ''}`, { method: 'GET', ...options });
+ }
+
+ async getDemoActivity(options) {
+ return this._request('/demo/activity', { method: 'GET', ...options });
+ }
+}
+
+export function createClient(options) {
+ return new LodestarClient(options);
+}
diff --git a/packages/client/openapi.json b/packages/client/openapi.json
new file mode 100644
index 00000000..ebcde51a
--- /dev/null
+++ b/packages/client/openapi.json
@@ -0,0 +1,1011 @@
+{
+ "openapi": "3.0.3",
+ "info": {
+ "title": "Lodestar API",
+ "version": "1.0.0",
+ "description": "On-chain service discovery and credit scoring protocol for x402 AI agents on Stellar."
+ },
+ "servers": [
+ {
+ "url": "http://localhost:3001",
+ "description": "Local development server"
+ }
+ ],
+ "paths": {
+ "/healthz": {
+ "get": {
+ "summary": "Liveness check",
+ "description": "Returns process uptime, queue depth, and status.",
+ "responses": {
+ "200": {
+ "description": "Process is alive",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HealthResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/readyz": {
+ "get": {
+ "summary": "Readiness check",
+ "description": "Checks connectivity to Soroban RPC and Redis.",
+ "responses": {
+ "200": {
+ "description": "Backend is ready to serve traffic",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReadinessResponse"
+ }
+ }
+ }
+ },
+ "503": {
+ "description": "Backend dependency unreachable",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReadinessResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/stats": {
+ "get": {
+ "summary": "Get aggregate registry and agent stats",
+ "responses": {
+ "200": {
+ "description": "Aggregate statistics",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/StatsResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/services": {
+ "get": {
+ "summary": "List services",
+ "parameters": [
+ {
+ "name": "category",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "$ref": "#/components/schemas/Category"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List of active registered services",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ServicesResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/services/{id}": {
+ "get": {
+ "summary": "Get service by ID",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Service entry",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ServiceEntry"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Service not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/registry/by-provider/{address}": {
+ "get": {
+ "summary": "Get services registered by provider",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Services by provider",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ServicesResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/registry/prepare-register": {
+ "post": {
+ "summary": "Prepare an unsigned transaction to register a service",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PrepareRegisterRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Unsigned transaction XDR and submit token",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PrepareRegisterResponse"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Validation error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/registry/submit-signed-tx": {
+ "post": {
+ "summary": "Submit a wallet-signed service registration transaction",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SubmitSignedRegistryTxRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Transaction result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SubmitSignedRegistryTxResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/reputation/{id}": {
+ "post": {
+ "summary": "Submit a reputation vote for a service",
+ "parameters": [
+ {
+ "name": "id",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SubmitReputationRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Updated reputation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ReputationResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents": {
+ "get": {
+ "summary": "List agents with pagination",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 0
+ }
+ },
+ {
+ "name": "pageSize",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 12
+ }
+ },
+ {
+ "name": "sort",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string",
+ "enum": ["score", "payments", "registered_at", "newest"],
+ "default": "score"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Paginated agents list",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentsResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/stats": {
+ "get": {
+ "summary": "Get aggregate agent scoring statistics",
+ "responses": {
+ "200": {
+ "description": "Agent stats",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentStats"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/register": {
+ "post": {
+ "summary": "Register a new AI agent on-chain",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterAgentRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Agent registered successfully",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RegisterAgentResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}": {
+ "get": {
+ "summary": "Get agent profile and spending policy",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Agent profile",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentProfileResponse"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Agent not found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ErrorResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}/eligible": {
+ "get": {
+ "summary": "Check if agent meets minimum score for a service tier",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "min_score",
+ "in": "query",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Eligibility result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentEligibilityResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}/can-spend": {
+ "get": {
+ "summary": "Check spending policy limits for an upcoming transaction",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "amount",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "amount_stroops",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "category",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Spend check result",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/AgentSpendCheckResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}/payment": {
+ "post": {
+ "summary": "Record a payment result and update agent score",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RecordPaymentRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Payment recorded and score updated",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/RecordPaymentResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}/build-tx": {
+ "post": {
+ "summary": "Build an unsigned transaction XDR for agent policy management",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "x-caller-address",
+ "in": "header",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BuildAgentTxRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Unsigned transaction XDR",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/BuildAgentTxResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/agents/{address}/submit-signed-tx": {
+ "post": {
+ "summary": "Submit a wallet-signed agent transaction",
+ "parameters": [
+ {
+ "name": "address",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SubmitSignedAgentTxRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Transaction submitted",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/SubmitSignedAgentTxResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/api/activity": {
+ "get": {
+ "summary": "Get live activity events",
+ "parameters": [
+ {
+ "name": "page",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 1
+ }
+ },
+ {
+ "name": "limit",
+ "in": "query",
+ "required": false,
+ "schema": {
+ "type": "integer",
+ "default": 20
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Activity events",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ActivityResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/demo/activity": {
+ "get": {
+ "summary": "Get demo activity events",
+ "responses": {
+ "200": {
+ "description": "Demo activity feed",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/DemoActivityResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Category": {
+ "type": "string",
+ "enum": ["search", "weather", "finance", "ai", "data", "compute"]
+ },
+ "ServiceEntry": {
+ "type": "object",
+ "required": ["id", "name", "description", "endpoint", "price_usdc", "category", "provider", "reputation", "active"],
+ "properties": {
+ "id": { "type": "integer" },
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "endpoint": { "type": "string" },
+ "price_usdc": { "type": "string" },
+ "category": { "$ref": "#/components/schemas/Category" },
+ "provider": { "type": "string" },
+ "reputation": { "type": "integer" },
+ "active": { "type": "boolean" },
+ "registered_at": { "type": "integer", "nullable": true }
+ }
+ },
+ "ServicesResponse": {
+ "type": "object",
+ "required": ["services"],
+ "properties": {
+ "services": {
+ "type": "array",
+ "items": { "$ref": "#/components/schemas/ServiceEntry" }
+ },
+ "total": { "type": "integer" }
+ }
+ },
+ "StatsResponse": {
+ "type": "object",
+ "required": ["total_services", "total_categories", "active_services", "top_category"],
+ "properties": {
+ "total_services": { "type": "integer" },
+ "total_categories": { "type": "integer" },
+ "active_services": { "type": "integer" },
+ "top_category": { "type": "string" },
+ "total_agents": { "type": "integer" },
+ "total_volume_stroops": { "type": "string" },
+ "total_volume_usdc": { "type": "string" },
+ "average_score": { "type": "integer" }
+ }
+ },
+ "PrepareRegisterRequest": {
+ "type": "object",
+ "required": ["name", "description", "endpoint", "priceUsdc", "category", "providerAddress"],
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "endpoint": { "type": "string" },
+ "priceUsdc": { "type": "string" },
+ "category": { "$ref": "#/components/schemas/Category" },
+ "providerAddress": { "type": "string" }
+ }
+ },
+ "PrepareRegisterResponse": {
+ "type": "object",
+ "required": ["xdr", "submitToken"],
+ "properties": {
+ "xdr": { "type": "string" },
+ "submitToken": { "type": "string" }
+ }
+ },
+ "SubmitSignedRegistryTxRequest": {
+ "type": "object",
+ "required": ["signedXdr", "submitToken"],
+ "properties": {
+ "signedXdr": { "type": "string" },
+ "submitToken": { "type": "string" }
+ }
+ },
+ "SubmitSignedRegistryTxResponse": {
+ "type": "object",
+ "required": ["success", "hash"],
+ "properties": {
+ "success": { "type": "boolean" },
+ "hash": { "type": "string" },
+ "id": { "type": "integer", "nullable": true }
+ }
+ },
+ "SubmitReputationRequest": {
+ "type": "object",
+ "required": ["positive", "agent"],
+ "properties": {
+ "positive": { "type": "boolean" },
+ "agent": { "type": "string" }
+ }
+ },
+ "ReputationResponse": {
+ "type": "object",
+ "required": ["newReputation"],
+ "properties": {
+ "newReputation": { "type": "integer" },
+ "txHash": { "type": "string", "nullable": true }
+ }
+ },
+ "AgentEntry": {
+ "type": "object",
+ "required": [
+ "address",
+ "name",
+ "description",
+ "score",
+ "total_payments",
+ "successful_payments",
+ "failed_payments",
+ "total_volume_stroops",
+ "active",
+ "flagged"
+ ],
+ "properties": {
+ "address": { "type": "string" },
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "endpoint": { "type": "string" },
+ "score": { "type": "integer" },
+ "total_payments": { "type": "string" },
+ "successful_payments": { "type": "string" },
+ "failed_payments": { "type": "string" },
+ "total_volume_stroops": { "type": "string" },
+ "active": { "type": "boolean" },
+ "flagged": { "type": "boolean" },
+ "registered_at": { "type": "integer", "nullable": true },
+ "owner": { "type": "string" }
+ }
+ },
+ "SpendingPolicy": {
+ "type": "object",
+ "required": ["max_per_tx_stroops", "max_per_day_stroops", "spent_today_stroops", "last_spend_day", "allowed_categories", "min_score_to_earn"],
+ "properties": {
+ "max_per_tx_stroops": { "type": "string" },
+ "max_per_day_stroops": { "type": "string" },
+ "spent_today_stroops": { "type": "string" },
+ "last_spend_day": { "type": "integer" },
+ "allowed_categories": {
+ "type": "array",
+ "items": { "type": "string" }
+ },
+ "min_score_to_earn": { "type": "integer" }
+ }
+ },
+ "AgentsResponse": {
+ "type": "object",
+ "required": ["agents", "total", "page", "pageSize", "totalPages"],
+ "properties": {
+ "agents": {
+ "type": "array",
+ "items": { "$ref": "#/components/schemas/AgentEntry" }
+ },
+ "total": { "type": "integer" },
+ "page": { "type": "integer" },
+ "pageSize": { "type": "integer" },
+ "totalPages": { "type": "integer" }
+ }
+ },
+ "AgentStats": {
+ "type": "object",
+ "required": ["total_agents", "active_agents", "flagged_agents", "average_score", "total_volume_stroops", "tier_distribution"],
+ "properties": {
+ "total_agents": { "type": "integer" },
+ "active_agents": { "type": "integer" },
+ "flagged_agents": { "type": "integer" },
+ "average_score": { "type": "integer" },
+ "total_volume_stroops": { "type": "string" },
+ "tier_distribution": {
+ "type": "object",
+ "properties": {
+ "unrated": { "type": "integer" },
+ "bronze": { "type": "integer" },
+ "silver": { "type": "integer" },
+ "gold": { "type": "integer" },
+ "platinum": { "type": "integer" }
+ }
+ }
+ }
+ },
+ "AgentProfileResponse": {
+ "type": "object",
+ "required": ["agent"],
+ "properties": {
+ "agent": { "$ref": "#/components/schemas/AgentEntry" },
+ "policy": {
+ "allOf": [{ "$ref": "#/components/schemas/SpendingPolicy" }],
+ "nullable": true
+ }
+ }
+ },
+ "RegisterAgentRequest": {
+ "type": "object",
+ "required": ["name", "description", "address"],
+ "properties": {
+ "name": { "type": "string" },
+ "description": { "type": "string" },
+ "address": { "type": "string" },
+ "endpoint": { "type": "string" }
+ }
+ },
+ "RegisterAgentResponse": {
+ "type": "object",
+ "required": ["agent"],
+ "properties": {
+ "txHash": { "type": "string", "nullable": true },
+ "agent": { "$ref": "#/components/schemas/AgentEntry" }
+ }
+ },
+ "AgentEligibilityResponse": {
+ "type": "object",
+ "required": ["eligible", "score", "minScore"],
+ "properties": {
+ "eligible": { "type": "boolean" },
+ "score": { "type": "integer" },
+ "minScore": { "type": "integer" }
+ }
+ },
+ "AgentSpendCheckResponse": {
+ "type": "object",
+ "required": ["allowed"],
+ "properties": {
+ "allowed": { "type": "boolean" },
+ "reason": { "type": "string", "nullable": true },
+ "currentScore": { "type": "integer", "nullable": true },
+ "dailySpent": { "type": "string", "nullable": true },
+ "dailyLimit": { "type": "string", "nullable": true }
+ }
+ },
+ "RecordPaymentRequest": {
+ "type": "object",
+ "required": ["success", "stroops"],
+ "properties": {
+ "success": { "type": "boolean" },
+ "stroops": { "type": "string" },
+ "txHash": { "type": "string" }
+ }
+ },
+ "RecordPaymentResponse": {
+ "type": "object",
+ "required": ["newScore"],
+ "properties": {
+ "newScore": { "type": "integer" },
+ "txHash": { "type": "string", "nullable": true }
+ }
+ },
+ "BuildAgentTxRequest": {
+ "type": "object",
+ "required": ["action"],
+ "properties": {
+ "action": { "type": "string" }
+ },
+ "additionalProperties": true
+ },
+ "BuildAgentTxResponse": {
+ "type": "object",
+ "required": ["xdr"],
+ "properties": {
+ "xdr": { "type": "string" }
+ }
+ },
+ "SubmitSignedAgentTxRequest": {
+ "type": "object",
+ "required": ["signedXdr"],
+ "properties": {
+ "signedXdr": { "type": "string" }
+ }
+ },
+ "SubmitSignedAgentTxResponse": {
+ "type": "object",
+ "required": ["txHash"],
+ "properties": {
+ "txHash": { "type": "string" }
+ }
+ },
+ "ActivityEvent": {
+ "type": "object",
+ "required": ["id", "type", "timestamp"],
+ "properties": {
+ "id": { "type": "string" },
+ "type": { "type": "string" },
+ "timestamp": { "type": "string" },
+ "data": { "type": "object" }
+ }
+ },
+ "ActivityResponse": {
+ "type": "object",
+ "required": ["events"],
+ "properties": {
+ "events": {
+ "type": "array",
+ "items": { "$ref": "#/components/schemas/ActivityEvent" }
+ },
+ "pagination": { "type": "object" }
+ }
+ },
+ "DemoActivityEntry": {
+ "type": "object",
+ "required": ["id", "type", "timestamp", "summary"],
+ "properties": {
+ "id": { "type": "string" },
+ "type": { "type": "string" },
+ "timestamp": { "type": "string" },
+ "summary": { "type": "string" },
+ "agent": { "type": "string" },
+ "service": { "type": "string" },
+ "amount": { "type": "string" },
+ "txHash": { "type": "string" }
+ }
+ },
+ "DemoActivityResponse": {
+ "type": "object",
+ "required": ["activity"],
+ "properties": {
+ "activity": {
+ "type": "array",
+ "items": { "$ref": "#/components/schemas/DemoActivityEntry" }
+ }
+ }
+ },
+ "HealthResponse": {
+ "type": "object",
+ "required": ["status", "uptimeSeconds", "timestamp"],
+ "properties": {
+ "status": { "type": "string" },
+ "uptimeSeconds": { "type": "integer" },
+ "queueDepth": { "type": "integer" },
+ "pendingTransactions": { "type": "integer" },
+ "timestamp": { "type": "string" }
+ }
+ },
+ "ReadinessResponse": {
+ "type": "object",
+ "required": ["ready", "status", "timestamp"],
+ "properties": {
+ "ready": { "type": "boolean" },
+ "status": { "type": "string" },
+ "rpc": { "type": "object" },
+ "redis": { "type": "object" },
+ "timestamp": { "type": "string" }
+ }
+ },
+ "ErrorResponse": {
+ "type": "object",
+ "required": ["error"],
+ "properties": {
+ "error": { "type": "string" },
+ "code": { "type": "string" },
+ "requestId": { "type": "string" }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/client/package.json b/packages/client/package.json
new file mode 100644
index 00000000..2a1db5d8
--- /dev/null
+++ b/packages/client/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@lodestar/client",
+ "version": "1.0.0",
+ "description": "Typed OpenAPI client for the Lodestar API, consumed by the frontend, agent, and external integrators.",
+ "type": "module",
+ "main": "index.js",
+ "types": "index.d.ts",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "import": "./index.js",
+ "default": "./index.js"
+ },
+ "./openapi.json": "./openapi.json"
+ },
+ "files": [
+ "index.js",
+ "index.d.ts",
+ "openapi.json",
+ "README.md"
+ ],
+ "license": "Apache-2.0"
+}