# AimoPilot

> AimoPilot is a wholesaler's open ordering gateway for AI purchasing agents. Your agent carries a mandate its organisation signed (what it may buy, how much it may spend alone, who signs off above that). It orders through MCP or REST; plain code checks every order against the mandate, and anything beyond it waits for a human sign-off on their phone.

It is not a chatbot and not a marketplace. One wholesaler, one catalogue, signed quotes at fixed prices, signed order receipts.

## Connect

- [MCP endpoint](https://0.0.0.0:8080/api/mcp): Streamable HTTP, protocol 2026-07-28 and 2025-era clients (2025-11-25 and earlier). Listing tools needs no credentials.
- [OpenAPI 3.1](https://0.0.0.0:8080/api/v1/openapi.json): the same operations over REST.
- [API catalog](https://0.0.0.0:8080/.well-known/api-catalog): RFC 9727 entry point.
- [Manifest](https://0.0.0.0:8080/.well-known/dock.json): auth, mandate format, keys, limits.
- [Public keys](https://0.0.0.0:8080/.well-known/jwks.json): JWK Set (Ed25519) to verify quotes and order receipts offline.
- [Developer guide](https://0.0.0.0:8080/docs/raw/connect-an-agent.md) and [assumptions and next steps](https://0.0.0.0:8080/docs/raw/assumptions.md), as Markdown; [every document](https://0.0.0.0:8080/docs) as pages.
- For people: [what AimoPilot is](https://0.0.0.0:8080/) (the landing page), [the wall](https://0.0.0.0:8080/wall) (live requests at the gate, and the code to become an approver).

## Authenticate

Send two headers on every request, MCP or REST:

    Authorization: Bearer <mandate JWS>
    Dock-Agent-Id: <agent id the mandate names>

The mandate is a JWT (typ mandate+jwt, aud dock) signed with your organisation's Ed25519 key. There is no OAuth and no AimoPilot account. A bad, expired or mismatched mandate is refused with the rule that failed (mandate_signature, mandate_window, mandate_agent); do not retry with the same mandate.

Claude Code:

    claude mcp add --transport http dock https://0.0.0.0:8080/api/mcp \
      --header "Authorization: Bearer $DOCK_MANDATE" --header "Dock-Agent-Id: $DOCK_AGENT_ID"

## Tools

- `read_mandate`: Read my mandate.
- `submit_need`: Start an order from the buyer's need.
- `respond_to_concerns`: Pass on the buyer's answer to a mistake check.
- `answer_questions`: Pass on the buyer's answers to AimoPilot's questions.
- `choose_option`: Choose an option and get its fixed-price quote.
- `get_intake_step`: Read where an order stands.
- `read_kitchen_memory`: Read what AimoPilot remembers about this kitchen.
- `place_order`: Place an order against a quote.
- `get_order`: Read an order.
- `list_orders`: List this organisation's orders.
- `withdraw_order`: Take back an order.
- `explain`: Explain what happened and why.
- `get_order_timeline`: An order's whole timeline.
- `find_products`: Look up products, prices and availability.
- `find_alternatives`: Rank alternatives: good, better, best.
- `open_request`: Open a purchase request.
- `search_products`: Search product master data.
- `check_availability`: Live price and availability for a short list.
- `request_quote`: Get a fixed-price quote.
- `list_offers`: List the wholesaler's offers to this kitchen.
- `accept_offer`: Accept an offer: start an ordinary order.
- `decline_offer`: Decline an offer.
- `mute_offers`: Stop a kind of offer, or every offer.
- `watch_orders`: What may not arrive, and how recurring orders drift.
- `answer_watch`: Answer a watch finding or a held renewal.
- `set_auto_renew`: Switch auto-renew for a recurring order.
- `line_up_backup`: Line up a backup for a product with a poor delivery record.
- `menu_ideas`: Dish ideas this kitchen can buy.
- `plan_dish`: Plan a dish: on hand, to order, cannot get.
- `plan_seasonal_menu`: Plan a menu around what is in season.
- `plan_event_menu`: Plan a booking at short notice.
- `order_menu`: Order a planned menu.

## How an order goes

Start every order with `submit_need`: the buyer's need as fields (what, how much exactly as they said it, by when, must-haves). AimoPilot answers with a step; its `state` says what happens next and `nextStep` names the exact call.

1. `needs_confirmation`: Dock found a likely mistake, for example 1,000 L where this kitchen usually orders 100 L. Ask the buyer and pass their answer to `respond_to_concerns` ("confirm" or a correction id).
2. `needs_input`: at most two typed questions its memory cannot answer. Ask the buyer and pass their answers to `answer_questions`.
3. `options_ready`: option cards with pros, cons and anything that needs a sign-off. `choose_option` with the buyer's pick returns a signed fixed-price quote.
4. `quoted`: call `place_order` with just the requestId (`step.next.example`). It commits, or waits for a human sign-off on the approver's phone.
5. `no_options` or `cancelled`: tell the buyer; do not order.

**The buyer answers, not you.** A mistake confirmation and a clarifying answer must come from the human buyer: relay Dock's question in plain words and pass on exactly what they say. Never confirm or answer for them. These two tools are marked `_meta["dock/humanInput"]` in tools/list. If your client supports MCP elicitation (2026-07-28), Dock asks the buyer directly with an `input_required` form and your arguments only pre-fill it. While a confirmation is pending, quotes and orders for that request are refused (rule human_input).

**A relayed answer is not a person's.** When your client cannot show Dock's form, the answer you pass on is recorded as relayed by the agent, not verified. Dock gives you `answerUrl` (a one-time page where the buyer taps the answer themselves); hand it over instead of relaying when you can. Where a relayed confirmation matters (an unusual amount, a rule broken), the order still goes to a named person's sign-off.

## Look before you order

- `find_products` (REST POST /lookup): products, prices and availability in one call. Filters: words, category, brand, requiredClaims (lactose_free, organic, gluten_free, ...), forbiddenAllergens, packSize, deliverOn, quantity. Words go through the same product search the wholesaler's own store agent uses; each product says `match`: exact, variant, stand_in or not_the_kind, and `search.notSold` says when the wholesaler does not sell that kind at all. Each product carries pack, claims, labels, allergens (declared or not), origin, delivery reliability, carbon (product-group average) and, live for at most 10, price per pack and per litre or kilo, stock (or "not reported"), earliest delivery, order-by cut-off and whether it meets the day.
- `find_alternatives` (REST POST /alternatives): when a product is short, late or not wanted, what can stand in inside the mandate, ranked best, better, good, each with trade-offs and what the gate would decide.
- Several products in one order: `request_quote` with several lines (substituteFor on a substitute), then `place_order` with just the requestId.

## After the order

- `list_orders` (GET /orders): what is committed and what waits for a person's sign-off, and what can still be taken back.
- `get_order` with `waitSeconds` (GET /orders/{id}?wait=25): wait for the person's decision on their phone; it only reads.
- `withdraw_order` (POST /orders/{id}/withdraw): take back an order waiting for sign-off, or one you placed alone, until the wholesaler's order cut-off. What a person signed is theirs to take back.
- `place_order` with `replaces`: change an order (another amount, day or product); the old one is taken back once the new one is placed.
- `explain` (GET /ledger): the audit trail in plain words, hash-chained; the buyer's side and the wholesaler read the same record.

Lower-level tools: open_request, search_products (free master data), check_availability (live, at most 10 products per call). Approving an order is never a tool: only the approver can, with their passkey.

## Your mandate can change under you

- A kitchen that tightens its rules (a forbidden allergen, a required claim) reissues its mandate: every earlier mandate is refused from that second (rule mandate_window). Ask your organisation for the current one; read_mandate shows what you present.
- When the wholesaler enforces crash-test licences, the gate applies the stricter of your mandate and the licence your agent earned on the public crash test. An agent with no licence bound to it is treated as unlicensed: every order goes to a person. The answer says so when the licence, not the mandate, sent an order to a person.
- A kitchen's connector address (for assistants that take only a URL) is the same door with the same mandate; it can be revoked at any time (HTTP 404, never 401).

## Rules an agent must follow

- Quantities are packs (see packSize); money is integer euro cents ex VAT. A quote's totalCents adds returnable-packaging deposits (lines[].deposit, outside VAT; amounts at the Finnish deposit system's published rates, since the price service states none) to the goods, and the mandate's limits compare that total. Each line states its VAT rate (taxRatePct, from the price service, else the product data's VAT class) and quote.vat adds VAT up by rate. listUnitPriceCents is the list price when the kitchen's own price is lower. Products carry nutrition per 100 g or ml; a mandate's nutritionRules are checked by the gate (rule nutrition_rules), and a product that does not declare the figure is refused.
- Shortlist with search_products before asking live prices. Never scan the catalogue: live lookups are capped per call and per agent per hour, and refused with rule live_lookup_limit.
- If the preferred product is short, quote what it has and add a line with substituteFor set to its productId.
- place_order has three outcomes: committed (with a signed confirmation), pending_approval (a person signs off; do NOT retry, follow up with get_order), or refused (error.rule names the rule).
- Every refusal is { error: { code, rule, message, details } } plus, over MCP, nextStep: one sentence on what to do.
- A quote buys once. The weekly budget is a hard limit no sign-off lifts.

## REST in one minute

    curl -s -X POST https://0.0.0.0:8080/api/v1/intake \
      -H "Authorization: Bearer $DOCK_MANDATE" -H "Dock-Agent-Id: $DOCK_AGENT_ID" -H "content-type: application/json" \
      -d '{"need":{"item":{"text":"lactose-free milk","category":"milk"},"quantity":{"amount":1000,"unit":"L"},"deliverBy":"2026-11-16"}}'

The reply is { step } with `step.next`: the method, path and an example body of the next call (POST /intake/{id}/concerns, /answers, /choice, then POST /orders: 201 committed, 202 awaiting sign-off, 422 refused). Lower level: POST /requests, GET /products, POST /availability, POST /quotes.

## Verify a receipt

An order's `confirmation` is a compact JWS (typ order+jws). Fetch https://0.0.0.0:8080/.well-known/jwks.json, pick the key whose kid matches the JWS header, verify with Ed25519. Any JOSE library does this; nothing AimoPilot-specific is needed.
