// Lists a LAX offer on the registry, paying the 402 through a caller supplied function.
// The key never enters this module: pay() signs outside and returns only the header value.
export interface ListOptions {
offer: Record<string, unknown>;
/** Default https://lax.kognai.ai/registry */
registryUrl?: string;
fetch?: (url: string, init: { method: string; headers: Record<string, string>; body: string }) => Promise<Response>;
/** Gets the 402 terms, returns the payment header value (base64). */
pay: (paymentRequired: PaymentRequired) => Promise<string> | string;
}
export interface PaymentRequired {
x402Version: number;
accepts: Array<Record<string, unknown>>;
error?: string;
resource?: unknown;
[k: string]: unknown;
}
export class ListOfferError extends Error {
constructor(message: string, readonly status: number) {
super(message);
this.name = 'ListOfferError';
}
}
function fromBase64Json(v: string): unknown {
const bin = atob(v.trim());
return JSON.parse(new TextDecoder().decode(Uint8Array.from(bin, (c) => c.charCodeAt(0))));
}
async function bodyOf(res: Response): Promise<Record<string, unknown>> {
const text = await res.text();
try {
const b = JSON.parse(text);
return b && typeof b === 'object' ? b : {};
} catch {
return {};
}
}
/** The x402 terms from a 402: the v2 PAYMENT-REQUIRED header when present, else the JSON body. */
export async function readPaymentRequired(res: Response): Promise<PaymentRequired> {
const body = await bodyOf(res);
const h = res.headers.get('payment-required');
if (h) {
try {
const v = fromBase64Json(h) as PaymentRequired;
if (v && Array.isArray(v.accepts)) return v;
} catch {
// fall back to the body
}
}
if (Array.isArray(body.accepts)) return body as unknown as PaymentRequired;
throw new ListOfferError('the registry answered 402 without payment terms', 402);
}
export async function listOffer(o: ListOptions): Promise<{ id: string }> {
const url = `${(o.registryUrl ?? 'https://lax.kognai.ai/registry').replace(/\/+$/, '')}/offers`;
const f = o.fetch ?? ((u, i) => globalThis.fetch(u, i));
const body = JSON.stringify(o.offer);
const headers = { 'content-type': 'application/json', accept: 'application/json' };
let res = await f(url, { method: 'POST', headers, body });
if (res.status === 402) {
const terms = await readPaymentRequired(res);
const value = await o.pay(terms);
if (typeof value !== 'string' || value === '') throw new ListOfferError('pay() must return the payment header value', 402);
const name = Number(terms.x402Version) >= 2 ? 'PAYMENT-SIGNATURE' : 'X-PAYMENT';
res = await f(url, { method: 'POST', headers: { ...headers, [name]: value }, body });
}
const out = await bodyOf(res);
if (res.status < 200 || res.status >= 300) {
throw new ListOfferError(typeof out.error === 'string' ? out.error : `the registry answered HTTP ${res.status}`, res.status);
}
if (typeof out.id !== 'string' || out.id === '') throw new ListOfferError('the registry answer has no listing id', res.status);
return { id: out.id };
}
agentgate-source: code mirror on Cloudflare Pages. All repos