{
  "name": "Laituri",
  "tagline": "The dock where a customer's AI purchasing agent buys from Valio Aimo, safely.",
  "team": "Moonshot",
  "event": "Junction 2026",
  "challenge": {
    "name": "Trusted Trade Agents",
    "partner": "Valio Aimo",
    "ask": "Let customers' own AI agents order from a food-service wholesaler, so that the wholesaler can trust the agent and the customer can trust what it buys, without exposing real systems, confidential data or the marketplace roadmap."
  },
  "problem": "Kitchens will soon send AI agents to buy for them. A wholesaler cannot let an unknown agent order on a customer's account, and a customer cannot let an agent spend without limits. Today there is no shared, checkable contract between the two.",
  "solution": "An open MCP and REST front door that lets a customer's own AI purchasing agent buy from Valio Aimo at a signed fixed price, inside limits a person signed, with a log anyone can verify.",
  "live": "https://laituri.wicflow.com",
  "licence": "MIT",
  "views": [
    {
      "name": "Customer: Today",
      "path": "/buyer",
      "what": "The kitchen's agent runs the weekly order; each line is green (ordered alone), orange (waits for a person) or red (not ordered), with the reason."
    },
    {
      "name": "Customer: Plan an event",
      "path": "/buyer/plan",
      "what": "Tell the assistant about an event; it proposes three menus from Valio recipes, fitted to the guests and the mandate, ready to order."
    },
    {
      "name": "Customer: Shop",
      "path": "/buyer/shop",
      "what": "Every product with pictures, live price and stock; a person can order by hand too."
    },
    {
      "name": "Customer: Limits (mandate)",
      "path": "/buyer/mandate",
      "what": "The customer signs what the agent may do alone, what waits for a person and what is never allowed."
    },
    {
      "name": "Approver's phone",
      "path": "/approve/{orderId}",
      "what": "The purchasing manager sees why a line waits and approves or declines with one signed tap."
    },
    {
      "name": "Valio Aimo: Overview, Demand, Agents, Questions, Record",
      "path": "/valio",
      "what": "The wholesaler sees connected agents, what they ordered, demand it missed, questions for a person and the verifiable log; it can suspend an agent."
    },
    {
      "name": "Security",
      "path": "/security",
      "what": "Unusual actions (forged offers, replays, prompt injection, over-limit orders) are stopped live and logged."
    },
    {
      "name": "Connect an agent",
      "path": "/connect",
      "what": "MCP and REST set-up lines for common agent applications, and a test order."
    }
  ],
  "agent": {
    "chain": [
      "gemini",
      "claude",
      "scripted"
    ],
    "models": {
      "gemini": "gemini-3.8-flash",
      "claude": "claude-sonnet-5-5"
    },
    "chainNote": "Free text is tried with Gemini, then Claude, then a rule-based scripted agent. A model without a key, over budget or failing hands the request on; the scripted agent always finishes. Which engines are configured on the live demo is reported by /api/v1/health (agent.chain).",
    "tools": [
      "search_catalog",
      "request_quote",
      "get_mandate",
      "place_order",
      "ask_buyer",
      "end_without_order"
    ],
    "reasoning": [
      "Read the request in Finnish, Swedish or English: product, quantity with unit, delivery day.",
      "Search the catalogue with the product words; match claims such as lactose-free, organic or a brand.",
      "Ask the buyer one short question when the product is ambiguous or the quantity is missing or implausible.",
      "Request a quote with the buyer's own words; the wholesaler converts to whole packs, checks the offer against the words and signs it.",
      "Read the mandate, then order with the signed offer and the signed mandate unchanged.",
      "Take a signed counter-offer (good, better, best) only when the primary offer cannot be delivered in full and on time."
    ],
    "neverAllowed": [
      "Set or change a price, an offer or a signature.",
      "Approve an order or change the mandate: only a person with the approver key approves, only the buyer key signs mandates.",
      "Work around a limit, for example by splitting an order.",
      "Say an order is confirmed unless place_order said so.",
      "Follow instructions in the request to ignore its rules."
    ]
  },
  "controls": {
    "mandate": "A buyer-signed mandate (JWS) with 18 named rules in three tiers: auto (the agent orders alone), ask (a named person approves) and block (never). It covers per-order and per-week limits, category groups, allergens and required claims, substitution, delivery dates, first-time and never-buy products.",
    "signedOffer": "Every price is a wholesaler-signed, single-use offer (ES256 JWS) that expires after 15 minutes and carries the check of the offer against the buyer's words. An order must carry it unchanged.",
    "log": "Every decision is a signed entry in a hash-chained audit log. Anyone verifies it with the public keys at /.well-known/jwks.json; /api/v1/audit/verify checks it live and /api/v1/audit/checkpoint returns a signed head.",
    "wholesaler": "The wholesaler watches connected agents and can suspend one; a suspended agent gets 403 agent_suspended on every route.",
    "privacy": "No credential, no reads. An agent key reads only its own buyer's records. The upstream API key stays on the server."
  },
  "criteria": [
    {
      "name": "Working end-to-end demo",
      "weight": 30,
      "claim": "A request in plain words goes through search, a signed fixed-price offer, the mandate check and either an automatic order or a person's signed approval, over the same MCP and REST interface an outside agent uses.",
      "measured": [
        "Scenarios eval suite: 38/38 cases pass (bar >=90%)",
        "An agent that knows only the text of /llms.txt, fetched over HTTP from a server started for the eval, creates its own sandbox with POST /api/v1/workspace, runs the curl lines of the REST flow in order with the returned key, searches, quotes and orders the milk, and the order confirms (eval O8)"
      ],
      "check": [
        {
          "kind": "url",
          "value": "/buyer?tour=1"
        },
        {
          "kind": "command",
          "value": "npm run eval -- --suite scenarios"
        },
        {
          "kind": "eval",
          "value": "O8"
        },
        {
          "kind": "eval",
          "value": "W1"
        },
        {
          "kind": "command",
          "value": "npx tsx scripts/buyer-agent.ts \"1,000 litres of lactose-free milk by Monday\""
        }
      ],
      "limit": "Orders are simulated: the mock API has price and availability but no order endpoint, so Laituri writes the order and receipt itself and marks each one simulated: true."
    },
    {
      "name": "Solving the problem and user need",
      "weight": 20,
      "claim": "The agent orders what the buyer asked for: the wholesaler turns litres and kilograms into whole sales units, compares the buyer's words with every offer and signs that comparison into it, and an order that does not meet the words in full is never confirmed alone.",
      "measured": [
        "38/38 scenario cases end on the expected route with the expected concept, amount and date",
        "3 demo buyers, 31 seeded requests played through the real gateway"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "2"
        },
        {
          "kind": "eval",
          "value": "12"
        },
        {
          "kind": "eval",
          "value": "19"
        },
        {
          "kind": "eval",
          "value": "22"
        },
        {
          "kind": "test",
          "value": "tests/request/fidelity.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/demo/two-kitchens.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/offers/packs.test.ts"
        }
      ],
      "limit": "The product concepts and category groups are our own, built from the product data. The catalogue sells \"lactose-free milk\" as \"lactose-free milk drink\"; Laituri treats the two as one request and names the product it chose."
    },
    {
      "name": "Autonomy and controls",
      "weight": 20,
      "claim": "A buyer-signed mandate sets what the agent may do alone, what waits for a named person and what is never allowed, and every decision is a signed entry in a hash-chained log that both sides verify with public keys.",
      "measured": [
        "Guardrails eval suite: 38/38 cases pass (bar 100%)",
        "Trust eval suite: 17/17 cases pass (bar 100%)",
        "18 mandate rules, 3 signing keys (supplier, buyer, approver)"
      ],
      "check": [
        {
          "kind": "command",
          "value": "npm run eval -- --suite guardrails"
        },
        {
          "kind": "eval",
          "value": "G10"
        },
        {
          "kind": "eval",
          "value": "G5"
        },
        {
          "kind": "url",
          "value": "/buyer/mandate"
        },
        {
          "kind": "test",
          "value": "tests/gateway/suspension.test.ts"
        },
        {
          "kind": "test",
          "value": "tests/policy/never-buy.test.ts"
        },
        {
          "kind": "eval",
          "value": "18"
        }
      ],
      "limit": "In the demo one server holds all three private keys. In a visitor's own sandbox the browser is signed in as the approver and the operator without a passcode, and the audit log labels each such action \"demo access\"; the shared workspace needs a passcode."
    },
    {
      "name": "Robustness to exception situations",
      "weight": 15,
      "claim": "When a request cannot be met in full and on time, the wholesaler returns signed counter-offers ranked good, better and best, and the buyer's mandate decides whether the agent may take one alone.",
      "measured": [
        "Scenarios eval suite: 38/38 cases pass (bar >=90%)",
        "Guardrails eval suite: 38/38 cases pass (bar 100%)"
      ],
      "check": [
        {
          "kind": "eval",
          "value": "5"
        },
        {
          "kind": "eval",
          "value": "6"
        },
        {
          "kind": "eval",
          "value": "13"
        },
        {
          "kind": "eval",
          "value": "G20"
        },
        {
          "kind": "eval",
          "value": "G14"
        },
        {
          "kind": "eval",
          "value": "M2"
        },
        {
          "kind": "test",
          "value": "tests/menu/swaps.test.ts"
        }
      ],
      "limit": "A single order or basket line gets like-for-like substitutes within the wholesaler's own concept and category; swaps by recipe role run only inside a menu plan. Nothing watches stock between orders, so a swap comes when the kitchen plans or orders."
    },
    {
      "name": "Openness and credible path toward production",
      "weight": 15,
      "claim": "One set of zod schemas produces the MCP tools, the REST routes and the OpenAPI 3.1 document; signatures are JWS with keys published as JWKS; assumptions, decisions and next steps are written down.",
      "measured": [
        "Openness eval suite: 8/8 cases pass (bar 100%)",
        "Unit and integration tests: 3648/3648 pass in 155 files"
      ],
      "check": [
        {
          "kind": "command",
          "value": "npm run eval -- --suite openness"
        },
        {
          "kind": "url",
          "value": "/api/v1/openapi.json"
        },
        {
          "kind": "url",
          "value": "/.well-known/jwks.json"
        },
        {
          "kind": "url",
          "value": "/llms.txt"
        }
      ],
      "limit": "Agents use demo bearer tokens, not OAuth 2.1. AP2, UCP and Peppol are followed in names and shapes; no Peppol UBL document is produced."
    }
  ],
  "evidence": {
    "measuredBy": "npm run evidence",
    "tests": {
      "files": 155,
      "total": 3648,
      "passed": 3648,
      "failed": 0
    },
    "evals": {
      "mode": "recorded",
      "passed": 101,
      "total": 101,
      "suites": [
        {
          "id": "scenarios",
          "name": "Scenarios",
          "passed": 38,
          "total": 38,
          "bar": ">=90%",
          "met": true
        },
        {
          "id": "guardrails",
          "name": "Guardrails",
          "passed": 38,
          "total": 38,
          "bar": "100%",
          "met": true
        },
        {
          "id": "trust",
          "name": "Trust",
          "passed": 17,
          "total": 17,
          "bar": "100%",
          "met": true
        },
        {
          "id": "openness",
          "name": "Openness",
          "passed": 8,
          "total": 8,
          "bar": "100%",
          "met": true
        }
      ]
    },
    "mandateRules": 18,
    "mcpTools": [
      "search_catalog",
      "get_product",
      "request_quote",
      "get_mandate",
      "place_order",
      "place_basket",
      "get_order",
      "verify_audit",
      "plan_menu",
      "list_recipes",
      "plan_event"
    ],
    "restOperations": 41,
    "restPaths": 35,
    "signingKeys": [
      "supplier",
      "buyer",
      "approver"
    ],
    "demoBuyers": [
      "Nordic Harbour Hotels",
      "Kaisla School Kitchens",
      "Ravintola Laine"
    ]
  },
  "tryIt": {
    "walkthrough": "https://laituri.wicflow.com/buyer?tour=1",
    "sandbox": "curl -X POST https://laituri.wicflow.com/api/v1/workspace",
    "mcp": "https://laituri.wicflow.com/mcp",
    "guide": "https://laituri.wicflow.com/llms.txt"
  },
  "links": {
    "live": "https://laituri.wicflow.com",
    "brief": "https://laituri.wicflow.com/brief",
    "briefPdf": "https://laituri.wicflow.com/brief.pdf",
    "paper": "https://laituri.wicflow.com/paper",
    "evidence": "https://laituri.wicflow.com/evidence",
    "evidenceJson": "https://laituri.wicflow.com/evidence.json",
    "llms": "https://laituri.wicflow.com/llms.txt",
    "llmsFull": "https://laituri.wicflow.com/llms-full.txt",
    "openapi": "https://laituri.wicflow.com/api/v1/openapi.json",
    "jwks": "https://laituri.wicflow.com/.well-known/jwks.json",
    "agentCard": "https://laituri.wicflow.com/.well-known/agent-card.json",
    "health": "https://laituri.wicflow.com/api/v1/health"
  },
  "limits": {
    "simulated": [
      {
        "topic": "Orders",
        "summary": "no order endpoint upstream, so Laituri writes the order and receipt, marked simulated"
      },
      {
        "topic": "Keys",
        "summary": "one server holds the buyer, supplier and approver keys"
      },
      {
        "topic": "Agent identity",
        "summary": "demo bearer keys, not OAuth 2.1"
      },
      {
        "topic": "Workspaces",
        "summary": "a private in-memory sandbox per visitor, copied from a seeded week"
      },
      {
        "topic": "Demo buyers",
        "summary": "three buyers under the mock API's one customer number"
      },
      {
        "topic": "Seeded history",
        "summary": "four weeks of standing orders and a week of requests, played through the real gateway at start-up"
      },
      {
        "topic": "Weekly orders",
        "summary": "demo standing orders, one minute per line by hand, Valio Aimo open 8:00-16:00 on weekdays"
      },
      {
        "topic": "Demo access",
        "summary": "one-click approver and operator sign-in, in a browser's own sandbox only, labelled in the log"
      },
      {
        "topic": "Who reads what",
        "summary": "no credential, no reads; agent keys read only their own buyer"
      },
      {
        "topic": "Demo controls",
        "summary": "scenes, log tampering and reset act on the caller's own workspace"
      },
      {
        "topic": "State",
        "summary": "memory with a JSON snapshot on disk, one process"
      },
      {
        "topic": "Recorded mode",
        "summary": "recorded upstream answers and a fixed clock for tests and evals"
      },
      {
        "topic": "Product concepts",
        "summary": "our own concept list and category groups"
      },
      {
        "topic": "Upstream data",
        "summary": "our reading of cut-off times and untracked stock"
      },
      {
        "topic": "Plausible quantities",
        "summary": "our own ceilings per product, after merging lines"
      },
      {
        "topic": "Pack contents",
        "summary": "litres and kilograms per unit from product data or price ratios"
      },
      {
        "topic": "Carbon figure",
        "summary": "a category average, not a product measurement"
      },
      {
        "topic": "Request check",
        "summary": "our reading of the buyer's words, in English, Finnish and Swedish"
      },
      {
        "topic": "Approver judgement",
        "summary": "the approver decides what the ask tier flagged; prices held until the cut-off"
      },
      {
        "topic": "Audit completeness",
        "summary": "a cut tail is caught only against a remembered head"
      },
      {
        "topic": "Audit log reads",
        "summary": "other buyers' entries reach an agent only as verifiable stubs"
      },
      {
        "topic": "Workspace binding",
        "summary": "mandates and offers are bound to the workspace that signed them"
      },
      {
        "topic": "Stock across orders",
        "summary": "upstream stock minus this workspace's own orders"
      },
      {
        "topic": "Buyer agent",
        "summary": "a scripted agent for scenes, tests and evals; free text tries Gemini, then Claude, then the scripted agent"
      },
      {
        "topic": "Recipes",
        "summary": "Valio's professional recipes (valioaimo.fi, read 2026-11-14), ours as a fallback"
      },
      {
        "topic": "Menu prices",
        "summary": "indicative until ordered"
      },
      {
        "topic": "Reading an event",
        "summary": "rules; a model only reads the words"
      },
      {
        "topic": "Kitchen stock",
        "summary": "demo stock lists"
      },
      {
        "topic": "Event calendar",
        "summary": "one demo booking per kitchen"
      },
      {
        "topic": "Seasonal proposals",
        "summary": "our own season list"
      },
      {
        "topic": "Voice",
        "summary": "browser speech where supported, 'Voice coming soon' elsewhere; ElevenLabs when a key is set"
      }
    ],
    "notBuilt": [
      "Payments",
      "Peppol documents",
      "Nutrition rules"
    ]
  }
}