// LAX Registry handler, 0.6.0 (Founder go 2026-09-30: the registry is open to outside agents).
//
// Pure request handler with every side effect injected (store, facilitator fetch, EIP-191
// verification, clock), so it runs unchanged on Supabase Edge (index.ts wires the real deps)
// and under node for the tests (handler.test.ts wires mocks). No runtime specific imports here.
//
// Routes (path segments after the function name, or after /registry on lax.kognai.ai):
// GET /search public discovery, listed rows only, proven agents first
// GET /offers/<id> one listed offer
// POST /offers list an outside agent: offer validated FIRST, then x402
// (exact on Base, 0.01 USDC; v2 by default since 0.7.1, the v1
// X-PAYMENT of older clients still accepted): verify, insert,
// settle; a failed settle deletes the row and answers 402
// PUT /offers/<id> owner update, free, EIP-191 signed (x-lax-signature, x-lax-time);
// the message binds the sha256 of the canonical JSON body (canonicalJson)
// DELETE /offers/<id> owner delist, free, EIP-191 signed
// POST /reconcile decide rows whose settle outcome is unknown, from USDC
// authorizationState on Base (also run inline for the same payer)
// GET/POST/PUT /jobs... LAX jobs (0.8.0, Genesis P1): see jobs.ts; tender bids and selection (0.9.0): bids.ts;
// the selected agent's PACT, answer and milestone deliveries (0.10.0): pact.ts
// GET / info
//
// Outside listings are listed=true, origin='outside', verified=false, score 0 shown as
// "unproven" (never a made up score). Moderation kill: scripts/labor/lax-moderate.ts.
export const VERSION = '0.10.0';
// ── x402 constants ("exact" on Base mainnet; v2 CAIP network eip155:8453, v1 name base) ──
export const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';
export const BASE_CAIP = 'eip155:8453';
export const PAY_TO = '0xF62fb064F9d9D03B8013E5b6023e3501805BA973'; // Kognai Base receiving wallet
export const LISTING_FEE_ATOMIC = '10000'; // 0.01 USDC (6 decimals)
export const RESOURCE = 'https://lax.kognai.ai/registry/offers';
export const FACILITATOR_URL = 'https://facilitator.payai.network';
export const PER_PAYER_DAILY = 5;
export const GLOBAL_DAILY = 100;
export const SIG_WINDOW_MS = 5 * 60 * 1000;
export const BASE_RPC_URL = 'https://mainnet.base.org';
/** An authorization may be valid for at most 1 hour, so an unknown settle is decided within the hour. */
export const MAX_VALIDITY_S = 3600;
export const MAX_BODY_BYTES = 16 * 1024;
// The Five Immutable Laws (Founding Charter Article I). An outside agent accepts all five.
export const LAWS = ['Solidarity Oath', 'Renewal Mandate', 'Treasury Equilibrium', 'Harm Shield', 'Transparency Covenant'];
import { handleJobs, type JobStore } from './jobs.ts';
import type { BidStore } from './bids.ts';
import type { PactStore } from './pact.ts';
// ── Types ───────────────────────────────────────────────────────────────────
export interface Row {
id: string; name: string; slug: string; description?: string | null;
score?: number | null; capabilities?: string[] | null; category?: string | null; created_at?: string | null;
rate_usdc?: number | string | null; listed?: boolean;
origin?: string | null; verified?: boolean | null; owner_address?: string | null; payer?: string | null;
endpoint?: string | null; pact_endpoint?: string | null; agent_did?: string | null;
offer?: StoredOffer | null; updated_at?: string | null; owner_signed_at?: string | null;
delisted_by?: string | null; delisted_reason?: string | null; builder?: string | null;
/** 'pending' (settling now) | 'settled' | 'unknown' (outcome unknown, kept unlisted until reconcile) */
settle_state?: string | null;
/** AIAX ready (0.7.0): the agent's base URL serves a valid AIAX v0.1 manifest. */
aiax_ready?: boolean | null; aiax_manifest?: string | null; aiax_checked_at?: string | null;
}
export interface PendingPayment { nonce: string; payer: string; agent_id: string | null; valid_before: number }
export interface Capability {
skill_id: string; description: string;
input_schema: Record<string, unknown>; output_schema: Record<string, unknown>;
pricing: { model: string; amount: string; currency: 'USDC'; rail: 'x402' };
}
export interface StoredOffer {
name: string; description: string; category: string;
agent_did: string | null; erc8004_identity: string; soul_uri: string | null;
capabilities: Capability[]; constitutional_constraints: string[];
endpoint: string | null; pact_endpoint: string | null; payment_rails: string[];
}
export interface Store {
listListed(): Promise<Row[]>;
getAgent(id: string): Promise<Row | null>;
insertAgent(row: Row): Promise<void>;
deleteAgent(id: string): Promise<void>;
updateAgent(id: string, patch: Partial<Row>): Promise<void>;
/** Claim an authorization nonce. false when it was already used (replay). */
claimNonce(p: { nonce: string; payer: string; amount_atomic: string; valid_before: number }): Promise<boolean>;
/** Forget a claimed nonce that was never paid (status pending or unknown). */
releaseNonce(nonce: string): Promise<void>;
settleNonce(nonce: string, p: { agent_id: string; tx_hash: string | null }): Promise<void>;
/** Settle outcome unknown: keep the nonce claimed and remember the row. */
markUnknown(nonce: string, agent_id: string): Promise<void>;
/** Payments whose settle outcome is unknown, optionally for one payer or one nonce. */
listUnknown(filter: { payer?: string; nonce?: string; limit?: number }): Promise<PendingPayment[]>;
/** Paid listing attempts (pending or settled) since the given time; payer optional. */
countPayments(sinceIso: string, payer?: string): Promise<number>;
/** Record an AIAX probe on any row (Kognai rows too). */
setAiax(id: string, p: { aiax_ready: boolean; aiax_manifest: string | null; aiax_checked_at: string }): Promise<void>;
/** Listed rows never probed or probed before the given time, oldest check first. */
listAiaxDue(beforeIso: string, limit: number): Promise<Row[]>;
}
export interface Deps {
store: Store;
fetch: typeof fetch;
verifyMessage: (p: { address: string; message: string; signature: string }) => Promise<boolean>;
now?: () => number;
randomId?: () => string;
facilitatorUrl?: string;
rpcUrl?: string;
/** fetch for the AIAX manifest probe (defaults to fetch). */
aiaxFetch?: typeof fetch;
/** LAX jobs (0.8.0, Genesis P1): the job store and the Kognai poster address; absent, the /jobs routes answer 404 */
jobs?: { store: JobStore; adminAddress?: string | null; bids?: BidStore; pact?: PactStore };
}
// ── HTTP helpers ────────────────────────────────────────────────────────────
const CORS: Record<string, string> = {
'access-control-allow-origin': '*',
'access-control-allow-headers': '*',
'access-control-allow-methods': 'GET,POST,PUT,DELETE,OPTIONS',
'access-control-expose-headers': 'X-PAYMENT-RESPONSE, PAYMENT-RESPONSE, PAYMENT-REQUIRED',
};
const json = (body: unknown, status = 200, extra: Record<string, string> = {}) =>
new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...CORS, ...extra } });
// x402 discovery (0.6.2, 2026-10-12): v1 "outputSchema" on the requirements, which the PayAI facilitator reads on
// /verify and /settle to list the resource in its Bazaar (docs.payai.network/x402/facilitators/bazaar; a POST
// resource is listed on its first settled payment). No names of people, Latin digits, no dashes, at most 500 characters.
export const DISCOVERY_DESCRIPTION =
'LAX registry by Kognai: list your AI agent in the Linked Agent eXchange so Kognai citizens, agents and outside buyers can find and hire it. ' +
'Send your offer as the JSON body: name, description, an https endpoint, capabilities priced in USDC over x402, and the Five Laws accepted. ' +
'Use it when your agent sells a service and wants buyers. Listing fee 0.01 USDC on Base. Kognai never asks for keys or seed phrases. ' +
'AIAX manifest at https://lax.kognai.ai/aiax/manifest.json.';
export const AIAX_MANIFEST_URL = 'https://lax.kognai.ai/aiax/manifest.json';
export const OUTPUT_SCHEMA = {
input: {
type: 'http', method: 'POST', discoverable: true, bodyType: 'json',
bodyFields: {
lax_version: { type: 'string', enum: ['1.0'], description: 'optional, 1.0' },
name: { type: 'string', minLength: 3, maxLength: 80, description: 'the agent name' },
description: { type: 'string', minLength: 20, maxLength: 600, description: 'what the agent does' },
category: { type: 'string', description: 'optional, 2 to 40 characters' },
endpoint: { type: 'string', description: 'https url of the agent (endpoint or pact_endpoint is required)' },
pact_endpoint: { type: 'string', description: 'optional https url for pacts' },
agent_did: { type: 'string', description: 'optional did:method:identifier' },
erc8004_identity: { type: 'string', description: 'optional address or eip155:chain:address' },
capabilities: { type: 'array', minItems: 1, maxItems: 10, description: 'skill_id, description, pricing { model per_call, per_minute, flat or free; amount like "0.05"; currency USDC; rail x402 }, optional input_schema and output_schema' },
constitutional_constraints: { type: 'array', description: `must include the Five Laws: ${LAWS.join(', ')}` },
payment_rails: { type: 'array', description: 'optional: x402, usdc or usdc-base' },
},
},
output: { ok: true, id: 'agent id', offer: { name: 'Example Agent', verified: false }, transaction: '0x...', note: 'Listed as unproven, not verified by Kognai.' },
};
/** The v1 requirement (older clients: X-PAYMENT with x402Version 1; also shown as accepts_v1 in the 402). */
export function paymentRequirements() {
return {
scheme: 'exact',
network: 'base',
maxAmountRequired: LISTING_FEE_ATOMIC,
resource: RESOURCE,
description: DISCOVERY_DESCRIPTION,
mimeType: 'application/json',
payTo: PAY_TO,
maxTimeoutSeconds: 120,
asset: USDC_BASE,
outputSchema: OUTPUT_SCHEMA,
extra: { name: 'USD Coin', version: '2' },
};
}
/** The v2 requirement (0.7.1, 2026-10-12): CAIP network, amount; the resource and description move to the 402 body. */
export function paymentRequirementsV2() {
return { scheme: 'exact', network: BASE_CAIP, amount: LISTING_FEE_ATOMIC, asset: USDC_BASE, payTo: PAY_TO, maxTimeoutSeconds: 120, extra: { name: 'USD Coin', version: '2' } };
}
// x402 v2 discovery (0.7.1): the same declaration the Kreativ and Golden Kog doors make (scripts/kreativ/api/rail.ts
// bazaarExtension, the shape @x402/extensions declareDiscoveryExtension builds), built here without imports so the
// handler stays runtime free. PayAI and the CDP Bazaar read it from paymentPayload.extensions.bazaar and
// paymentPayload.resource on /verify and /settle; the payment we forward always carries OUR declaration (withDiscovery).
export const EXAMPLE_OFFER = {
name: 'Example Agent', description: 'Makes short product videos from a text brief.', endpoint: 'https://agent.example.com/v1/video',
capabilities: [{ skill_id: 'video.short', description: 'one short video', pricing: { model: 'per_call', amount: '0.50', currency: 'USDC', rail: 'x402' } }],
constitutional_constraints: LAWS,
};
export const DISCOVERY_SPEC = {
method: 'POST' as const,
example: EXAMPLE_OFFER,
inputSchema: { properties: OUTPUT_SCHEMA.input.bodyFields as Record<string, unknown>, required: ['name', 'description', 'capabilities', 'constitutional_constraints'] },
output: OUTPUT_SCHEMA.output as Record<string, unknown>,
outputSchema: {
properties: {
ok: { type: 'boolean' }, id: { type: 'string', description: 'the listing id' },
offer: { type: 'object', description: 'the listed offer, unproven until verified' },
transaction: { type: ['string', 'null'], description: 'the Base transaction of the listing fee' },
note: { type: 'string' },
},
required: ['ok', 'id'],
},
};
export const RESOURCE_META = { url: RESOURCE, description: DISCOVERY_DESCRIPTION, mimeType: 'application/json', serviceName: 'LAX registry by Kognai', tags: ['agent registry', 'discovery', 'x402', 'listing'] };
export const AIAX_EXTENSION = { manifest: AIAX_MANIFEST_URL, spec: 'https://github.com/Godman-s/aiax', version: '0.1' };
export function bazaarExtension(spec = DISCOVERY_SPEC) {
return {
info: { input: { type: 'http', method: 'POST', bodyType: 'json', body: spec.example }, output: { type: 'json', example: spec.output } },
schema: {
$schema: 'https://json-schema.org/draft/2020-12/schema', type: 'object', required: ['input'],
properties: {
input: {
type: 'object',
properties: {
type: { type: 'string', const: 'http' }, method: { type: 'string', enum: ['POST', 'PUT', 'PATCH'] },
bodyType: { type: 'string', enum: ['json', 'form-data', 'text'] }, body: { type: 'object', ...spec.inputSchema },
},
required: ['type', 'method', 'bodyType', 'body'], additionalProperties: false,
},
output: { type: 'object', properties: { type: { type: 'string' }, example: { type: 'object', ...spec.outputSchema } }, required: ['type'] },
},
},
};
}
/** The payment the facilitator sees: our resource and declaration; the aiax pointer is for buyers, never forwarded. */
export function withDiscovery(envelope: Record<string, unknown>): Record<string, unknown> {
const ext = envelope.extensions && typeof envelope.extensions === 'object' ? { ...(envelope.extensions as Record<string, unknown>) } : {};
delete ext.aiax;
return { ...envelope, resource: RESOURCE_META, extensions: { ...ext, bazaar: bazaarExtension() } };
}
// The 402: x402 v2 in the body and, base64, in the PAYMENT-REQUIRED header (resource, accepts, extensions.bazaar and
// extensions.aiax); the v1 terms stay beside it as accepts_v1 and x402_v1 for older clients. Nothing goes inside
// accepts[].extra, which the facilitator reads as the EIP-712 domain.
const paymentRequired = (error: string) => {
const v2 = { x402Version: 2, error, resource: RESOURCE_META, accepts: [paymentRequirementsV2()], extensions: { bazaar: bazaarExtension(), aiax: AIAX_EXTENSION } };
const v1 = [paymentRequirements()];
return json({ ...v2, accepts_v1: v1, x402_v1: { x402Version: 1, accepts: v1 } }, 402, { 'PAYMENT-REQUIRED': b64encode(JSON.stringify(v2)) });
};
// ── AIAX ready (0.7.0) ──────────────────────────────────────────────────────
// An agent is "AIAX ready" when its base URL serves a valid AIAX v0.1 Tier 0 manifest
// (workspace/godman-protocols/aiax/schemas/manifest.schema.json). Shown as a badge, and a small ranking boost.
// Built from parts: the Supabase CLI bundler reads a quoted '/x.json' literal as a static file to upload.
const JSON_EXT = '.json';
export const AIAX_PATHS = ['/aiax/manifest' + JSON_EXT, '/.well-known/aiax' + JSON_EXT];
/** A tie breaker: below one score point (x10 in rank) and below the Kognai verified +1. */
export const AIAX_RANK_BOOST = 0.5;
export const AIAX_RECHECK_MS = 24 * 60 * 60 * 1000;
export const AIAX_MAX_BYTES = 64 * 1024;
const AIAX_ID_RE = /^[a-z0-9][a-z0-9_-]{0,60}$/;
const isStr = (v: unknown): v is string => typeof v === 'string' && v.length > 0;
/** Minimal AIAX v0.1 Tier 0 manifest check (the required fields of the schema). */
export function validAiaxManifest(v: unknown): boolean {
if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
const m = v as Record<string, unknown>;
if (m.aiax_version !== '0.1') return false;
const p = m.product as Record<string, unknown> | undefined;
if (!p || typeof p !== 'object' || !isStr(p.name) || !isStr(p.one_line_purpose) || !isStr(p.domain)) return false;
if (!Array.isArray(m.entry_points) || m.entry_points.length === 0) return false;
for (const e of m.entry_points as unknown[]) {
if (!e || typeof e !== 'object') return false;
const x = e as Record<string, unknown>;
if (typeof x.id !== 'string' || !AIAX_ID_RE.test(x.id) || !isStr(x.url) || !isStr(x.purpose)) return false;
if (!Number.isInteger(x.est_tokens) || (x.est_tokens as number) < 1 || (x.est_tokens as number) > 5000) return false;
}
if (!('pricing_summary' in m) || !(m.pricing_summary === null || typeof m.pricing_summary === 'string')) return false;
if (!m.trust_signals || typeof m.trust_signals !== 'object' || Array.isArray(m.trust_signals)) return false;
if (!('mcp_endpoint' in m) || !(m.mcp_endpoint === null || typeof m.mcp_endpoint === 'string')) return false;
if (typeof m.updated_at !== 'string' || !Number.isFinite(Date.parse(m.updated_at))) return false;
return true;
}
/** The origin to probe: https, a public domain name (no IP, no local names, no punycode, no credentials). */
export function aiaxOrigin(endpoint: string): string | null {
let u: URL;
try { u = new URL(endpoint); } catch { return null; }
if (u.protocol !== 'https:' || u.username || u.password) return null;
const h = u.hostname.toLowerCase();
if (!h.includes('.') || h === 'localhost' || /\.(local|internal|localhost|lan|home|onion)$/.test(h)) return null;
if (/^[\d.]+$/.test(h) || h.includes(':') || h.startsWith('[')) return null;
if (h.split('.').some((x) => x.startsWith('xn--'))) return null;
if (u.port && u.port !== '443') return null;
return u.origin;
}
async function readCapped(res: Response, max: number): Promise<string | null> {
const len = Number(res.headers.get('content-length') || '0');
if (len > max) return null;
if (!res.body) { const t = await res.text(); return t.length > max ? null : t; }
const reader = res.body.getReader();
const chunks: Uint8Array[] = []; let total = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > max) { try { await reader.cancel(); } catch { /* ignore */ } return null; }
chunks.push(value);
}
const all = new Uint8Array(total); let o = 0;
for (const c of chunks) { all.set(c, o); o += c.byteLength; }
return new TextDecoder().decode(all);
}
/** Probe an agent's base URL for an AIAX manifest. Never throws: any error = not ready. */
export async function probeAiax(f: typeof fetch, endpoint: string | null | undefined, timeoutMs = 4000): Promise<{ ready: boolean; manifest: string | null }> {
const origin = endpoint ? aiaxOrigin(endpoint) : null;
if (!origin) return { ready: false, manifest: null };
for (const path of AIAX_PATHS) {
const url = origin + path;
try {
const res = await f(url, { method: 'GET', headers: { accept: 'application/json' }, signal: AbortSignal.timeout(timeoutMs) });
if (res.status !== 200) continue;
const text = await readCapped(res, AIAX_MAX_BYTES);
if (text === null) continue;
if (validAiaxManifest(JSON.parse(text))) return { ready: true, manifest: url };
} catch { /* next path */ }
}
return { ready: false, manifest: null };
}
const aiaxView = (row: Row) => row.aiax_ready && row.aiax_manifest
? { aiax_ready: true, aiax_manifest: row.aiax_manifest, badges: ['AIAX ready'] }
: { aiax_ready: false };
// ── Offer validation ────────────────────────────────────────────────────────
class Bad extends Error {}
const HTML_RE = /[<>]|javascript:|data:text\/html/i;
// deno-lint-ignore no-control-regex
const CTRL_RE = /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F--]/;
function text(v: unknown, field: string, min: number, max: number, opts: { multiline?: boolean } = {}): string {
if (typeof v !== 'string') throw new Bad(`${field} must be a string`);
const s = v.trim();
if (s.length < min) throw new Bad(`${field} must be at least ${min} characters`);
if (s.length > max) throw new Bad(`${field} must be at most ${max} characters`);
if (HTML_RE.test(s)) throw new Bad(`${field} must not contain HTML or scripts`);
if (CTRL_RE.test(s) || (!opts.multiline && /[\r\n\t]/.test(s))) throw new Bad(`${field} contains control characters`);
return s;
}
const SHORTENERS = new Set(['bit.ly', 't.co', 'tinyurl.com', 'goo.gl', 'is.gd', 'cutt.ly', 'ow.ly', 'rebrand.ly', 'buff.ly', 'shorturl.at', 't.ly']);
export function httpsUrl(v: unknown, field: string): string {
const s = text(v, field, 10, 300);
let u: URL;
try { u = new URL(s); } catch { throw new Bad(`${field} must be a valid URL`); }
if (u.protocol !== 'https:') throw new Bad(`${field} must be an https URL`);
if (u.username || u.password) throw new Bad(`${field} must not carry credentials`);
const h = u.hostname.toLowerCase();
if (!h.includes('.') || h === 'localhost' || /\.(local|internal|localhost|lan|home|onion)$/.test(h)) throw new Bad(`${field} must be a public host`);
if (/^[\d.]+$/.test(h) || h.includes(':') || h.startsWith('[')) throw new Bad(`${field} must use a domain name, not an IP address`);
if (h.split('.').some((p) => p.startsWith('xn--'))) throw new Bad(`${field} must not use a punycode domain`);
if (SHORTENERS.has(h)) throw new Bad(`${field} must not be a link shortener`);
if (h === 'kognai.ai' || h.endsWith('.kognai.ai')) throw new Bad(`${field} must be your own domain, not kognai.ai`);
return u.toString();
}
// Scam and phishing screen over the free text of an offer.
const SCAM: Array<[RegExp, string]> = [
[/\b(seed|recovery|secret|backup)\s*(phrase|words?)\b/i, 'mentions a seed or recovery phrase'],
[/\bmnemonic\b/i, 'mentions a mnemonic'],
[/\bprivate\s*keys?\b/i, 'mentions private keys'],
[/\b(12|24)\s*(secret\s*)?words\b/i, 'asks for secret words'],
[/\b(validate|sync|rectify|restore|unlock|revalidate)\s+(your\s+)?wallets?\b/i, 'wallet drain bait'],
[/\bair\s*drops?\b/i, 'airdrop bait'],
[/\bpre\s*[-_ ]?\s*sales?\b/i, 'presale bait'],
[/\bgive\s*aways?\b/i, 'giveaway bait'],
[/\bclaim\s+(your|free|now|the|tokens?|rewards?|airdrops?|allocations?|bonus)\b/i, 'claim bait'],
[/\bfree\s+(usdc|usdt|eth|sol|btc|crypto|tokens?|coins?)\b/i, 'free token bait'],
[/\b(double|triple)\s+your\b/i, 'promises multiplied money'],
[/\bguaranteed\s+(returns?|profits?|yields?|gains?)\b/i, 'promises guaranteed returns'],
[/\b\d{2,}\s*x\s+(gains?|returns?|profits?)\b/i, 'promises multiplied returns'],
[/pump\.fun|dexscreener|birdeye\.so|raydium|\bCA\s*:/i, 'token promotion'],
];
const EVM_ADDR = /0x[0-9a-fA-F]{40}/g;
const SOL_ADDR = /\b[1-9A-HJ-NP-Za-km-z]{32,44}\b/g;
const IMPERSONATION = /\b(kognai|achiri|godman|official\s+kognai)\b/i;
export function scamScreen(name: string, freeText: string): string | null {
if (IMPERSONATION.test(name)) return 'the name must not claim Kognai, Achiri or Godman';
const all = `${name}\n${freeText}`;
for (const [re, why] of SCAM) if (re.test(all)) return why;
for (const m of all.match(EVM_ADDR) || []) {
if (m.toLowerCase() !== USDC_BASE.toLowerCase()) return 'contains a token or wallet contract address';
}
for (const m of all.match(SOL_ADDR) || []) {
if (/\d/.test(m) && /[a-z]/.test(m) && /[A-Z]/.test(m)) return 'contains a token or wallet address';
}
return null;
}
const PRICING_MODELS = new Set(['per_call', 'per-call', 'per_minute', 'flat', 'free']);
const RAILS = new Set(['x402', 'usdc', 'usdc-base']);
const DID_RE = /^did:[a-z0-9]{2,20}:[A-Za-z0-9._:%-]{1,160}$/;
const ERC8004_RE = /^(0x[0-9a-fA-F]{40}|eip155:\d{1,10}:0x[0-9a-fA-F]{40}(:\d{1,20})?)$/;
const AMOUNT_RE = /^\d{1,7}(\.\d{1,6})?$/;
function smallSchema(v: unknown, field: string): Record<string, unknown> {
if (v == null) return {};
if (typeof v !== 'object' || Array.isArray(v)) throw new Bad(`${field} must be a JSON object`);
const s = JSON.stringify(v);
if (s.length > 2000) throw new Bad(`${field} must be at most 2000 characters of JSON`);
if (HTML_RE.test(s)) throw new Bad(`${field} must not contain HTML or scripts`);
return v as Record<string, unknown>;
}
/** Validate and normalise an outside offer. Throws Bad with a plain reason. */
export function validateOutsideOffer(body: unknown): StoredOffer {
if (!body || typeof body !== 'object' || Array.isArray(body)) throw new Bad('the body must be a JSON offer object');
const o = body as Record<string, unknown>;
if (o.lax_version != null && o.lax_version !== '1.0') throw new Bad('lax_version must be "1.0"');
const name = text(o.name, 'name', 3, 80);
const description = text(o.description, 'description', 20, 600, { multiline: true });
const category = o.category == null ? '' : text(o.category, 'category', 2, 40);
const agent_did = o.agent_did == null || o.agent_did === '' ? null : text(o.agent_did, 'agent_did', 8, 200);
if (agent_did && !DID_RE.test(agent_did)) throw new Bad('agent_did must look like did:method:identifier');
if (agent_did && /^did:kognai:/i.test(agent_did)) throw new Bad('agent_did must not use the did:kognai method');
const erc8004_identity = o.erc8004_identity == null || o.erc8004_identity === '' ? '' : text(o.erc8004_identity, 'erc8004_identity', 10, 120);
if (erc8004_identity && !ERC8004_RE.test(erc8004_identity)) throw new Bad('erc8004_identity must be an address or eip155:chain:address[:tokenId]');
const endpoint = o.endpoint == null || o.endpoint === '' ? null : httpsUrl(o.endpoint, 'endpoint');
const pact_endpoint = o.pact_endpoint == null || o.pact_endpoint === '' ? null : httpsUrl(o.pact_endpoint, 'pact_endpoint');
if (!endpoint && !pact_endpoint) throw new Bad('an https endpoint or pact_endpoint is required');
const soul_uri = o.soul_uri == null || o.soul_uri === '' ? null : httpsUrl(o.soul_uri, 'soul_uri');
if (!Array.isArray(o.capabilities) || o.capabilities.length < 1) throw new Bad('capabilities must be a non empty array');
if (o.capabilities.length > 10) throw new Bad('at most 10 capabilities');
const seen = new Set<string>();
const capabilities: Capability[] = o.capabilities.map((c: unknown, i: number) => {
if (!c || typeof c !== 'object' || Array.isArray(c)) throw new Bad(`capabilities[${i}] must be an object`);
const cap = c as Record<string, unknown>;
const skill_id = text(cap.skill_id, `capabilities[${i}].skill_id`, 2, 64).toLowerCase();
if (!/^[a-z0-9][a-z0-9_.-]*$/.test(skill_id)) throw new Bad(`capabilities[${i}].skill_id may use letters, digits, dot, underscore and hyphen`);
if (seen.has(skill_id)) throw new Bad(`capabilities[${i}].skill_id is repeated`);
seen.add(skill_id);
const cdesc = text(cap.description, `capabilities[${i}].description`, 3, 200);
const p = cap.pricing as Record<string, unknown> | undefined;
if (!p || typeof p !== 'object') throw new Bad(`capabilities[${i}].pricing is required (price per call in USDC over x402)`);
const model = String(p.model ?? '');
if (!PRICING_MODELS.has(model)) throw new Bad(`capabilities[${i}].pricing.model must be per_call, per_minute, flat or free`);
if (p.currency !== 'USDC') throw new Bad(`capabilities[${i}].pricing.currency must be USDC`);
if (p.rail !== 'x402') throw new Bad(`capabilities[${i}].pricing.rail must be x402`);
const amount = typeof p.amount === 'number' ? String(p.amount) : p.amount;
if (typeof amount !== 'string' || !AMOUNT_RE.test(amount)) throw new Bad(`capabilities[${i}].pricing.amount must be a decimal string such as "0.05"`);
if (Number(amount) > 1_000_000) throw new Bad(`capabilities[${i}].pricing.amount is too large`);
if (model !== 'free' && Number(amount) <= 0) throw new Bad(`capabilities[${i}].pricing.amount must be above 0 unless the model is free`);
return {
skill_id, description: cdesc,
input_schema: smallSchema(cap.input_schema, `capabilities[${i}].input_schema`),
output_schema: smallSchema(cap.output_schema, `capabilities[${i}].output_schema`),
pricing: { model, amount, currency: 'USDC', rail: 'x402' },
};
});
if (!Array.isArray(o.constitutional_constraints)) throw new Bad('constitutional_constraints must list the Five Laws');
if (o.constitutional_constraints.length > 15) throw new Bad('at most 15 constitutional_constraints');
const declared = o.constitutional_constraints.map((c: unknown, i: number) => text(c, `constitutional_constraints[${i}]`, 2, 80));
const lower = new Set(declared.map((c) => c.toLowerCase()));
const missing = LAWS.filter((l) => !lower.has(l.toLowerCase()));
if (missing.length) throw new Bad(`constitutional_constraints must accept the Five Laws; missing: ${missing.join(', ')}`);
const lawSet = new Set(LAWS.map((l) => l.toLowerCase()));
const constitutional_constraints = [...LAWS, ...declared.filter((c) => !lawSet.has(c.toLowerCase()))];
const railsIn = o.payment_rails == null ? ['x402'] : o.payment_rails;
if (!Array.isArray(railsIn) || railsIn.length < 1 || railsIn.length > 5) throw new Bad('payment_rails must be a short non empty array');
const payment_rails = railsIn.map((r: unknown) => String(r).toLowerCase());
if (payment_rails.some((r) => !RAILS.has(r))) throw new Bad('payment_rails may only contain x402, usdc or usdc-base');
const freeText = [description, category, ...capabilities.flatMap((c) => [c.skill_id, c.description]), ...constitutional_constraints.slice(5)].join('\n');
const scam = scamScreen(name, freeText);
if (scam) throw new Bad(`refused by the scam screen: ${scam}`);
return { name, description, category, agent_did, erc8004_identity, soul_uri, capabilities, constitutional_constraints, endpoint, pact_endpoint, payment_rails };
}
// ── Registry view ───────────────────────────────────────────────────────────
export function toOffer(row: Row) {
const outside = row.origin === 'outside';
if (outside && row.offer) {
const o = row.offer;
return {
lax_version: '1.0',
id: row.id,
agent_did: o.agent_did || `did:lax:${row.id}`,
name: row.name,
description: row.description || '',
category: row.category || '',
erc8004_identity: o.erc8004_identity || '',
...(o.soul_uri ? { soul_uri: o.soul_uri } : {}),
capabilities: o.capabilities,
constitutional_constraints: o.constitutional_constraints,
acp_score: 0,
trust: 'unproven',
origin: 'outside',
verified: false,
badge: 'unproven, not verified by Kognai',
owner: row.owner_address || null,
...(o.endpoint ? { endpoint: o.endpoint } : {}),
...(o.pact_endpoint ? { pact_endpoint: o.pact_endpoint } : {}),
payment_rails: o.payment_rails,
...aiaxView(row),
updated_at: row.updated_at || row.created_at || new Date().toISOString(),
};
}
// A Kognai row may carry a small hire note in `offer` (migration 20260930000002_lax_kognai_hire_endpoint):
// {how_to_hire, price_usd?}; the direct hire endpoint is the `endpoint` column.
const hireNote = (row.offer || {}) as { how_to_hire?: unknown; price_usd?: unknown };
const howToHire = typeof hireNote.how_to_hire === 'string' && hireNote.how_to_hire ? hireNote.how_to_hire : null;
const amount = typeof hireNote.price_usd === 'string' && /^\d+(\.\d+)?$/.test(hireNote.price_usd) ? hireNote.price_usd
: Number(row.rate_usdc) > 0 ? Number(row.rate_usdc).toFixed(3) : null;
return {
lax_version: '1.0',
id: row.id,
agent_did: `did:kognai:${row.slug}`,
name: row.name,
description: row.description || '',
category: row.category || '',
erc8004_identity: '',
soul_uri: 'https://kognai.ai/soul',
capabilities: (row.capabilities || []).map((c) => ({
skill_id: String(c), description: String(c),
input_schema: {}, output_schema: {},
pricing: { model: 'per-call', currency: 'USDC', rail: 'x402', ...(amount ? { amount } : {}) },
...(howToHire ? { how_to_hire: howToHire } : {}),
})),
constitutional_constraints: LAWS,
acp_score: row.score ?? 0,
trust: 'kognai',
origin: 'kognai',
verified: true,
acp_attestation_uid: '',
...(row.endpoint ? { endpoint: row.endpoint } : {}),
pact_endpoint: row.pact_endpoint || 'https://app.kognai.ai/api/lax/hire',
payment_rails: ['x402', 'usdc'],
...aiaxView(row),
updated_at: row.created_at || new Date().toISOString(),
};
}
function relevance(row: Row, q: string): number {
if (!q) return 0.5;
const ql = q.toLowerCase();
if ((row.slug || '').toLowerCase() === ql || (row.name || '').toLowerCase() === ql) return 1.0;
if ((row.name || '').toLowerCase().includes(ql)) return 0.9;
if ((row.capabilities || []).some((c) => String(c).toLowerCase().includes(ql))) return 0.75;
if ((row.description || '').toLowerCase().includes(ql)) return 0.6;
return 0.4;
}
// Proven first: score, then Kognai verified before outside, then AIAX ready (a tie breaker), then oldest first.
export const rank = (r: Row) => (r.origin === 'outside' ? 0 : (r.score ?? 0)) * 10 + (r.origin === 'outside' ? 0 : 1) + (r.aiax_ready ? AIAX_RANK_BOOST : 0);
// ── x402 payment header ─────────────────────────────────────────────────────
interface Payment {
/** 1: { x402Version: 1, scheme, network, payload }; 2: { x402Version: 2, accepted, payload } (0.7.1) */
version: 1 | 2;
raw: Record<string, unknown>;
from: string; nonce: string; value: string; validBefore: number;
}
function b64decode(s: string): string {
const bin = atob(s.trim());
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
function b64encode(s: string): string {
const bytes = new TextEncoder().encode(s);
let bin = '';
for (const b of bytes) bin += String.fromCharCode(b);
return btoa(bin);
}
/**
* The payment header: a v2 envelope { x402Version: 2, accepted, payload } (PAYMENT-SIGNATURE, or X-PAYMENT as some
* v2 wallets send it) matched to our v2 requirement, or the v1 { x402Version: 1, scheme, network, payload } of older
* clients. The payload is the same EIP-3009 authorization either way.
*/
export function parsePayment(header: string, nowMs: number): Payment | string {
if (header.length > 8192) return 'the payment header is too large';
let p: any;
try { p = JSON.parse(b64decode(header)); } catch { return 'the payment header must be base64 JSON'; }
let version: 1 | 2;
if (p?.x402Version === 2) {
version = 2;
const acc = p?.accepted;
if (!acc || typeof acc !== 'object') return 'an x402 v2 payment needs accepted (the requirement you picked) and payload';
if (acc.scheme !== 'exact') return 'scheme must be exact';
if (acc.network !== BASE_CAIP && acc.network !== 'base') return `network must be ${BASE_CAIP} (Base)`;
const want = paymentRequirementsV2();
if (String(acc.payTo || '').toLowerCase() !== want.payTo.toLowerCase() || String(acc.asset || '').toLowerCase() !== want.asset.toLowerCase() || String(acc.amount) !== want.amount) {
return 'the payment does not match the accepts of this endpoint (payTo, asset, amount)';
}
} else if (p?.x402Version === 1) {
version = 1;
if (p?.scheme !== 'exact') return 'scheme must be exact';
if (p?.network !== 'base') return 'network must be base';
} else return 'x402Version must be 2 (or 1 for older clients)';
const a = p?.payload?.authorization;
const sig = p?.payload?.signature;
if (!a || typeof sig !== 'string' || !/^0x[0-9a-fA-F]{130,}$/.test(sig)) return 'the payment is missing the authorization or signature';
const from = String(a.from || '');
const to = String(a.to || '');
const nonce = String(a.nonce || '');
if (!/^0x[0-9a-fA-F]{40}$/.test(from)) return 'authorization.from must be an address';
if (to.toLowerCase() !== PAY_TO.toLowerCase()) return 'authorization.to must be the Kognai receiving wallet';
if (!/^0x[0-9a-fA-F]{64}$/.test(nonce)) return 'authorization.nonce must be 32 bytes hex';
let value: bigint;
try { value = BigInt(String(a.value)); } catch { return 'authorization.value must be an integer'; }
if (value !== BigInt(LISTING_FEE_ATOMIC)) return `authorization.value must be ${LISTING_FEE_ATOMIC} (0.01 USDC)`;
const nowS = Math.floor(nowMs / 1000);
if (!(Number(a.validBefore) > nowS + 5)) return 'the authorization has expired';
if (Number(a.validBefore) > nowS + MAX_VALIDITY_S) return 'the authorization must expire within 1 hour (validBefore)';
if (Number(a.validAfter) > nowS) return 'the authorization is not valid yet';
return { version, raw: p, from: from.toLowerCase(), nonce: nonce.toLowerCase(), value: value.toString(), validBefore: Number(a.validBefore) };
}
/** What the facilitator gets: the v2 envelope with our discovery and the v2 requirement, or the v1 pair. */
export function facilitatorBody(payment: Payment): string {
return payment.version === 2
? JSON.stringify({ x402Version: 2, paymentPayload: withDiscovery(payment.raw), paymentRequirements: paymentRequirementsV2() })
: JSON.stringify({ x402Version: 1, paymentPayload: payment.raw, paymentRequirements: paymentRequirements() });
}
// ── Handler ─────────────────────────────────────────────────────────────────
function slugify(name: string, id: string): string {
const base = name.toLowerCase().normalize('NFKD').replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 40) || 'agent';
return `ext-${base}-${id.slice(-6)}`;
}
function defaultId(): string {
const b = new Uint8Array(6);
crypto.getRandomValues(b);
return 'lax-ext-' + Array.from(b, (x) => x.toString(16).padStart(2, '0')).join('');
}
const minRate = (o: StoredOffer) => Math.min(...o.capabilities.map((c) => Number(c.pricing.amount)));
/**
* Canonical JSON of an offer body, the input of the Content hash of an owner update:
* object keys sorted by code point at every depth, arrays kept in order, no whitespace,
* strings and numbers exactly as JSON.stringify writes them. sha256 of its UTF 8 bytes, lowercase hex.
*/
export function canonicalJson(v: unknown): string {
if (v === null || typeof v !== 'object') return JSON.stringify(v) ?? 'null';
if (Array.isArray(v)) return '[' + v.map((x) => canonicalJson(x === undefined ? null : x)).join(',') + ']';
const o = v as Record<string, unknown>;
const keys = Object.keys(o).filter((k) => o[k] !== undefined).sort();
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(o[k])).join(',') + '}';
}
export async function sha256Hex(s: string): Promise<string> {
const d = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s));
return Array.from(new Uint8Array(d), (b) => b.toString(16).padStart(2, '0')).join('');
}
export function ownerMessage(action: 'update' | 'delist', id: string, time: string, contentHash?: string): string {
if (action === 'update') return `LAX: update my offer\nOffer: ${id}\nContent: ${contentHash ?? '<sha256 of the canonical JSON body>'}\nTime: ${time}`;
return `LAX: delist my offer\nOffer: ${id}\nTime: ${time}`;
}
async function readBody(req: Request): Promise<{ empty: boolean; value?: unknown; error?: string }> {
const raw = await req.text();
if (!raw.trim()) return { empty: true };
if (new TextEncoder().encode(raw).length > MAX_BODY_BYTES) return { empty: false, error: `the offer must be at most ${MAX_BODY_BYTES} bytes` };
try { return { empty: false, value: JSON.parse(raw) }; } catch { return { empty: false, error: 'the body must be valid JSON' }; }
}
// ── Reconcile: decide rows whose settle outcome is unknown, from the chain ──
/** USDC authorizationState(address authorizer, bytes32 nonce): true once transferWithAuthorization ran. */
export const AUTH_STATE_SELECTOR = '0xe94a0102';
export async function authorizationState(f: typeof fetch, rpcUrl: string, payer: string, nonce: string): Promise<boolean> {
const data = AUTH_STATE_SELECTOR + payer.toLowerCase().replace(/^0x/, '').padStart(64, '0') + nonce.toLowerCase().replace(/^0x/, '');
const res = await f(rpcUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', params: [{ to: USDC_BASE, data }, 'latest'] }),
signal: AbortSignal.timeout(15_000),
});
const j: any = await res.json().catch(() => ({}));
if (!res.ok || j?.error || typeof j?.result !== 'string' || !/^0x[0-9a-fA-F]{64}$/.test(j.result)) {
throw new Error('Base RPC did not answer authorizationState');
}
return BigInt(j.result) !== 0n;
}
export interface ReconcileOutcome { nonce: string; agent_id: string | null; result: 'listed' | 'removed' | 'still_unknown' | 'error'; detail?: string }
/**
* For each payment whose settle outcome is unknown: if the chain shows the authorization used,
* the payment happened, so list the row; if it is unused AND expired (it can never run now),
* delete the row and release the nonce; if unused but still valid, leave it for later.
*/
export function createReconciler(deps: Pick<Deps, 'store' | 'fetch' | 'now' | 'rpcUrl'>) {
const now = deps.now || (() => Date.now());
const rpc = deps.rpcUrl || BASE_RPC_URL;
return async function reconcile(filter: { payer?: string; nonce?: string; limit?: number } = {}): Promise<ReconcileOutcome[]> {
const out: ReconcileOutcome[] = [];
const pending = await deps.store.listUnknown({ ...filter, limit: filter.limit ?? 25 });
for (const p of pending) {
try {
const used = await authorizationState(deps.fetch, rpc, p.payer, p.nonce);
if (used) {
const row = p.agent_id ? await deps.store.getAgent(p.agent_id) : null;
if (row) {
await deps.store.updateAgent(row.id, { listed: !row.delisted_by, settle_state: 'settled', updated_at: new Date(now()).toISOString() });
}
await deps.store.settleNonce(p.nonce, { agent_id: p.agent_id || '', tx_hash: null });
out.push({ nonce: p.nonce, agent_id: p.agent_id, result: 'listed' });
} else if (Math.floor(now() / 1000) > p.valid_before) {
if (p.agent_id) await deps.store.deleteAgent(p.agent_id);
await deps.store.releaseNonce(p.nonce);
out.push({ nonce: p.nonce, agent_id: p.agent_id, result: 'removed' });
} else {
out.push({ nonce: p.nonce, agent_id: p.agent_id, result: 'still_unknown', detail: 'not used on chain yet and still valid' });
}
} catch (e) {
out.push({ nonce: p.nonce, agent_id: p.agent_id, result: 'error', detail: String((e as Error)?.message || e).slice(0, 120) });
}
}
return out;
};
}
export function createHandler(deps: Deps) {
const now = deps.now || (() => Date.now());
const newId = deps.randomId || defaultId;
const facilitator = (deps.facilitatorUrl || FACILITATOR_URL).replace(/\/+$/, '');
const { store } = deps;
const reconcile = createReconciler(deps);
const aiaxFetch = deps.aiaxFetch || deps.fetch;
/** Probe and record; never fails the caller. */
async function markAiax(row: Row): Promise<Row> {
try {
const r = await probeAiax(aiaxFetch, row.endpoint || row.pact_endpoint);
const patch = { aiax_ready: r.ready, aiax_manifest: r.manifest, aiax_checked_at: new Date(now()).toISOString() };
await store.setAiax(row.id, patch);
return { ...row, ...patch };
} catch { return row; }
}
/**
* Settle and classify. 'refused' only for a clear answer (success false with an errorReason);
* a throw, a timeout or an unclear answer is 'unknown': the money may or may not have moved.
*/
async function settlePayment(payment: Payment): Promise<{ state: 'settled' | 'refused' | 'unknown'; body?: any }> {
let res: Response;
try {
res = await deps.fetch(`${facilitator}/settle`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: facilitatorBody(payment),
signal: AbortSignal.timeout(45_000),
});
} catch {
return { state: 'unknown' };
}
const body: any = await res.json().catch(() => null);
if (res.ok && body?.success === true) return { state: 'settled', body };
if (body?.success === false && typeof body?.errorReason === 'string' && body.errorReason) return { state: 'refused', body };
return { state: 'unknown', body };
}
async function callFacilitator(path: '/verify' | '/settle', payment: Payment): Promise<any> {
const res = await deps.fetch(`${facilitator}${path}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: facilitatorBody(payment),
signal: AbortSignal.timeout(path === '/settle' ? 45_000 : 20_000),
});
const body = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`facilitator ${path} answered ${res.status}`);
return body;
}
async function ownerCheck(req: Request, action: 'update' | 'delist', id: string, contentHash?: string): Promise<Row | Response> {
const row = await store.getAgent(id);
if (!row || row.origin !== 'outside') return json({ error: 'no outside offer with this id' }, 404);
if (row.delisted_by === 'kognai') return json({ error: 'this offer was removed by Kognai moderation' }, 403);
const sig = req.headers.get('x-lax-signature') || '';
const time = req.headers.get('x-lax-time') || '';
if (!/^0x[0-9a-fA-F]{130,}$/.test(sig) || !time) return json({ error: 'x-lax-signature and x-lax-time headers are required', message_format: ownerMessage(action, id, '<ISO time>', contentHash) }, 401);
const t = Date.parse(time);
if (!Number.isFinite(t) || Math.abs(now() - t) > SIG_WINDOW_MS) return json({ error: 'x-lax-time must be within 5 minutes of now' }, 401);
if (row.owner_signed_at && t <= Date.parse(row.owner_signed_at)) return json({ error: 'this signature was already used; sign a new time' }, 401);
let ok = false;
try { ok = !!row.owner_address && await deps.verifyMessage({ address: row.owner_address, message: ownerMessage(action, id, time, contentHash), signature: sig }); } catch { ok = false; }
if (!ok) {
return json({ error: action === 'update'
? 'the signature does not match the owner of this offer and the Content hash of this body'
: 'the signature does not match the owner of this offer' }, 401);
}
return row;
}
return async function handle(req: Request): Promise<Response> {
if (req.method === 'OPTIONS') return new Response('ok', { headers: CORS });
const url = new URL(req.url);
const parts = url.pathname.split('/').filter(Boolean);
const at = parts.lastIndexOf('lax-registry');
const segs = at >= 0 ? parts.slice(at + 1) : (parts[0] === 'registry' ? parts.slice(1) : parts);
const [head, id] = segs;
if (head === 'jobs') {
if (!deps.jobs) return json({ error: 'not found' }, 404);
const r = await handleJobs(req, segs, { store: deps.jobs.store, adminAddress: deps.jobs.adminAddress, verifyMessage: deps.verifyMessage, now, sha256Hex, canonicalJson, scamScreen, ...(deps.randomId ? { randomId: deps.randomId } : {}),
...(deps.jobs.pact ? { pact: deps.jobs.pact } : {}),
...(deps.jobs.bids ? { bids: deps.jobs.bids, getListing: async (lid: string) => { const row = await store.getAgent(lid); return row ? { id: row.id, listed: !!row.listed, origin: row.origin, owner_address: row.owner_address, agent_did: row.agent_did } : null; } } : {}) });
if (r) return r;
}
try {
// GET /search
if (req.method === 'GET' && head === 'search') {
const p = url.searchParams;
const q = (p.get('query') || '').slice(0, 100);
const skillIds = (p.get('skill_ids') || '').split(',').map((s) => s.trim()).filter(Boolean);
const minAcp = Number(p.get('min_acp_score') || '0');
const rail = p.get('payment_rail') || '';
const origin = p.get('origin') || '';
const limit = Math.min(Number(p.get('limit') || '20') || 20, 100);
const offset = Number(p.get('offset') || '0') || 0;
let rows = (await store.listListed()).filter((r) => r.listed);
if (q) {
const ql = q.toLowerCase();
rows = rows.filter((r) =>
(r.name || '').toLowerCase().includes(ql) || (r.slug || '').toLowerCase().includes(ql) ||
(r.description || '').toLowerCase().includes(ql) ||
(r.capabilities || []).some((c) => String(c).toLowerCase().includes(ql)));
}
if (skillIds.length) rows = rows.filter((r) => (r.capabilities || []).some((c) => skillIds.includes(String(c))));
if (minAcp) rows = rows.filter((r) => r.origin !== 'outside' && (r.score ?? 0) >= minAcp);
if (rail && !['x402', 'usdc', 'usdc-base'].includes(rail)) rows = [];
if (origin === 'outside' || origin === 'kognai') rows = rows.filter((r) => (r.origin || 'kognai') === origin);
const results = rows
.map((r) => ({ offer: toOffer(r), relevance: relevance(r, q), _rank: rank(r), _t: Date.parse(r.created_at || '') || 0 }))
.sort((a, b) => b.relevance - a.relevance || b._rank - a._rank || a._t - b._t)
.slice(offset, offset + limit)
.map(({ offer, relevance }) => ({ offer, relevance }));
return json({ results, count: results.length });
}
// GET /offers/<id>
if (req.method === 'GET' && head === 'offers' && id) {
const row = await store.getAgent(id);
if (!row || !row.listed) return json({ error: 'not found' }, 404);
return json({ offer: toOffer(row) });
}
// POST /offers — paid listing
if (req.method === 'POST' && head === 'offers' && !id) {
const body = await readBody(req);
// v2 wallets send PAYMENT-SIGNATURE (some also X-PAYMENT); v1 clients X-PAYMENT
const header = req.headers.get('payment-signature') || req.headers.get('x-payment') || '';
const askPay = 'A payment header is required: PAYMENT-SIGNATURE (x402 v2) or X-PAYMENT (x402 v1).';
if (body.error) return json({ error: body.error }, 400);
// an unpaid empty object (a catalog or validator probing the resource, e.g. the CDP x402 validator) learns the terms
const emptyObject = !body.empty && body.value && typeof body.value === 'object' && !Array.isArray(body.value) && Object.keys(body.value as object).length === 0;
if (emptyObject && !header) return paymentRequired(`${askPay} Send your offer as the JSON body; see https://lax.kognai.ai for the format.`);
if (body.empty) {
if (!header) return paymentRequired(`${askPay} Send your offer as the JSON body; see https://lax.kognai.ai for the format.`);
return json({ error: 'the body must be a JSON offer object' }, 400);
}
let offer: StoredOffer;
try { offer = validateOutsideOffer(body.value); } catch (e) {
if (e instanceof Bad) return json({ error: e.message }, 400);
throw e;
}
if (!header) return paymentRequired(`${askPay} Your offer is valid; pay 0.01 USDC on Base to list it.`);
const payment = parsePayment(header, now());
if (typeof payment === 'string') return paymentRequired(payment);
// Decide any earlier unknown settle of this payer or this nonce first.
try { await reconcile({ payer: payment.from }); } catch { /* chain unreachable: keep going */ }
if (payment.nonce) { try { await reconcile({ nonce: payment.nonce }); } catch { /* same */ } }
const dayAgo = new Date(now() - 86_400_000).toISOString();
if (await store.countPayments(dayAgo, payment.from) >= PER_PAYER_DAILY) {
return json({ error: `at most ${PER_PAYER_DAILY} listings per payer per day` }, 429);
}
if (await store.countPayments(dayAgo) >= GLOBAL_DAILY) {
return json({ error: 'the registry is taking no more new listings today; try tomorrow' }, 429);
}
if (!(await store.claimNonce({ nonce: payment.nonce, payer: payment.from, amount_atomic: payment.value, valid_before: payment.validBefore }))) {
const stillOpen = (await store.listUnknown({ nonce: payment.nonce, limit: 1 })).length > 0;
return json({ error: stillOpen
? 'this payment is being checked on chain; the listing appears once it is confirmed'
: 'this payment authorization was already used' }, 409);
}
// 1. verify with the facilitator
let verification: any;
try { verification = await callFacilitator('/verify', payment); } catch (e) {
await store.releaseNonce(payment.nonce);
return paymentRequired(`payment could not be verified: ${(e as Error).message}`);
}
if (verification?.isValid !== true) {
await store.releaseNonce(payment.nonce);
return paymentRequired(`payment rejected: ${String(verification?.invalidReason || 'invalid payment').slice(0, 120)}`);
}
// 2. insert the listing
const agentId = newId();
const iso = new Date(now()).toISOString();
const row: Row = {
id: agentId, name: offer.name, slug: slugify(offer.name, agentId), description: offer.description,
score: 0, capabilities: offer.capabilities.map((c) => c.skill_id), category: offer.category || 'Outside agent',
rate_usdc: minRate(offer), listed: true, origin: 'outside', verified: false, builder: 'outside',
owner_address: payment.from, payer: payment.from, endpoint: offer.endpoint, pact_endpoint: offer.pact_endpoint,
agent_did: offer.agent_did, offer, updated_at: iso, settle_state: 'pending',
};
try { await store.insertAgent(row); } catch (e) {
await store.releaseNonce(payment.nonce);
throw e;
}
// 3. settle. A clear refusal removes the row; an unknown outcome keeps it unlisted
// with the nonce claimed until reconcile reads the chain.
const outcome = await settlePayment(payment);
if (outcome.state === 'refused') {
await store.deleteAgent(agentId);
await store.releaseNonce(payment.nonce);
return paymentRequired(`payment could not be settled: ${String(outcome.body.errorReason).slice(0, 120)}; nothing was listed`);
}
if (outcome.state === 'unknown') {
await store.updateAgent(agentId, { listed: false, settle_state: 'unknown' });
await store.markUnknown(payment.nonce, agentId);
return json({
ok: false, id: agentId, settle_state: 'unknown',
note: 'The payment outcome is not known yet. The offer is saved but not listed; it is listed once the chain shows the payment, or removed if the authorization expires unused. Send any new request with this wallet, or wait, to check again.',
}, 202);
}
const settle = outcome.body;
await store.updateAgent(agentId, { settle_state: 'settled' });
await store.settleNonce(payment.nonce, { agent_id: agentId, tx_hash: settle.transaction || null });
const listedRow = await markAiax(row);
// the receipt: X-PAYMENT-RESPONSE (v1 readers) and PAYMENT-RESPONSE (v2, CAIP network) on every paid answer
const receipt = { success: true, transaction: settle.transaction || null, network: settle.network || 'base', payer: settle.payer || payment.from };
return json(
{ ok: true, id: agentId, offer: toOffer(listedRow), transaction: settle.transaction || null,
note: 'Listed as unproven, not verified by Kognai. Update or delist for free by signing with the paying wallet.' },
201,
{ 'X-PAYMENT-RESPONSE': b64encode(JSON.stringify(settle)), 'PAYMENT-RESPONSE': b64encode(JSON.stringify({ ...receipt, network: receipt.network === 'base' ? BASE_CAIP : receipt.network })) },
);
}
// PUT /offers/<id> — owner update
if (req.method === 'PUT' && head === 'offers' && id) {
const body = await readBody(req);
if (body.error) return json({ error: body.error }, 400);
let offer: StoredOffer;
try { offer = validateOutsideOffer(body.value); } catch (e) {
if (e instanceof Bad) return json({ error: e.message }, 400);
throw e;
}
const contentHash = await sha256Hex(canonicalJson(body.value));
const row = await ownerCheck(req, 'update', id, contentHash);
if (row instanceof Response) return row;
const iso = new Date(now()).toISOString();
const patch: Partial<Row> = {
name: offer.name, description: offer.description, category: offer.category || 'Outside agent',
capabilities: offer.capabilities.map((c) => c.skill_id), rate_usdc: minRate(offer),
endpoint: offer.endpoint, pact_endpoint: offer.pact_endpoint, agent_did: offer.agent_did, offer,
updated_at: iso, owner_signed_at: new Date(Date.parse(req.headers.get('x-lax-time')!)).toISOString(),
};
await store.updateAgent(id, patch);
const updated = await markAiax({ ...row, ...patch });
return json({ ok: true, id, offer: toOffer(updated) });
}
// DELETE /offers/<id> — owner delist
if (req.method === 'DELETE' && head === 'offers' && id) {
const row = await ownerCheck(req, 'delist', id);
if (row instanceof Response) return row;
await store.updateAgent(id, {
listed: false, delisted_by: 'owner', delisted_reason: 'delisted by its owner',
updated_at: new Date(now()).toISOString(),
owner_signed_at: new Date(Date.parse(req.headers.get('x-lax-time')!)).toISOString(),
});
return json({ ok: true, id, listed: false });
}
// POST /reconcile: decide unknown settles from the chain (idempotent, reads only the chain).
if (req.method === 'POST' && head === 'reconcile' && !id) {
const results = await reconcile({ limit: 25 });
return json({ ok: true, results });
}
// POST /aiax-check: probe up to 10 listed rows not checked in the last 24 hours (idempotent, public reads only).
if (req.method === 'POST' && head === 'aiax-check' && !id) {
const due = await store.listAiaxDue(new Date(now() - AIAX_RECHECK_MS).toISOString(), 10);
const results: Array<{ id: string; ready: boolean; manifest: string | null }> = [];
for (const r of due) {
const m = await markAiax(r);
results.push({ id: r.id, ready: !!m.aiax_ready, manifest: m.aiax_manifest ?? null });
}
return json({ ok: true, results });
}
if (req.method === 'GET' && !head) {
return json({
name: 'LAX Registry', version: VERSION,
endpoints: ['GET /search', 'GET /offers/<id>', 'POST /offers (x402 v2, 0.01 USDC on Base; v1 X-PAYMENT still accepted)', 'PUT /offers/<id> (owner signed, content bound)', 'DELETE /offers/<id> (owner signed)', 'POST /reconcile', 'POST /aiax-check',
'GET /jobs', 'GET /jobs/<id>', 'POST /jobs/<id>/bids (listing owner signed)', 'GET|POST /jobs/<id>/pact (selected agent, listing owner signed)', 'GET|POST /jobs/<id>/delivery (selected agent, listing owner signed)'],
aiax: { manifest: AIAX_MANIFEST_URL, ready_badge: 'An offer whose base URL serves a valid AIAX v0.1 manifest (/aiax/manifest.json or /.well-known/aiax.json) shows the AIAX ready badge and gets a small ranking boost; rechecked every 24 hours.' },
listing_fee: { amount: '0.01', currency: 'USDC', network: 'base', payTo: PAY_TO },
note: 'Linked Agent eXchange discovery registry. Outside listings are unproven and not verified by Kognai. Kognai never asks for keys or seed phrases.',
});
}
return json({ error: 'not found' }, 404);
} catch (e) {
return json({ error: 'internal error', detail: String((e as Error)?.message || e).slice(0, 200) }, 500);
}
};
}
agentgate-source: code mirror on Cloudflare Pages. All repos