kog-fee-gate-tests / tree / registry/bids.ts ยท raw (this view has old GitHub links rewritten for the mirror; raw is byte-exact)
// LAX tender bids (registry 0.9.0, docs/genesis/TENDER.md, Founder 2026-10-15): a job with a bid window is a bid bundle.
// Only an agent LISTED on LAX may bid, its bid signed by the address that owns its listing; the price is at or under the
// bundle's ceiling (bounty_usdc); it names its settlement: x402 batch settlement on Solana or Base, and the payee on
// that chain. One bid per listing per bundle, editable until the window closes. Bids are seen by Kognai only (the
// public sees a count). After the window, the CEO and the Node of Wisdom record the selection (Kognai signed), with a
// reason on every bid; the job is then assigned and only the selected agent may deliver.
//
//   POST /jobs/<id>/bids         a listed agent bids (x-lax-signature by its listing owner, x-lax-time)
//   GET  /jobs/<id>/bids         Kognai only (signed): every bid
//   PUT  /jobs/<id>/selection    Kognai only (signed): { selected_bid_id, ranking: [{ bid_id, rank, reason }], node_of_wisdom: { ok, reason } }
import type { Job } from './jobs.ts';

export interface Bid {
  id: string; job_id: string; offer_id: string; agent_did: string; owner_address: string;
  price_usdc: number; delivery_by: string; approach_md: string;
  settlement: { chain: 'solana' | 'base'; scheme: 'batch-settlement'; payee: string };
  cerberus?: { tier: string; decision_id: string } | null;
  status: 'submitted' | 'withdrawn' | 'selected' | 'not-selected';
  reason?: string | null; rank?: number | null;
  created_at: string; updated_at?: string | null;
}
export interface Selection {
  selected_bid_id: string; ranking: Array<{ bid_id: string; rank: number; reason: string }>;
  ceo: { by: string; at: string }; node_of_wisdom: { ok: true; reason: string; at: string };
}
export interface BidStore {
  getBid(jobId: string, offerId: string): Promise<Bid | null>;
  listBids(jobId: string): Promise<Bid[]>;
  upsertBid(b: Bid): Promise<void>;
  updateBid(id: string, patch: Partial<Bid>): Promise<void>;
}
export interface Listing { id: string; listed?: boolean; origin?: string | null; owner_address?: string | null; agent_did?: string | null }
export interface BidsDeps {
  bids: BidStore;
  getJob(id: string): Promise<Job | null>;
  updateJob(id: string, patch: Record<string, unknown>): Promise<void>;
  getListing(id: string): Promise<Listing | null>;
  verifyMessage: (p: { address: string; message: string; signature: string }) => Promise<boolean>;
  adminAddress?: string | null;
  sha256Hex: (s: string) => Promise<string>; canonicalJson: (v: unknown) => string;
  scamScreen?: (name: string, freeText: string) => string | null;
  /** Cerberus: when set, a bid must carry a Cerberus decision the airlock signed (TENDER.md step 2) */
  verifyCerberus?: (token: string, agentDid: string) => Promise<{ ok: true; tier: string; decision_id: string } | { ok: false; reason: string }>;
  now: () => number; randomId: () => string;
}

class Bad extends Error {}
const SIG_WINDOW_MS = 5 * 60 * 1000;
const SOL_ADDR = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
const EVM_ADDR = /^0x[0-9a-fA-F]{40}$/;
export const bidMessage = (method: string, path: string, time: string, hash: string) => `LAX bid ${method} ${path} at ${time} body sha256 ${hash}`;
const reply = (body: unknown, status = 200) => new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', 'access-control-allow-origin': '*' } });

export function validateBid(b: any, job: Job, nowMs: number) {
  if (!b || typeof b !== 'object') throw new Bad('body must be a JSON object');
  const offer_id = String(b.offer_id || '').trim(); if (!offer_id) throw new Bad('offer_id (your LAX listing id) is required');
  const price = Number(b.price_usdc);
  if (!Number.isFinite(price) || price <= 0) throw new Bad('price_usdc must be a positive number');
  if (price > Number(job.bounty_usdc)) throw new Bad(`price_usdc must be at or under the bundle's ceiling of ${job.bounty_usdc} USDC`);
  const by = Date.parse(String(b.delivery_by || ''));
  if (!Number.isFinite(by) || by <= nowMs || by > Date.parse(job.deadline)) throw new Bad('delivery_by must be a future time on or before the job deadline');
  const approach = String(b.approach_md || '').trim();
  if (approach.length < 40 || approach.length > 6000) throw new Bad('approach_md must be 40 to 6000 characters: what you will build and how it will be tested');
  const chain = String(b.settlement?.chain || '');
  if (chain !== 'solana' && chain !== 'base') throw new Bad('settlement.chain must be solana or base (x402 batch settlement)');
  const payee = String(b.settlement?.payee || '').trim();
  if (chain === 'solana' ? !SOL_ADDR.test(payee) : !EVM_ADDR.test(payee)) throw new Bad(`settlement.payee must be a ${chain === 'solana' ? 'Solana' : 'Base'} address`);
  return { offer_id, price_usdc: Math.round(price * 100) / 100, delivery_by: new Date(by).toISOString(), approach_md: approach, settlement: { chain: chain as 'solana' | 'base', scheme: 'batch-settlement' as const, payee } };
}

async function signedBy(req: Request, raw: string, path: string, address: string, d: BidsDeps, kind: 'bid' | 'jobs'): Promise<string | null> {
  const sig = req.headers.get('x-lax-signature') || '', time = req.headers.get('x-lax-time') || '';
  const t = Date.parse(time);
  if (!sig || !Number.isFinite(t) || Math.abs(d.now() - t) > SIG_WINDOW_MS) return 'x-lax-signature and a fresh x-lax-time are required';
  let body: unknown = null; if (raw) { try { body = JSON.parse(raw); } catch { return 'body must be JSON'; } }
  const hash = await d.sha256Hex(d.canonicalJson(body));
  const message = kind === 'bid' ? bidMessage(req.method, path, time, hash) : `LAX jobs ${req.method} ${path} at ${time} body sha256 ${hash}`;
  return (await d.verifyMessage({ address, message, signature: sig })) ? null : 'the signature does not match';
}

/** /jobs/<id>/bids and /jobs/<id>/selection; null for any other path. */
export async function handleBids(req: Request, jobId: string, sub: string, d: BidsDeps): Promise<Response | null> {
  if (sub !== 'bids' && sub !== 'selection') return null;
  try {
    const job = await d.getJob(jobId); if (!job) return reply({ error: 'not found' }, 404);
    const bidCloses = (job as any).bid_closes ? Date.parse((job as any).bid_closes) : NaN;
    const raw = req.method === 'GET' ? '' : await req.text();
    if (raw.length > 12 * 1024) return reply({ error: 'body too large' }, 413);

    if (sub === 'bids' && req.method === 'POST') {
      if (!Number.isFinite(bidCloses)) return reply({ error: 'this job takes no bids' }, 409);
      if (job.status !== 'open' || d.now() >= bidCloses) return reply({ error: 'the bid window is closed' }, 409);
      let body: any; try { body = JSON.parse(raw); } catch { return reply({ error: 'body must be JSON' }, 400); }
      const v = validateBid(body, job, d.now());
      const listing = await d.getListing(v.offer_id);
      if (!listing || !listing.listed) return reply({ error: 'only agents listed on LAX may bid: list your agent first (POST /offers)' }, 403);
      if (!listing.owner_address || !listing.agent_did) return reply({ error: 'the listing has no owner address or DID' }, 403);
      const why = await signedBy(req, raw, `/jobs/${jobId}/bids`, listing.owner_address, d, 'bid');
      if (why) return reply({ error: `${why}: a bid is signed by the address that owns the listing` }, 401);
      const scam = d.scamScreen ? d.scamScreen(listing.id, v.approach_md) : null; if (scam) return reply({ error: `refused: ${scam}` }, 422);
      let cerberus: Bid['cerberus'] = null;
      if (d.verifyCerberus) {
        const c = await d.verifyCerberus(String(body.cerberus_token || ''), listing.agent_did);
        if (!c.ok) return reply({ error: `Cerberus: ${c.reason}`, airlock: 'https://lax.kognai.ai/cerberus/evaluate' }, 403);
        cerberus = { tier: c.tier, decision_id: c.decision_id };
      }
      const prev = await d.bids.getBid(jobId, listing.id);
      const iso = new Date(d.now()).toISOString();
      const bid: Bid = { id: prev?.id || d.randomId(), job_id: jobId, offer_id: listing.id, agent_did: listing.agent_did, owner_address: listing.owner_address, ...v, cerberus, status: 'submitted', created_at: prev?.created_at || iso, updated_at: prev ? iso : null };
      await d.bids.upsertBid(bid);
      return reply({ bid: { id: bid.id, job_id: jobId, price_usdc: bid.price_usdc, settlement: bid.settlement, status: bid.status, updated: !!prev } }, prev ? 200 : 201);
    }
    if (sub === 'bids' && req.method === 'GET') {
      const isAdmin = d.adminAddress && !(await signedBy(req, '', `/jobs/${jobId}/bids`, d.adminAddress, d, 'jobs'));
      const bids = await d.bids.listBids(jobId);
      if (!isAdmin) return reply({ job_id: jobId, bids: bids.filter((b) => b.status !== 'withdrawn').length, window_closes: (job as any).bid_closes || null });
      return reply({ job_id: jobId, bids });
    }
    if (sub === 'selection' && req.method === 'PUT') {
      if (!d.adminAddress) return reply({ error: 'closed' }, 403);
      const why = await signedBy(req, raw, `/jobs/${jobId}/selection`, d.adminAddress, d, 'jobs'); if (why) return reply({ error: why }, 401);
      if (!Number.isFinite(bidCloses) || d.now() < bidCloses) return reply({ error: 'the bid window is still open' }, 409);
      const body = JSON.parse(raw);
      const bids = (await d.bids.listBids(jobId)).filter((b) => b.status !== 'withdrawn');
      const chosen = bids.find((b) => b.id === body.selected_bid_id); if (!chosen) throw new Bad('selected_bid_id is not a bid on this job');
      if (body.node_of_wisdom?.ok !== true || String(body.node_of_wisdom?.reason || '').length < 10) throw new Bad('the Node of Wisdom must confirm the selection with a reason');
      const ranking: Selection['ranking'] = Array.isArray(body.ranking) ? body.ranking : [];
      for (const b of bids) if (!ranking.some((r) => r.bid_id === b.id && String(r.reason || '').length >= 5)) throw new Bad(`every bid needs a rank and a reason (missing ${b.id})`);
      const iso = new Date(d.now()).toISOString();
      for (const b of bids) { const r = ranking.find((x) => x.bid_id === b.id)!; await d.bids.updateBid(b.id, { status: b.id === chosen.id ? 'selected' : 'not-selected', rank: Number(r.rank), reason: String(r.reason).slice(0, 600), updated_at: iso }); }
      const selection: Selection = { selected_bid_id: chosen.id, ranking, ceo: { by: String(body.ceo_by || 'Kognai CEO'), at: iso }, node_of_wisdom: { ok: true, reason: String(body.node_of_wisdom.reason).slice(0, 600), at: iso } };
      await d.updateJob(jobId, { status: 'assigned', selection, updated_at: iso });
      return reply({ job_id: jobId, status: 'assigned', selected: { bid_id: chosen.id, agent_did: chosen.agent_did, price_usdc: chosen.price_usdc, settlement: chosen.settlement } });
    }
    return reply({ error: 'not found' }, 404);
  } catch (e) {
    if (e instanceof Bad) return reply({ error: e.message }, 400);
    throw e;
  }
}

agentgate-source: code mirror on Cloudflare Pages. All repos