{"name":"Sats 'n Shake","tagline":"Famous for Satburgers since 402.","kind":"l402-store-manifest","version":"2","mode":"sim","room":"shake","teaches":"Online ordering with Bitcoin at checkout: one basket, one invoice. Ordered is not paid, paid is not ready, ready is not picked up.","menu_url":"https://l402lab.com/api/shake/menu","locations_url":"https://l402lab.com/api/shake/locations","auth":{"scheme":"L402","challenge":"HTTP 402 + WWW-Authenticate: L402 macaroon=\"…\", invoice=\"…\" (the same values are in the JSON body under l402)","credential":"Authorization: L402 <macaroon>:<preimage>","session_header":"X-Lab-Session"},"handoff":{"rule":"A basket belongs to one location, one order type, and one time. Set them when you create the cart (POST /api/shake/cart) and change them with PATCH; any change voids a held price.","locations":[{"id":"route-402","name":"Route 402 Drive-In","hours":{"open":"00:00","close":"24:00","label":"Open 24 hours"},"types":["counter","curbside","drive-thru"],"stalls":8},{"id":"midtown-mempool","name":"Midtown Mempool","hours":{"open":"10:30","close":"22:00","label":"10:30 am to 10 pm"},"types":["counter","curbside"],"stalls":6},{"id":"block-height-plaza","name":"Block Height Plaza","hours":{"open":"11:00","close":"21:00","label":"11 am to 9 pm"},"types":["counter","drive-thru"],"stalls":0}],"order_types":[{"id":"counter","label":"Pickup at the counter","available":true,"how":"Walk in and show your code at the counter."},{"id":"curbside","label":"Curbside","available":true,"how":"Park, tap I'm here with your code, and a car hop brings it out."},{"id":"drive-thru","label":"Drive-thru lane","available":true,"how":"Use the pickup lane and read your code at the window."},{"id":"delivery","label":"Delivery","available":false,"how":"Coming never. This drive-in does not deliver, and delivery is out of scope for this room."}],"when":"\"asap\" (the location must be open now) or an ISO time at least 15 minutes and at most 2 days ahead, inside the location's hours (server-local clock). GET /api/shake/locations lists the next slots.","car_colors":[{"id":"red","label":"Red"},{"id":"cream","label":"Cream"},{"id":"black","label":"Black"},{"id":"silver","label":"Silver"},{"id":"blue","label":"Blue"},{"id":"turquoise","label":"Turquoise (a '57)"}]},"payment":{"route":"POST /api/shake/orders","amount":"exactly the frozen quote's totalSats, in one invoice for the whole basket","bound_to":"resource /shake/orders/<cartId>; a credential for one basket cannot pay another","expires":"with the price hold (10 minutes after GET /api/shake/cart/:cartId froze it)","checked_first":"cartId, name, phone (555-01xx or none), car (curbside), a live price hold, the location still open (ASAP) or the time still ahead (scheduled); refusals before the gate cost nothing and mint no invoice","sim_pay":"/api/wallet/pay { invoice } returns { preimage } (pretend sats)"},"idempotency":{"rule":"One cart, one order. Repeating POST /api/shake/orders for the same cart with the original credential returns the same order and the same pickup code; the same cart unpaid returns the same invoice while it is payable; without the credential a paid cart answers 409 cart_already_ordered.","key":"the cartId (and the quote digest it was frozen with)"},"statuses":["paid","cooking","ready","picked_up"],"status_labels":{"paid":"Received","cooking":"In the kitchen","ready":"Ready","picked_up":"Picked up"},"two_acknowledgements":{"kitchen":"ready: the kitchen says the order is up (the first acknowledgement)","customer":"the guest's pickup code: at the counter or the drive-thru window (POST /orders/:number/pickup), or curbside \"I'm here\" (POST /orders/:number/arrive) (the second acknowledgement)","rule":"Paid is neither. A payment proves the invoice settled; the tracker shows the rest."},"pickup":{"code":"SHK-XXXX, returned once with the paid order. Single use. A wrong code is refused (403) and logged without the code; five wrong tries lock the order (ask at the window).","not_a_receipt":"The code gets the bag. The preimage proves the payment. Different things.","curbside":"Check in with the code when you park. Early is fine: the bag comes out the moment it is ready."},"kitchen":{"pace_ms":2500,"stages":"received (paid) → in the kitchen after one pace tick → ready after three; a scheduled order is fired three ticks before its time","note":"the kitchen is a timer; nobody cooks"},"happy_hour":{"window":"14:00-16:00 server time","shake_sats":201,"note":"The live menu changes on its own. A held price keeps until it expires. Prices follow the time you order, not the pickup time."},"combo":{"rule":"each burger + Shoestring Fries + shake trio in one basket","discount_sats":50},"for_agents":{"ordering_for_people":"Confirm the order and the total with your person before you pay. Put their name on the bag, not yours.","budget":"Carry a budget. Hold the price on every basket first, add the totals, and stop before the budget, not after it.","check_the_402":"Pay only when the 402 asks for exactly the quoted total.","codes":"Hand the pickup codes to your person. Do not post them in logs or chat.","where_and_when":"Ask your person where, how and when before you build the basket. Curbside needs their car colour.","lab_agent":"POST /api/shake/agent runs the Lab's own car hop: it builds four lunches under 3,000 sats and waits for POST /api/shake/agent/:runId/confirm before paying."},"endpoints":[{"method":"GET","path":"/api/shake/manifest","gate":null,"returns":"this document"},{"method":"GET","path":"/api/shake/locations","gate":null,"returns":"{ locations:[{ id, name, where, hours, types, stalls, openNow, slots:[ISO] }], orderTypes, carColors, leadMinutes, serverTime }"},{"method":"GET","path":"/api/shake/menu","gate":null,"returns":"{ categories, items:[{ id, name, kind, category, sats, regularSats, happyHourSats?, flavors?, modifiers }], modifiers, combo, happyHour, locations, orderTypes, carColors, leadMinutes, quoteMinutes, payWith, parody }"},{"method":"POST","path":"/api/shake/cart","gate":null,"body":"{ location:'route-402'|'midtown-mempool'|'block-height-plaza', orderType?:'counter'|'curbside'|'drive-thru' (default counter; 'delivery' is refused), when?:'asap'|ISO time }","returns":"201 { cartId, handoff:{ location, orderType, when }, lines:[], runningSats:0, frozen:false } | 400 location_required, unknown_location, unknown_order_type, delivery_not_offered, type_not_here, bad_when, when_too_soon, when_too_far, when_outside_hours | 409 closed_now"},{"method":"PATCH","path":"/api/shake/cart/:cartId","gate":null,"body":"any of { location, orderType, when }","returns":"the running basket with the new handoff (voids a held price)"},{"method":"POST","path":"/api/shake/cart/:cartId/items","gate":null,"body":"{ item, qty?:1-9, flavor? (shakes), modifiers?:['no-onion'|'extra-pickle'|'well-done'] }","returns":"{ cartId, handoff, lines, discounts, runningSats, frozen:false } (any change voids a held price)"},{"method":"DELETE","path":"/api/shake/cart/:cartId/items/:lineId","gate":null,"returns":"the running basket"},{"method":"GET","path":"/api/shake/cart/:cartId","gate":null,"returns":"the held price (frozen quote) { cartId, handoff, lines, discounts, totalSats, happyHour, frozen:true, quoteExpiresAt (10 min), quoteDigest (covers the handoff), status:'open'|'expired'|'ordered' }; an expired hold re-freezes at the current menu after the time is checked again"},{"method":"POST","path":"/api/shake/orders","gate":"L402 for exactly the frozen quote's totalSats","body":"{ cartId, name (on the bag, 1-24 chars), phone? ('555-01xx' only; pretend, never texted), car? (curbside only, required there: 'red'|'cream'|'black'|'silver'|'blue'|'turquoise') }","returns":"402 { error, cartId, quoteDigest, totalSats, handoff, l402 }, then 200 { orderNumber, pickupCode, status:'paid', totalSats, name, car, location, orderType, when, how, readyBy, lines, board, receipt:{ paymentHash }, statusUrl, pickupUrl | checkInUrl }"},{"method":"GET","path":"/api/shake/orders/:number","gate":null,"returns":"{ orderNumber, status, statusLabel, lamps, location, orderType, when, totalSats, lines, times, checkIn, acknowledgements:{ kitchen, customer } }; never the name, phone, car, or code"},{"method":"POST","path":"/api/shake/orders/:number/pickup","gate":null,"body":"{ pickupCode:'SHK-XXXX' }","returns":"counter and drive-thru orders: 200 { status:'picked_up', acknowledgements } | 400 pickup_code_required | 403 wrong_code | 409 not_ready | 409 already_picked_up | 409 curbside_checks_in | 429 too_many_tries"},{"method":"POST","path":"/api/shake/orders/:number/arrive","gate":null,"body":"{ pickupCode:'SHK-XXXX', stall?:number }","returns":"curbside \"I'm here\": 200 { status, checkIn, acknowledgements } (picked_up when the food is ready; otherwise it comes out the moment it is) | 400 pickup_code_required | 400 bad_stall | 403 wrong_code | 409 not_curbside | 409 already_checked_in | 409 already_picked_up | 429 too_many_tries"},{"method":"GET","path":"/api/shake/board","gate":null,"returns":"{ version, updatedAt, orders:[{ number, status, lamps:{ paid, cooking, ready, picked_up } }] }"},{"method":"GET","path":"/api/shake/board/stream","gate":null,"returns":"text/event-stream; event: board, data: the whole board (numbers and lamps only)"},{"method":"POST","path":"/api/shake/clock","gate":null,"body":"{ happyHour: true|false|null }","returns":"the menu, with this session's clock wound (a Lab demo control; null follows the server clock)"},{"method":"POST","path":"/api/shake/agent","gate":null,"body":"{ budgetSats?:3000, people?:4, name?:'Office', location?:'route-402', orderType?:'counter', when?:'asap', car? }","returns":"202 { runId, events }: the Lab's own car hop orders lunch and asks the person who sent it before paying"},{"method":"GET","path":"/api/shake/agent/:runId","gate":null,"returns":"{ runId, state:'building'|'waiting'|'paying'|'watching'|'done', events }"},{"method":"GET","path":"/api/shake/agent/:runId/events","gate":null,"returns":"text/event-stream of the car hop's radio"},{"method":"POST","path":"/api/shake/agent/:runId/confirm","gate":"same X-Lab-Session that started the run","body":"{ ok: true|false }","returns":"{ runId, state }"},{"method":"GET","path":"/api/shake/agent/:runId/bags","gate":"same X-Lab-Session that started the run","returns":"{ bags:[{ orderNumber, pickupCode, name, lunch, totalSats, location, orderType, when }] }"}],"flow":["GET /api/shake/locations (or /api/shake/menu): pick a location, an order type, and ASAP or a slot","POST /api/shake/cart { location, orderType, when } → cartId","POST /api/shake/cart/:cartId/items { item, qty, flavor?, modifiers? } (repeat)","GET /api/shake/cart/:cartId → held price: totalSats, quoteExpiresAt, quoteDigest","STOP: confirm the basket, the place, the time and the total with your person","POST /api/shake/orders { cartId, name, phone?, car? } → 402 for exactly totalSats","pay the invoice (sim: POST /api/wallet/pay { invoice })","POST /api/shake/orders again with Authorization: L402 <macaroon>:<preimage> → 200 orderNumber + pickupCode","watch GET /api/shake/board/stream (or GET /api/shake/orders/:number) until ready","the person uses the code: POST /api/shake/orders/:number/pickup { pickupCode } at the counter or drive-thru, or POST /api/shake/orders/:number/arrive { pickupCode, stall? } at the curb"],"errors":{"shape":"{ error, message }","codes":["unknown_item","unknown_flavor","flavor_required","unknown_modifier","bad_qty","tray_full","no_such_cart","no_such_line","cart_empty","cart_ordered","quote_required","quote_expired","location_required","unknown_location","unknown_order_type","delivery_not_offered","type_not_here","closed_now","bad_when","when_too_soon","when_too_far","when_outside_hours","session_required","when_passed","name_required","phone_pretend_only","car_required","unknown_car","cart_already_ordered","no_such_order","pickup_code_required","wrong_code","too_many_tries","not_ready","already_picked_up","curbside_checks_in","not_curbside","already_checked_in","bad_stall","bad_budget","bad_people","no_such_run","not_waiting","rate_limited"]},"out_of_scope":["real menu data","real prices","any store integration","delivery (coming never)","tips","refunds (the sim says: ask at the window)","real phone numbers (555-01xx only)"],"proves":"Settlement of one invoice for one basket at its frozen total.","does_not_prove":["who ordered","that the food is ready","that the bag was picked up","that the fries were hot"],"honesty":"Ordered is not paid. Paid is not ready. Ready is not picked up. A pickup code gets the bag; the preimage proves the payment.","parody":"Sats 'n Shake is a parody. Not affiliated with any restaurant that sounds like it. No food was harmed."}