x402-paid-agent-starter / tree / typescript/src/list.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// 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