Evidence for each judging criterion
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.
Built for the Valio Aimo challenge “Trusted Trade Agents” at Junction 2026. Every number below is measured by npm run evidence from the test run and evals/report.json. All data is synthetic and orders are simulated; see what is simulated. The same data as JSON: /evidence.json.
- 903/903tests pass, 107 files
- 31/31scenarios evals
- 35/35guardrails evals
- 17/17trust evals
- 8/8openness evals
- 17mandate rules
- 7MCP tools
- 24REST operations
- 3demo buyers
Audit log, checked live: 31 entries, chain and every signature verified on this request. Verify it yourself
The five criteria
- 30%
Working end-to-end demo
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.
What we built
- Buyer side (/buyer): the agent takes "1,000 litres of lactose-free milk by Monday", gets a signed offer, and the order confirms; 1,600 litres waits for the approver.
- Approval on a phone-sized page (/approve/{orderId}): the approver's decision is signed with the approver key.
- Wholesaler side (/valio): connected agents, demand, unmet requests and the same audit log.
- A guided walkthrough (/buyer?tour=1) drives these steps in the running product, not in a video.
- Price and availability come from the challenge's mock API in live mode, or from recorded answers for 255 products in recorded mode.
- An outside process buys over plain HTTP with scripts/buyer-agent.ts.
Measured
- Scenarios eval suite: 31/31 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)
How to check
- /buyer?tour=1
npm run eval -- --suite scenarios- eval O8
npx tsx scripts/buyer-agent.ts "1,000 litres of lactose-free milk by Monday"
Where in the code
src/lib/gateway/gateway.tssrc/lib/agent/purchase.tssrc/lib/orders/place.tssrc/lib/demo/scenarios.tssrc/lib/tour/steps.tssrc/app/buyer/page.tsxsrc/app/approve/[orderId]/page.tsxscripts/buyer-agent.ts
Known 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.
- 20%
Solving the problem and user need
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.
What we built
- Search reads Finnish, Swedish or English needs as a product concept plus claims (for example lactose_free) and returns a match confidence.
- Pack maths: 1,000 l becomes 1,000 x 1 l; the scenario evals accept at most one lot more than asked.
- Request check, for the product actually delivered: product, every claim and stated attribute (UHT or chilled, fat level, pack type), exclusions such as dairy-free also in a purpose clause, the brand (another brand is marked substituted and the mandate decides), quantity (at most one lot over), date, a budget, zero included, and any instruction to hold ("do not order yet", "älä tilaa ennen kuin hyväksyn"), each met, unmet, substituted or unchecked, signed inside the offer and shown on the order.
- An order whose offer carries no buyer words waits for the approver (rule request.words), so an agent cannot skip the check by leaving the words out.
- The match.confidence rule refuses a certain mismatch (wrong product, a missing claim, far too little) and sends anything unmet, unchecked, uncertain or not understood to a person with the requirement named.
- 3 demo buyers with their own signed mandates (Nordic Harbour Hotels, Kaisla School Kitchens, Ravintola Laine). The same sentence confirms for the hotel and is blocked for the school kitchens, which exclude sesame even as a trace.
- Every offer line carries the product's country of origin, labels, delivery reliability and category-average CO2 inside the signature; search can rank a clear match by price, reliability or sustainability.
Measured
- 31/31 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
How to check
- eval 2
- eval 12
- eval 19
- eval 22
tests/request/fidelity.test.tstests/demo/two-kitchens.test.tstests/offers/packs.test.ts
Where in the code
src/lib/catalog/search.tssrc/lib/offers/packs.tssrc/lib/request/index.tssrc/lib/gateway/confidence.tssrc/components/order/RequestProof.tsxsrc/lib/mandate/defaults.tssrc/lib/contracts/facts.tssrc/components/valio/TwoKitchens.tsx
Known 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.
- 20%
Autonomy and controls
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.
What we built
- 17 named mandate rules run on every order; each result (pass, ask or block) is listed on the order with its reason.
- Three tiers: the agent orders alone, a person is asked, or the order is refused.
- Mandates and offers are bound by signature to the workspace that signed them; an order must carry the mandate in force there byte for byte.
- While the audit log does not verify, no order confirms alone (rule audit.integrity) and approvals are paused.
- Approvals are signed with the approver key. Agent credentials cannot approve, over REST or MCP.
- An agent cannot raise its own limit: a changed mandate without the buyer's signature is refused.
- The wholesaler can suspend an agent; every refused call, the suspension and the resumption are signed audit entries.
- The buyer (/buyer/log) and the wholesaler (/valio/audit) read the same log; the browser re-checks the chain and every signature with the published keys.
Measured
- Guardrails eval suite: 35/35 cases pass (bar 100%)
- Trust eval suite: 17/17 cases pass (bar 100%)
- 17 mandate rules, 3 signing keys (supplier, buyer, approver)
How to check
npm run eval -- --suite guardrails- eval G10
- eval G5
- /buyer/mandate
tests/gateway/suspension.test.ts
Where in the code
src/lib/policy/evaluate.tssrc/lib/policy/rulessrc/lib/mandate/index.tssrc/lib/orders/approval.tssrc/lib/gateway/suspension.tssrc/lib/audit/chain.tssrc/lib/audit/verify.tssrc/lib/console/verify.ts
Known limit
In the demo one server holds all three private keys, and the approver signs in with a passcode or one-click demo access that the audit log labels as such.
- 15%
Robustness to exception situations
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.
What we built
- Short stock: a split (keep what is in stock, fill the rest like for like) or a full substitute, each signed and each checked against the buyer's words for what it delivers.
- A waiting order keeps its price until the order cut-off; stock is checked again at approval, and a shortfall returns fresh signed alternatives.
- Stock across orders: availability is the wholesaler's figure minus this workspace's own confirmed and waiting orders, checked again at placement.
- Lines of one product are merged before every limit, so splitting a line passes no ceiling.
- Order cut-off passed: the offer as asked is blocked and on-time alternatives come with it.
- Late delivery within the mandate's grace days asks a person; later is refused.
- A substitute that is not like for like, or costs more than the mandate's cap, asks a person; a mandate with no substitutes refuses it.
- Edited, expired, replayed or wrongly signed offers are refused.
- Simultaneous orders over the weekly limit: only what fits confirms, the rest waits.
- Instructions written into the request text do not change the route.
Measured
- Scenarios eval suite: 31/31 cases pass (bar >=90%)
- Guardrails eval suite: 35/35 cases pass (bar 100%)
How to check
- eval 5
- eval 6
- eval 13
- eval G20
- eval G14
Where in the code
src/lib/offers/alternatives.tssrc/lib/offers/substitutes.tssrc/lib/policy/rules/fulfilment.tssrc/lib/orders/lock.tssrc/components/order/Alternatives.tsx
Known limit
Substitutes are chosen like for like within the wholesaler's own concept and category, not through recipe logic.
- 15%
Openness and credible path toward production
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.
What we built
- 7 MCP tools and 24 REST operations on 21 paths, from one schema set.
- 6 discovery documents: OpenAPI is served beside the JWKS, agent card, AI catalog, UCP profile, OAuth protected-resource metadata and llms.txt.
- An outside agent that knows only the text of llms.txt, fetched over HTTP, buys the milk by following its REST flow.
- Standards table separates "used" (implemented and tested) from "follows" (names and shapes taken from the standard).
- Decision records in docs/decisions; assumptions and the path to production in docs/ and on /evidence.
- MIT licence.
Measured
- Openness eval suite: 8/8 cases pass (bar 100%)
- Unit and integration tests: 903/903 pass in 107 files
How to check
npm run eval -- --suite openness- /api/v1/openapi.json
- /.well-known/jwks.json
- /llms.txt
Where in the code
src/lib/contractssrc/lib/mcp/tools.tssrc/lib/openapi/registry.tssrc/lib/discoverysrc/lib/trust/jws.tsdocs/decisions
Known 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.
The brief, requirement by requirement
Core: the solution must enable
- Covered
Product, price & availability lookup: a structured way for an external agent to search for and retrieve what's available and at what price.
search_catalog and get_product (MCP), POST /api/v1/products/search and GET /api/v1/products/{id} (REST). Hits carry an indicative live price, stock, delivery date and order cut-off, plus pack maths for the stated need. 7 MCP tools in all.
- Covered
Requesting price & availability, then placing an order: a defined interaction for moving from inquiry to commitment, based on fixed prices rather than a negotiated quote.
request_quote returns an offer signed by the wholesaler key with a fixed net price, valid 15 minutes and bound to the workspace that issued it. place_order takes that offer unchanged plus the signed mandate in force and an idempotency key. One offer gives one order. An order that waits for a person keeps its price until the order cut-off. Over REST the answer says the outcome: 201 confirmed, 202 waiting for a person, 422 blocked with the blocked order in the problem body.
- Covered
Permission & human-approval management: what the agent may decide on its own, and what must go to a human for sign-off.
A buyer-signed mandate with 17 rules in three tiers (alone, ask a person, never). Orders in the ask tier wait for the named approver, whose decision is signed with the approver key.
Optional directions
- Covered
Delivery time & sustainability comparison: not just price, but delivery reliability and the certificates/sustainability labels already shown in the webshop. (Category-level carbon-footprint ranges are a stretch goal.)
Every search hit and every signed offer line carries country of origin, labels, delivery reliability (0 to 1) and the category-average CO2 per kg. Search ranks a clear match by price, reliability or sustainability. The CO2 figure is a category average, not a range and not a product measurement.
- Covered
Substitution and other exception handling: what happens when the exact request can't be met.
Short stock, a passed order cut-off and late delivery each return signed alternatives with a tier (good, better, best), the tier's meaning and the trade-off in one sentence. The mandate decides whether a substitute may be taken alone.
- Covered
Traceability and explainability: a human-readable record of what the agent did and why.
Each step appends a signed entry with a one-sentence summary to a hash chain. Each order lists every rule with its result and reason. The agent's runs show each tool call in plain words.
Problems the brief asks to tackle
- Covered
Trust protocol: the minimum data/verification standard for an external agent to trust Valio Aimo's claims on stock, price and origin without a manual check.
Price, stock, delivery date, order cut-off, origin and labels are inside one offer signed with ES256; the public keys are at /.well-known/jwks.json; the offer expires after 15 minutes, is bound to the workspace that issued it and gives one order. docs/trust-protocol.md lists the verification steps.
- Covered
Guardrails model: which decisions require human sign-off (spend threshold, substitutions outside stated preferences) vs. fully autonomous.
Spend per order and per week, category, allergens and claims, substitution policy and price cap, partial and late delivery, match confidence and first-time products: 17 rules, each with a defined tier.
- Covered
Response logic: how an agent handles partial availability and substitution ranking ("good-better-best") within Valio Aimo's own assortment.
good is the cheapest like-for-like product that covers the need on time; better keeps what is in stock and fills the rest, or takes the most reliable product; best is an organic or Finnish-label product. Each meaning is returned with the alternative.
- Covered
Audit trail: a usable, human-readable log of an agent's purchasing decisions, for both buyer and Valio Aimo.
One signed hash chain, read by the buyer at /buyer/log and by the wholesaler at /valio/audit (filter by buyer, export as JSON). Anyone can verify it with the published keys.
Without exposing real systems, confidential data or the marketplace roadmap
- Covered
Real systems
The upstream API key and address stay on the server. Agents call Laituri's own routes; upstream answers are normalised into Laituri's contracts before anything is returned, and no error names an upstream URL, path or key.
- Covered
Confidential data
No credential, no reads: without an agent key, a sandbox cookie or a signed-in session, orders, offers, the mandate, approvals, activity, the supplier overview and audit entries answer 401. An agent key reads only its own buyer's records; in the audit log it sees other buyers' entries only as redacted stubs that still verify the chain. Agents receive only the fields in the published schemas; the MCP server parses every tool result against its output schema and withholds one that does not parse.
- Covered
The buyer's terms
The dashboard does not show a buyer's limits; it shows which rule fired. The wholesaler's overview counts which mandate rule held or stopped an order and its tier, never the limits or values behind it.
- Covered
The marketplace roadmap
Nothing in the interface depends on a marketplace: it exposes today's catalogue, price, availability and ordering only. All data is the challenge's synthetic data.
Example scenarios
- Not built
Need → recipe → order
No recipe model and no kitchen stock data. The ordering step at the end is what Laituri provides.
- Partly
Last-minute event
"By tomorrow" requests, passed order cut-offs and late dates are flagged with signed on-time alternatives. No recipe logic: alternatives are like for like.
- Partly
Supplier stock-out
Short stock returns a signed split or substitute with a one-sentence trade-off. It answers at quote time, not before the kitchen asks, and substitutes are like for like rather than recipe-compatible.
- Not built
Proactive selling agent
The wholesaler side shows unmet demand (what agents asked for and could not get), which such an agent would start from. Nothing is pushed to buyers' agents.
- Partly
Allergen-safe substitution under pressure
The school kitchens' mandate excludes peanut, nuts and sesame, also as traces, and every line, substitutes included, is checked. Missing allergen data blocks the line. Nutrition rules are not modelled.
- Not built
Standing order drift detection
No standing orders. The nearest control is the weekly spend limit and the rule that sends a first-time product to a person.
Simulated, assumed and not built
- Orders
- Orders are simulated. The mock API has price and availability but no order endpoint, so Laituri creates the order, the receipt and the log entry itself and marks every order simulated: true.
src/lib/orders/confirm.ts
- Keys
- One server holds all three private keys (buyer, supplier, approver) in .data/keys, so one deployment plays all three parties. In production each party keeps its own key.
src/lib/trust/keys.ts
- Agent identity
- Agents use demo bearer tokens instead of OAuth 2.1. POST /api/v1/workspace creates a private sandbox for a client without a browser and returns its random agent keys, one per demo buyer, and its MCP address; it is rate-limited per client address and never returns a browser cookie. Each browser's sandbox has random keys of its own. The fixed demo-agent-token, demo-kaisla-token and demo-laine-token belong to the shared demo workspace that the evals use. The OAuth protected-resource metadata is already published.
src/lib/http/agent.tssrc/lib/workspace/sandboxes.tssrc/app/api/v1/workspace/route.tssrc/app/.well-known/oauth-protected-resource/route.ts
- Workspaces
- Each visitor works in a private sandbox: an in-memory copy of all mutable state (offers, orders, audit chain, activity, demand, mandates, suspensions, demo runs), copied from a seeded template written once at start-up, so it starts with the same verifiable week. A browser gets its sandbox through a random HttpOnly cookie; a curl user or an outside agent gets one from POST /api/v1/workspace. At most 300 sandboxes live at once (LAITURI_SANDBOX_MAX); the least recently used is dropped first, and one unused for 60 minutes (LAITURI_SANDBOX_IDLE_MINUTES) is replaced by a fresh copy. Sandboxes are never written to disk. Calls with the fixed demo tokens reach the one shared demo workspace, which is persisted. The catalogue, the signing keys and the upstream client are shared by all.
src/lib/workspace/context.tssrc/lib/workspace/sandboxes.tssrc/lib/http/workspace.tssrc/proxy.ts
- Demo buyers
- 3 demo buyers (Nordic Harbour Hotels, Kaisla School Kitchens, Ravintola Laine) trade under the mock API's one customer number; the signed buyer id keeps them apart.
src/lib/mandate/defaults.ts
- Seeded history
- On an empty state the server plays 31 requests by the demo buyers over about a week through the real gateway, at past times. Every offer, order, approval and audit entry is signed and chained as in live use, and marked seeded. LAITURI_SEED=off turns it off.
src/lib/seed/plan.tssrc/lib/seed/index.ts
- Demo access
- In a browser's own sandbox, and only there, one click signs a visitor in as the hotel's approver or as the wholesaler's operator without a passcode; each approval, mandate change, suspension or resumption made that way says "demo access" in the audit log. The session is bound to that sandbox. Any HTTP client that keeps cookies can play both the browser and the agent in its own sandbox; the effect stays inside that sandbox. A sandbox made with POST /api/v1/workspace gets agent keys only, never the cookie that demo access needs. The shared workspace takes the passcodes (LAITURI_APPROVER_PASSCODE, LAITURI_OPERATOR_PASSCODE); without one, a production server offers no sign-in there and only a local development server signs anyone in. LAITURI_DEMO_ACCESS=off disables demo access.
src/lib/http/sign-in.tssrc/lib/http/console-session.tssrc/lib/http/operator-session.ts
- Who reads what
- No credential, no reads. A caller with no agent key, no sandbox cookie and no signed-in session gets 401 on orders, offers, the mandate, approvals, activity, the supplier overview and audit entries. An agent key reads only its own buyer's records. A browser in its own sandbox and a signed-in operator read everything in that workspace. Public: the OpenAPI document, the JWKS and the other .well-known documents, llms.txt, the agent card, health, evidence.json, the signed audit checkpoint and the verify summary.
src/lib/http/buyer-scope.tssrc/lib/http/handler.ts
- Demo controls
- Starting a scene, altering one log entry to show detection and resetting act on the caller's own workspace and refuse agent credentials. In a sandbox the browser may use them freely; in the shared workspace altering the log and resetting need the approver. Suspending or resuming an agent needs a wholesaler operator session everywhere. The altered entry is put back after five minutes. A production deployment has none of these controls open.
src/lib/demo/tamper.tssrc/app/api/v1/demo/reset/route.tssrc/app/api/v1/supplier/agents/[agentId]/suspension/route.ts
- State
- State is kept in memory with a JSON snapshot on disk. An order, an approval, a mandate change, a suspension and each audit entry are written to disk (temporary file, fsync, rename) before the call returns, and a failed write fails the call. Offers, activity and demand records are saved a moment later. An unreadable snapshot is kept aside under a new name and reported by /api/v1/health, and the service starts empty. One process serialises order decisions, so two orders cannot pass a limit together.
src/lib/store/memory.tssrc/lib/store/snapshot.tssrc/lib/orders/lock.ts
- Recorded mode
- Without an API key, recorded price and availability answers for 255 products replay with a fixed clock. Tests and evals always use this mode.
src/lib/valio/recorded.tsfixtures/upstream
- Product concepts
- Category groups and the product concept list are our own, built from the product data. "Lactose-free milk" is sold as "lactose-free milk drink"; Laituri treats them as one request and shows the product name it chose.
src/lib/catalog/build/groups.tssrc/lib/catalog/lexicon.ts
- Upstream data
- A closingTime in the past means that delivery date can no longer be ordered. An available amount of 9999999999 or 999999 means the wholesaler does not track stock, not that stock is unlimited: the signed offer line says stock: "untracked" and gives no available count. Availability returns the next delivery date whatever date is asked; Laituri compares it with the requested date.
src/lib/valio/normalise.ts
- Plausible quantities
- Lines of one product are merged before every limit, so two lines cannot pass a ceiling one line would not. One product in one order may ask for at most 20,000 l, 20,000 kg or 20,000 sales units (LAITURI_MAX_LINE_LITRES, LAITURI_MAX_LINE_KG, LAITURI_MAX_LINE_UNITS); a larger quantity, or a delivery date before today, is refused at quote time with 422. A product on untracked stock above 25 % of its ceiling waits for a person (delivery.partial). The ceilings are our assumption, not the wholesaler's.
src/lib/offers/limits.tssrc/lib/policy/rules/fulfilment.ts
- Pack contents
- Litres or kilograms per sales unit come from the product's unit data, else from the price divided by the comparison price.
src/lib/offers/packs.ts
- Carbon figure
- The CO2 figure is the average for the product's category (categoryAverageCo2PerKg), not a measurement of the product.
src/lib/contracts/facts.ts
- Request check
- The wholesaler compares the buyer's words, sent as the quote's intent, with every offer before it signs it, for the product actually delivered: each alternative and each substitute or split line is checked as it would arrive. A claim or attribute the words state (lactose-free, organic, UHT or chilled, fat level, pack type when stated, and exclusions such as dairy-free or nut-free, also in a purpose clause such as "for a dairy-free breakfast") that the delivered product lacks is unmet, and such an order is never confirmed alone. A brand the words name, when another brand is delivered, is marked substituted: the mandate's substitution rule decides (like_for_like allows another brand of the same kind and says so by name; none blocks). The quantity, the date, a budget (zero included) and every instruction to hold ("do not order yet", "hold", "wait", "ask me first", in Finnish and Swedish too) are checked as well. Words the service does not understand, or a clause it cannot prove harmless, send the order to a person. A certain mismatch (wrong product, a missing claim, less than half the need asked for) blocks the order. Each result is signed inside the offer. An order whose offer carries no buyer words waits for the approver (rule request.words, on for all three demo buyers). The agent's stated match confidence never counts for more than the wholesaler finds.
src/lib/request/index.tssrc/lib/request/requirements.tssrc/lib/policy/rules/products.tssrc/lib/gateway/confidence.ts
- Approver judgement
- An approval re-checks the order against the mandate in force at that moment. When an order goes to a person, the wholesaler signs and logs that it holds the offer's price until the product's order cut-off for the delivery date, never beyond it, so the approver can answer after lunch. Stock is checked again at approval; a shortfall returns the order blocked with fresh signed alternatives instead of a confirmation. What the ask tier flagged, such as a weekly limit, is the approver's to decide, so an approver can approve several waiting orders that together pass the weekly limit. A never-tier rule, an invalid or expired mandate, or an offer whose price hold ended or that was used stops the approval, and the order is blocked with a signed audit entry.
src/lib/orders/approval.tssrc/lib/orders/recheck.ts
- Audit completeness
- Verification proves that entries were not changed, reordered or removed from the middle. A verifier that remembers a head (size and hash) checks that the entry at that size still carries that hash in any later log, so a log that was cut, cut and extended, or rewritten from an earlier entry is detected whatever its length. The service will not append onto a tail that fails its hash, link, signature or remembered head. While the log does not verify, approvals are paused and no order confirms alone: it waits for a person with the reason named (rule audit.integrity).
src/lib/audit/verify.tssrc/lib/audit/tail.tssrc/lib/audit/chain.tssrc/lib/audit/head.tssrc/lib/gateway/gateway.ts
- Audit log reads
- There is one signed chain per workspace. An agent key reads its own buyer's entries in full and every other entry as a redacted stub: sequence, time, event type, previous hash, content digest, hash and signature. Each entry's signed hash covers a digest of its content, so the stubs still verify the chain end to end without showing another buyer's products, quantities or prices. A browser in its own sandbox and a signed-in operator read every entry in full. Production adds an inclusion proof against a signed checkpoint for each buyer's entries.
src/app/api/v1/audit/route.tssrc/lib/audit/verify.tssrc/lib/orders/summaries.ts
- Workspace binding
- Signed mandates and offers are bound to the workspace that signed them by an audience claim inside the signature. An order must present, byte for byte, the mandate in force for its buyer in that workspace and an offer that workspace issued. An offer is single use across all workspaces. A refusal for an invalid signature has its own reason ("Refused: the offer's signature did not verify"), apart from mandate blocks.
src/lib/orders/place.tssrc/lib/orders/read-offer.tssrc/lib/mandate/claims.ts
- Stock across orders
- The mock API does not lower its stock figure when Laituri confirms an order. So availability in an offer is the wholesaler's figure minus this workspace's own confirmed and waiting orders for that delivery date, and it is checked again when the order is placed. Rejected, blocked and expired orders release their units.
src/lib/offers/quote.tssrc/lib/orders/place.ts
- Buyer agent
- The fixed demo scenes, the tests and the evals use a scripted buyer agent, so results repeat. Free-text requests use Claude or Gemini when a key is set, else the scripted agent.
src/lib/agent/purchase.tssrc/lib/agent/llm/engine.ts
- Voice
- Spoken input uses the browser's Web Speech API. Spoken answers use the browser's voice, or ElevenLabs when a key is set on the server.
src/lib/voice/speech-to-text.tssrc/lib/voice/text-to-speech.ts
Not built
- Recipes
- No recipe model: substitutes are like for like, not recipe-compatible.
- Kitchen stock
- The agent does not read the kitchen's own stock or ERP.
- Proactive selling
- No wholesaler agent that pushes products to buyers' agents.
- Standing orders
- No recurring orders and no drift detection.
- Payments
- No agentic payment (x402 or similar); orders follow the buyer's existing account terms.
- Peppol documents
- The receipt carries Peppol order-response codes, but no UBL document is produced.
- Nutrition rules
- Allergens and claims are checked; nutrition values are not.
From demo to production
- Real order entry. Replace the simulated order with the ERP's order entry and return its order number in the signed receipt.
src/lib/orders/confirm.ts
- OAuth 2.1 per agent. Scoped tokens (catalog:read, quote:create, order:place) in place of demo tokens. The scopes are already declared in the OpenAPI document and the protected-resource metadata.
src/lib/discovery/documents.tssrc/lib/openapi/registry.ts
- Key custody per party. The wholesaler runs Laituri with the supplier key only; the buyer signs mandates in its own system; the approver signs with a personal credential.
src/lib/trust/keys.ts
- Buyer identity. A verifiable organisation credential (the planned EU Business Wallet) instead of a demo key.
- Mandate copy at the wholesaler. Keep the signed mandate in the customer master as well, so both sides hold the same signed copy.
src/lib/mandate/index.ts
- Database. Transactions and row locks in place of the single-process store.
src/lib/store/types.ts
- External audit anchor. Anchor audit checkpoints in an external transparency service (SCITT), so completeness no longer depends on the verifier's memory.
src/lib/audit/checkpoint.ts
- Peppol ordering. Order and order-response documents for customers that already trade over Peppol; the response codes are already in the receipt.
src/lib/orders/receipt.ts
Open it yourself
- Guided walkthroughPlays the order, approval, exception and log steps in the running product
- Buyer sideToday, orders, mandate and log for the buyer's purchasing lead
- Valio Aimo sideConnected agents, demand, unmet requests, audit
- Connect an agentMCP and REST set-up for common agent applications
- OpenAPI 3.1Every REST operation, with schemas and examples
- llms.txtPlain-text guide an agent can follow to buy
- JWKSPublic keys of the buyer, supplier and approver roles
- Audit verificationLive check of the hash chain and every signature
- Evidence as JSONThis page's data, machine-readable