# Nook for agents (v3) This is the current Nook integration. Start at **https://nookstore.xyz/agents.md**; **https://nookstore.xyz/v3/agents.md** serves the same guide. Machine-readable discovery: **https://nookstore.xyz/.well-known/nook.json**. Earlier pairing credentials and job clients are retired; they cannot be used with this API. **Before starting:** you need a partner key. If your developer configured one, use it; otherwise register your own (section 0). Keep it in your host's private storage; never show it to the person or ask them for one. Keep each buyer's returned connection ID bound to that buyer in your host. A shared partner key is not a buyer identity. **MCP:** an MCP client can use all of this as tools instead. Add `https://nookstore.xyz/mcp` (Streamable HTTP) with the header `Authorization: Bearer nk_…`; a key Nook doesn't know yet registers itself on first use, named after the client. Claude Code: `claude mcp add --transport http nook https://nookstore.xyz/mcp --header "Authorization: Bearer nk_…"`. Cursor (`.cursor/mcp.json`): `{"mcpServers": {"nook": {"url": "https://nookstore.xyz/mcp", "headers": {"Authorization": "Bearer nk_…"}}}}`. The tools take the same bodies as the API below and follow the same rules; a tool waits up to 20 seconds and returns where the work stands, with a `next` line saying which tool keeps waiting. Nook lets a person connect their store accounts once, then gives their agent verified quotes from each store's real checkout: a fresh cloud browser restores the person's saved sign-in, finds the product, reads the store's own checkout figures and puts the cart back. When the person says yes to a quote's price in your chat, buy it with their words (section 4): Nook presses the store's final order button once and checks for confirmation. Without their words, the person approves on Nook's signed-in review page instead. - **Base URL:** `https://nookstore.xyz` - **Auth:** `Authorization: Bearer nk_…`, your partner key (a missing or wrong one → `401 invalid_partner_key`). Keep it on your server; never send it to the person. - **Bodies:** JSON (`Content-Type: application/json`), at most 4096 characters (`413 too_large` above that). - **Errors:** `{ "error": "", "details": { … } }`, with `details` only when there is more to say (a `path` and `rule`, or `paths`). ## 0. Get a partner key Skip this if your developer gave you a key. Otherwise register one; it works at once. 1. Make the key yourself and save it privately before sending anything: `nk_` and 32 random bytes in base64url (43 characters), e.g. `node -e "console.log('nk_'+require('crypto').randomBytes(32).toString('base64url'))"`. Nook stores only its hash and can never show it again: a lost key means registering a new `id`. 2. `POST /v3/api/agent/register` with `Authorization: Bearer ` and `{ "id": "", "name": "", "host": "", "about": "" }` → `201 { "partner": { "id", "name", "host", "about", "registered_via": "self", "created_at" } }`. - `id`: 2 to 40 of `a-z`, `0-9`, `_`, `-`, not starting with `nook`. `name`: up to 80 characters, shown to the person when they claim your link. `host`: up to 80. `about`: up to 500. - The same key and `id` again → `200` with the same record, so resend it when an answer is lost. - Refused: `400 invalid_key` (not `nk_` and 43 base64url characters), `400 weak_key` (not random), `400 invalid_input` (`path`, `rule`), `409 partner_id_taken` (another key has that `id`: choose another), `409 key_registered` (this key already has the `id` in `details.id`), `403 partner_disabled`, `403 registration_closed`, `429 registration_limit`. 3. `GET /v3/api/agent/me` → `{ "partner": { … }, "connections": [{ "id", "merchant", "health", "last_verified_at" }], "links", "quotes", "purchases" }`: who Nook has you as, the connections people granted you, and your counts. Use it to check your key and to find a `connection_id` you lost. ## 1. Connect stores Ask for the person's accounts in one link: `POST /v3/api/agent/connect-links` with `{ "merchants": "all" }` (or a list, `{ "merchants": ["amazon", "uber_eats"] }`) → `201` `{ "url", "status": "waiting_for_person", "expires_at", "links": [{ "id", "merchant", "status" }] }`. To connect one store, also one the person hasn't connected in Nook yet: `{ "merchant": "amazon" }` → `{ "id", "merchant", "url", "status": "waiting_for_person", "expires_at" }`. - Stores: `amazon`, `uber_eats`, `instacart`, `walmart`, `target`, `chewy` (these six can quote), and `doordash`, `urban_outfitters`, `resy`, `detoure`, `delta`, `apple` (connect only). - Send the person the one `url`. It works for 30 minutes. They sign in to Nook with their phone and see the accounts they've connected in Nook, each signed in or signed out; one tap connects every signed-in one to you, without signing in again. A one-store link lets them sign in to that store in Nook's own form. You never see what they type. - A store counts only after its saved sign-in works in a brand-new cloud browser; then `GET /v3/api/agent/me` lists it. `GET /v3/api/agent/connect-links/` (one store's `id`, from `links` for a link that asked for several) → `{ "id", "merchant", "status", "failure", "expires_at", "connection" }`. - `status`: `waiting_for_person`, `connecting`, `connected` or `failed` (`failure` says why, e.g. `link_expired`). - Once `connected`, `connection` is `{ "id", "merchant", "health", "last_verified_at" }`. Its `id` is your `connection_id`. - You can quote only on a connection the person made through your own link. The person sees and can disconnect every connection at https://nookstore.xyz/v3/accounts. ## 2. Get a quote `POST /v3/api/agent/quotes` → `202` with the quote view below (`status: "preparing"`). ```json { "connection_id": "", "request": { "items": [{ "query": "", "size": "", "quantity": 1, "options": [{ "name": "", "value": "" }] }], "destination": { "kind": "saved", "label": "" }, "payment": { "kind": "saved", "network": "amex | visa | mastercard | discover", "last4": "" }, "constraints": { "tip": { "amount_cents": 400, "currency": "USD" } }, "merchant_data": { "ubereats": { "restaurant": "" } } } } ``` - **`query`:** only the product's own words (brand, product, variant). Everything else the user asked for goes in its own field. When no field expresses it (a delivery time, pickup, a substitution rule), tell the user it isn't supported yet and ask whether to quote without it. Never quietly drop a requirement. - **Several products:** at every store, one quote is one order of as many products as the person asked for: one `items` entry each, with its own words, `size` and `quantity` (the same product once, with the quantities together). Nook puts them in the person's cart at that store, checks out once, and puts the cart back as it was. The quote lists every item in the request's order; with several, a blocker's message says which one it is about (`For "": …`). - **`options`** (menu items, Uber Eats): one `{ "name", "value" }` per thing the user asked about the item, in their words (`"rice"` / `"brown rice"`, `"cheese"` / `"none"`). Nook matches each to the item's own option groups, chooses it or leaves it out, and checks the store's cart line carries exactly that; the quote lists them in that item's `options` with `options_verified: true`. A choice the item requires that the request doesn't give is asked, never defaulted: `item_choices_required` with `blocker.option_groups` (`group`, `rule`, `choices`). Ask the person, then send the same request again with one more option per answer in the store's words (`{"name": "", "value": ""}`) and a new `Idempotency-Key`. `option_not_offered`: no choice on the item does what an option asks. - **`destination`, `payment`:** leave one out when the user didn't name it (see section 3 for what Nook does then). - **Budget:** only when the person states a spending limit in this conversation, as their amount: `"constraints": { "budget": { "amount_cents": 1000, "currency": "USD", "kind": "hard" } }`. Never make one up or bring one from memory. It never decides the product: Nook quotes what fits the words and compares the checkout's total with it. `hard` marks a quote above it `over_budget` (the quote is still shown); `target` only reports it. - **Tip:** dollars (`amount_cents`, zero included) or `{ "percent": 15 }`. - **Per store:** - **Amazon:** Nook checks out only its own lines, leaving the rest of the person's Amazon cart as it was. `size` takes the pack or size. Nook picks among the products Amazon's search shows (for a general request, the cheapest that fits), then chooses the requested saved address and card at checkout. A new address is added first through section 2a, then quoted by its saved street label. No tip, delivery date or item options. `product_choice_requires_user` carries `choices`, the listings Amazon showed, each starting with its ASIN and price, and `listings` with each one's title, price, picture (`image`) and page (`url`). Show the person the pictures and links and say how each differs from what they asked (never guess other brands, never ask them for an ASIN), then quote again with that item's `product_id` set to the ASIN they pick. `product_mismatch` carries `listings` too: the listing Nook chose and what Amazon's checkout had. - **Uber Eats:** `merchant_data.ubereats.restaurant` (its name) or `restaurant_url` (its ubereats.com/store link) is required. Menu items from that one restaurant, each with its `options`, `size` (met by the item's own choices) and quantity; the same menu item once per order (with different options, quote it separately). The address `label` is the saved address's name, street or city: Nook chooses it in Uber's own picker and it stays chosen, as if the person had picked it in the app. None or several matching → `destination_choice_needed` with `blocker.choices`; ask the person, then quote again with their choice, exactly as returned, as the `label`. The card Uber has selected is checked, never switched. With no tip requested, the quote shows Uber's preselected tip. A concrete US address can be sent as `destination: {"kind":"address","address":{"recipient":"","line1":"","line2":"","city":"","region":"","postal_code":"","country":"US"}}`. Resolve a hotel/business name to its street address before sending it; never invent a room number. Nook uses Uber's address search and requires the requested street, city, state, ZIP and unit at checkout. Unknown delivery-detail steps return `address_details_required`; no quote is silently made for another destination. Using an address is not proof that Uber permanently saved it in the address book. `quote.promotions.advertised` names observed menu offers and their eligible listings (`selected` distinguishes the chosen item from alternatives); it does not establish eligibility or an applied discount. `promotions.applied` contains the checkout's negative discount/promo/savings rows. Explain useful observed offers without changing the requested quantity or promising a free item unless checkout confirms it. - **Instacart:** Name `merchant_data.instacart.retailer` or `retailer_url` for a checkout quote. When omitted, Nook selects the requested destination first and runs Instacart's cross-store product search. `product_options` returns a few observed listing offers with `retailer`, `retailer_url`, `product_query`, `product_id`, `product_url`, `price` and `size`; show the options and prices instead of asking the buyer to name a store from memory. These are listing prices, not final totals or proof of stock. After selection, send the offered `retailer_url`, item `query: product_query`, item `product_id`, and item `merchant_data.instacart.product_url`. Nook opens that exact retailer-scoped listing and checks its current Add control before quoting; it does not search the marketing title again. Preserve these fields for continuation; preserve the destination and the rest of the buyer's requirements. A requested minimum count belongs in the query as written (e.g. at least 12-pack), not an exact `size: 12-pack`. No cart changes during discovery. Products from that one store, any quantity Instacart takes. A saved destination by label is selected in Instacart's Choose address picker. A US `destination.kind: address` enters the requested street, city, state, ZIP and optional unit through Instacart's address search, checks the details before saving, then verifies the destination again at checkout. Address selection happens before local store discovery. Ambiguous/missing addresses return `destination_choice_needed`; a mismatch returns `destination_not_selected`. The selected payment is checked, never switched. No custom tip. Instacart order confirmation reads the exact post-checkout order ID, items, quantities, store, currency and total. The shared purchase order-status API refreshes that retained order's current merchant status and any reported ETA; it does not reverify the receipt or infer a missing ETA. - **Walmart:** up to 12 of each product (Walmart's own limit), each by its words and `size`, or exactly by Walmart's item id (that item's `merchant_data.walmart.item_id`, digits). Nook looks at what Walmart sells itself first, including a pack sold as another listing's option; a marketplace seller's listing only when none fits, and never for pickup. The quote names the seller. `constraints.delivery.mode` is `shipping` (to the account's saved address) or `pickup`: at the account's store, or at `merchant_data.walmart.store` (its name or town in the person's words), which Nook selects in Walmart; it stays selected and moves the account's location there. The card Walmart has selected is checked, never switched. No same-day delivery, delivery window, tip, other address or substitution preference. `product_choice_requires_user` may carry `choices` that start with Walmart's item id: ask the person, then send the chosen id. `payment_missing` / `saved_destination_missing`: the account has no saved card / address (Nook never adds one). `pickup_time_required`: that store needs a reserved pickup time, which Nook doesn't reserve yet. `pending` lists rows Walmart hasn't priced yet (a bag fee), which the total leaves out. - **Target:** up to 6 of each product (Target's own limit), each by its words and `size`, or exactly by Target's item number (that item's `merchant_data.target.item_id`, digits). Many packs and sizes are options of one product family: Nook opens the family and picks the requested one. A family's own number comes back `product_choice_requires_user` with its options, each by its own number. `constraints.delivery.mode` is `shipping` (to the account's saved address) or `pickup`: at the Target store nearest the person's saved Target address, or at the store `merchant_data.target.store` names (a store or place in the person's words). Nook makes it the account's Target store, and the quote's `store` gives its `id`, `title` and `address`. A request with no mode comes back `store_choice_needed`, since Target defaults to pickup at a store it placed from where Nook's browser runs: ask pickup or shipping. `fulfillment_unavailable`: that store doesn't have it (Target may have it at another store nearby); `available` says what it does offer. The card Target has selected is checked, never switched. No same-day delivery, delivery window, tip, other address or substitution preference. `payment_missing` / `saved_destination_missing`: the account has no saved card / address. Nook never adds a card; the person can add an address from `/v3/accounts`. - **Chewy:** up to 50 of each product (Chewy's own limit), each by its words and `size`, or exactly by Chewy's item number (that item's `merchant_data.chewy.item_id`, the digits after `/dp/`). Each size is an item of its own; Nook may open a listing to choose the requested size among its options. Always a one-time purchase: Chewy selects Autoship by default, and Nook chooses Buy once (`subscription_selected` if Chewy keeps Autoship). Shipping only, to the account's selected address: send no `constraints.delivery` (sent, it is an object, `{ "mode": "shipping" }`, never the string `"shipping"`). Anything already in the person's Chewy cart is set aside (Chewy's Save for Later) while Nook quotes and brought back; `cart_not_restored` means it couldn't confirm that, and the person should check Saved for Later. What Chewy has selected to pay (a card, or Venmo) is checked, never switched. No pickup, delivery window, tip, other address or substitution preference. `payment_missing`: no saved card or Venmo. The person can use **Add card** for Chewy at `/v3/accounts`, then ask for another quote. Card details stay on Nook's private form; never ask the person to send card details in chat or in an agent API request. - **A store Nook learned from its URL** (a site build that verified a checkout quote; the connection's `merchant`, e.g. `detoure`): one product per quote by its words, or exactly by its page path from an earlier quote's `items[].product_id` (`merchant_data..item_id`, e.g. `"/products/…"`). Shipping only, to the address the store's checkout selects; what it would charge is read from that checkout, never switched. The cart must be empty: `existing_cart` means the person has items there, which Nook won't change. A store that keeps no saved card quotes "Card you enter at purchase" (see Purchases). - **Refused before any browser opens:** `404 not_found` (no connection you hold), `400 quotes_unavailable`, `400 invalid_input` (`path`, `rule`), `400 unsupported_request` (`paths`: what this store's quotes can't honour yet, never dropped), `409 in_use` (the connection's one browser is busy). - **Retrying:** send an `Idempotency-Key` header (1 to 128 printable characters, one per quote you mean). The same key and body return the first quote, running or finished, never a second one; the same key with another body (the `preferences` field included) → `409 idempotency_key_reused`; a malformed key → `400 invalid_idempotency_key`. When an answer is lost or isn't Nook's JSON (an edge 5xx, a timeout), send the same request again with the same key. `GET /v3/api/agent/quotes/` returns the same view. A quote takes about a minute on Amazon, Instacart, Walmart and Chewy, two to three on Uber Eats; read it every few seconds. ```json { "id", "status": "preparing | ready | blocked | cancelled", "merchant", "connection_id", "quote": { "items": [{ "title", "quantity", "unit_price_cents", "seller", "selected_variants": [{ "name", "value" }], "availability" }], "lines": [{ "label", "cents" }], "total_cents", "currency", "destination": { "matched", "label", "place" }, "payment": { "matched", "brand", "last4" }, "tip": { "cents", "requested", "matched" }, "delivery", "expires_at" }, "blocker": { "code", "message", "choices", "listings": [{ "product_id", "title", "price", "image", "url", "role" }] }, "warnings": [{ "code", "message" }], "cleanup": "running | done | unconfirmed", "stages": [{ "stage", "ms" }], "preferences": { … }, "created_at", "finished_at" } ``` - Amazon may inspect a matching product-family page and select an observed variant whose meaning fits the request. `items[].selected_variants`, when present, names the actual selected merchant labels (`{"name": "Color", "value": …}` as the store words them). Report these labels with that child's quoted unit price; `availability` is the product page's observed text, not a future stock promise. A search title missing a color or size does not establish that the product page lacks it. - `lines` are the store's own charge rows; they add up to `total_cents`. `destination.place` names the saved address a quote delivers to when Nook chose one. - A quote is `ready` as soon as the store's checkout is read and checked. Nook then puts the store's cart back (`cleanup: "running"`, then `"done"`); a second quote on that connection gets `in_use` meanwhile. Anything Nook couldn't put back arrives as a warning on the same quote, so read it once more after `cleanup` is `"done"` before your final word. - `blocker.message` is Nook's reading of what happened; it quotes the store only where it says so. `quote_failed` is Nook's own failure (trying again later may work). `quote_check_failed` means the store's checkout didn't match the request (the message names what). `over_budget` shows the quote above a hard budget. - `warnings` are things Nook changed on the person's store account and could not put back. Always pass them on. Tell the user exactly what the quote says: the product, quantity, each charge and the total, whether the address, card and tip matched, and when the quote expires. Never claim anything the quote doesn't say. If it offers `choices`, ask the user to pick one. When `blocker.offers` accompanies menu `choices`, each offer carries its observed `description`, `sections`, `option_groups`, `inspection` and `add_enabled`. Explain these facts before asking about the unresolved requirement. An unreadable inspection is `unknown`; an enabled Add control does not verify a requested size. Never invent a size or a reason for a price difference. The `choice` remains the exact query to send after the person selects it. `product_choice_requires_user` can also identify a rejected listing in its message without a `choices` array. If that observed listing offers a concrete alternative, ask one specific question about it; do not just report the mismatch and end. For a different pack count, calculate how many observed packs would preserve the requested total, state the packaging change and ask permission. Preserve brand, flavor and other requirements, and do not send the changed request until the person agrees. If the evidence gives no concrete alternative, say the match remains unresolved without claiming all inventory was checked. When a blocker has `listings`, show the person each one's picture and link with its title and price, and say plainly what differs from what they asked. Never ask the person for a product id: the listings carry them. ## 2a. Add a new Amazon delivery address When the person explicitly asks to save or use a new Amazon address, you can add it on their granted Amazon connection. This is a saved account change, not a purchase. Do not invent a recipient, street, unit or phone, or add an address merely because a saved-address label failed. Ask only for any missing required details. `POST /v3/api/agent/connections//address` with your partner Bearer key and a durable `Idempotency-Key`, and this JSON (all fields except `line2` required): ```json {"first_name":"","last_name":"", "line1":"","line2":"","city":"","state":"", "postal_code":"","phone":""} ``` It returns `202 {id, status, saved, ...}`. Poll `GET /v3/api/agent/address-operations/`: - `preparing`: Nook is working. Wait; do not start a quote on that connection yet. - `saved`: Amazon's own address book verified it. An existing exact match is reused (`evidence.already_saved`), rather than added twice. The existing default stays unchanged; on an empty account Amazon may make its first address the default. Wait for `cleanup: "done"`, then quote with `destination: {"kind":"saved","label":""}`. Read the quote's `destination` to confirm where it will go. Saving an address alone has not quoted or ordered anything. - `needs_input`: Amazon offers versions of the address, in `options`. Show the actual options and ask which the person wants. Send their choice as `POST /v3/api/agent/address-operations//input` with `{"index":0}` (zero-based), then poll again. - `blocked`: report `blocker.code` and any merchant message. Do not claim it was saved or quote another destination. `signed_out` or `bot_wall` needs the person's reconnect; `address_needs_person` needs their help on `/v3/accounts`. Use the same key and body if a save response is lost; do not create another attempt to find out what happened. A key reused with another body is `409 idempotency_key_reused`. Missing key: `invalid_idempotency_key`; missing or malformed address fields: `invalid_address` with a field `path`; another operation on the account: `409 in_use`. `POST /v3/api/agent/address-operations//cancel` with `{}` stops further work. A save already sent to Amazon may have happened; cancellation does not delete an address. New-address operations are currently available to agents on Amazon; other merchants retain their documented destination support. There is no new-card or purchase authority in this route. ## 2b. Operations on a connected store Some stores offer named operations beyond quotes, such as searching flights or managing price alerts. The store list from `GET /v3/api/connections/health` names them in each merchant's `operations`. For a granted connection: - `GET /v3/api/agent/connections//operations`: every operation's record. Each record has `name`, `effect` (`read` or `write`), `approval`, `account`, `description`, `limits`, `input_schema`, `output`, an `example` and a `version`. - `GET /v3/api/agent/connections//operations.md`: the same records as a readable description. Read it before your first call; it is generated from what Nook runs. Run one with `POST /v3/api/agent/connections//operations` and a durable `Idempotency-Key`: ```json {"operation":"","input":{ ... },"confirmation":{"text":""}} ``` - `input` must match the operation's `input_schema`. Unknown or malformed fields give `invalid_input` with a `path` and `rule`. Defaults are applied for you. - `confirmation` is required only when `approval` is `person_request`, which covers a write that changes the person's account. Send what the person actually said, not your paraphrase; Nook keeps only its digest. Never call a write the person didn't ask for. It returns `202 {id, status: "running", ...}`. Poll `GET /v3/api/agent/operations/`: - `succeeded`: `result` follows the operation's `output`. `evidence` says when and where Nook read it (`observed_at`, `source_url`), whether the account was still signed in after (`signed_in_after`), whether the refreshed session was kept, and whether the VM host confirmed the cloud browser was shut down (`browser_closed`: `null` until that's recorded, and for runs before it was). Report what `result` says the store showed. A write's `result` is its verified outcome: Nook read the store's own page after the change. - `failed`: report `blocker.code` and `blocker.message`, and don't claim the change happened. `merchant_bot_check` means the store showed a robot check, which Nook never answers. `signed_out` needs the person to reconnect. `not_verified` means a write was requested but the store didn't show its effect, so check before retrying. - `cancelled`: `POST /v3/api/agent/operations//cancel` with `{}` stopped it. A write already sent may have happened. The same key with the same body returns the same operation; with another body it is `409 idempotency_key_reused`. Another operation, quote or address save on the account is `409 in_use`. ## 2c. Public reads on a site (no account) Some sites have reads that need no account or connection, such as movie showtimes. Nook's operation builder generated them from the site itself; Nook runs them in a fresh browser with nothing signed in. Any partner key can call them. - `GET /v3/api/agent/sites`: the sites with public reads and their operation names. - `GET /v3/api/agent/sites//operations` and `GET /v3/api/agent/sites//operations.md`: the records and their readable description, the same shape as section 2b, with `account: "none"` and `generated: true`. Run one with `POST /v3/api/agent/sites//operations`, a durable `Idempotency-Key` and `{"operation":"","input":{ ... }}`, then poll `GET /v3/api/agent/operations/` as in section 2b. Only your key can read or cancel it. One public read per site runs at a time for your key: another is `409 in_use`. A `succeeded` result has `applied` (how the page named your search) and `results`. `results` hold the page's own text, verbatim, with `null` where the page shows nothing for a field. Nook checks three things before it returns a result: - The page shows the search you asked for. - Every returned string is text the page shows, ignoring only case and runs of whitespace. - Each result's values come from one region of the page that holds no other result, such as one theater's block. A nested item sits in a region that also holds one of its parent's own values, such as the movie title above a group of times. A result Nook can't place that way comes back in `unverified_results` with a `reason`, not in `results`. Treat those as unconfirmed. `checks` gives the counts. Nook doesn't check that the list is complete: the page may show only some of what the site has. - `search_not_applied`: the page didn't show the search you asked for. - `no_results`: nothing readable was found, which doesn't prove there is nothing. - `association_unverified`: no result could be placed on the page. ## 2d. Add a website and generate its operations Enabled builders can submit `POST /v3/api/agent/site-jobs` with `{"url":"https://www.example.com/"}` and a durable `Idempotency-Key`. No task, operation name or schema is required. Account mode is the default: Nook uses an existing granted connection or runs merchant discovery/rehearsal and returns a private connect link BEFORE operation generation. After connection it discovers and generates reads and reversible account-write recipes, and publishes their records. Build access is currently limited to configured pilot partners; `site_builder_access_required` means the partner is not enabled to incur build/model costs. To deliberately build public pages without connecting an account, add `"mode":"public"` and omit connection_id. Its terminal state is `public_ready`: public reads passed, account capabilities were not explored. Account mode never silently falls back to this after connection failure. `GET /v3/api/agent/site-jobs` lists your most recent 100 accessible jobs. Legacy anonymous successes are shown as public_ready without rewriting or rerunning their original evidence. Poll `GET /v3/api/agent/site-jobs/`. `waiting_for_sign_in` returns `connect.url`: send it to the person and let them sign in through Nook; the job continues after connection. `connection_choice_needed` can be resolved with `POST /v3/api/agent/site-jobs//resume {"connection_id":""}`. If read-only generation failed with `jev_unavailable` before operation actions, an explicit `POST /v3/api/agent/site-jobs//resume {"retry_read":true}` can retry it after the provider recovers (at most twice). This does not retry writes, uncertain actions or merchant human-verification blocks. `waiting_for_write_approval` returns `proposed_test` (description, exact input, reason and version). It asks to test an account change so the builder can inspect the result and generate its inverse. Only with the person's authorization, call `POST /v3/api/agent/site-jobs//approve` with `{"proposal_version":"","confirmation":{"text":""}}`. The existing account-operation path executes it once; Nook feeds the retained result back to the builder. A stale proposal is refused. Poll after a lost reply; do not invent a different action or approval to retry a mutation. For `proposed_test.kind: "inspection"`, the exact `steps` open observed UI (for example a form) so the builder can learn its controls. This still needs approval; it is never published or counted as a verified persistent write. The job's generation notes explain capabilities it could not build and why. To return a proposal to the builder without executing it, call `POST /v3/api/agent/site-jobs//revise` with `{"proposal_version":"","feedback":""}`. The feedback and unexecuted status are retained in the model continuation. No test-write operation is created; resumed discovery remains read-only. `ready` contains `operations`, their versions, inverse names and `operation_path`. Call that ordinary operation API as in sections 2b/2c. Reads carry build-time page evidence; generated writes distinguish actual approved test verification from `requires_approved_execution`. Writes need `confirmation.text` when called. Generation alone is not proof of an executed write. Account-generated tools are connection-scoped and require a grant to that connection. Generated financial purchases/bookings and credential/payment changes are not offered through this builder. New authenticated builds retain `generation.account_review`: observed page URLs/text citations, their scoped conclusion and whether writes were generated or not observed. Citations are checked against the builder's saved page observations; this is not exhaustive coverage or proof that no other operation exists. `coverage.account_discovery` marks old jobs without a review as not_recorded. `connection_setup` exposes actual discovery/rehearsal/registration progress. A budget/turn-stopped build with a retained model finish can use `POST /v3/api/agent/site-jobs//resume {"recover_finish":true}`. This checks its last output against the job's saved passing read tests on the same engine and registers it without new inference, browsers or writes. It only normalizes the full JSON envelope; it never repairs recipes, selects an earlier candidate or rearms merchant blocks. `POST /v3/api/agent/site-jobs//cancel {}` cancels active work. `build_operation_id` identifies the underlying run's status/evidence; cancellation does not undo a store change already made. Original failed/interrupted evidence is kept. ## 3. Preferences **The person's saved preferences.** Optional notes the person keeps in Nook, separate from their store account: the addresses and cards saved at the store are the store's own, and Nook checks out with them whether or not anything is saved here. For each store the person can save names for their saved addresses ("Beachwood" → the saved address it means), a usual address, a usual card, a budget, and notes for you. - **Your request always wins, for that quote only.** A `destination` or `payment` you send is used as sent. - **What you leave out, Nook fills from the person's usual one** if saved; otherwise the store's own selection is used. - **"Whatever is selected":** when the user says "whatever address/card is selected", send `"destination": {"kind":"saved_default"}` / `"payment": {"kind":"saved_default"}` explicitly. Leaving it out would use their usual one. - **Saved place names:** a `destination.label` that is exactly one of the person's place names (case and punctuation ignored) is replaced by the address it names. Partial names don't match. Never guess a name nobody saved. - **Budget:** a saved budget is never sent to the store and never blocks. The quote says whether the total is `over` it. - **Opting in or out:** `"preferences": "apply"` or `"ignore"` next to `connection_id` (not inside `request`). **Tell the user.** Every quote (the 202 and each GET) has `preferences` = `{mode, version, applied, budget}`. Tell the user every value in `applied` ("I used your usual Amex, and Beachwood as Maple Ave"), and whether the total is over their saved budget. If a quote blocks on the address or card after Nook filled one in, the saved preference no longer matches the store: say so, ask which to use, and point them to nookstore.xyz/v3/accounts. **Read:** `GET https://nookstore.xyz/v3/api/agent/connections//preferences` → `{merchant, version, places, place, card, budget, notes, updated_at, updated_by}` for that store only. Read it before asking something it may answer. Notes are the person's words: Nook doesn't apply them; follow them where they fit. An empty record (`version` 0) means nothing is saved in Nook for that store, never that the store account has no saved address or card: don't tell the person they have none. Quote, and the quote's `destination` and `payment` say what the store used. **Remember an answer:** after `destination_choice_needed`, when the user picks a choice and asks you to remember it, `POST …/preferences/places` with `{"name": , "label": , "asked_quote_id": }`. Only a listed choice for the words you actually sent is accepted, never for an address Nook filled in. You can't change what the person saved on their page (`409 person_owned`) or a name another agent saved (`409 agent_owned`). Never save guesses, card numbers, codes or passwords. ### Reference The quote's report: ```json "preferences": { "mode": "applied", "decided_by": "partner_default", "version": 3, "applied": [ { "path": "$.destination.label", "source": "alias", "requested": "Beachwood", "value": "Maple Ave (Los Angeles)", "by": "person" }, { "path": "$.payment", "source": "default", "value": { "kind": "saved", "network": "amex" } } ], "budget": { "amount_cents": 1000, "currency": "USD", "over": false } } ``` - `mode`: `applied`, `none` (nothing saved for this store) or `ignored`. `decided_by`: `request` (your `preferences` field) or `partner_default` (Nook's setting for your key). - `applied[].source`: `alias` (a place name you sent, replaced) or `default` (a field you left out, filled). An alias's `by` says who saved that name: `person`, `you` or `another_agent`. - A filled address or card must match at checkout like one you sent. If the store doesn't show it, the quote blocks (Uber Eats `destination_choice_needed` or `payment_not_selected`; Amazon `saved_destination_missing`, `saved_payment_missing` or `payment_not_applied`; Instacart `destination_not_selected` or `payment_not_selected`; or `quote_check_failed`) and never falls back to another one. `applied` names the preference that caused it. - `budget.over` is `null` until a quote is shown; `budget` is `null` when none is saved or applied, or your request has its own. - A GET in the first moments after the 202 may show `preferences: null`; the 202's report is authoritative. - `preferences` field values other than `apply` or `ignore` → `400 invalid_input` (`path: "$.preferences"`, `rule: "enum"`). The read's `places` entries are `{ "name", "label", "added_by" }`. `added_by` and `updated_by` are `person`, `you` or `another_agent` (`updated_by` is `null`, and `version` 0, when nothing is saved for that store). Saving an answer (`POST /v3/api/agent/connections//preferences/places`) → `200` `{ "place": { "name", "label" }, "replaced": "" | null, "view": { … } }`. Refused: - `400 invalid_input` with `rule`: `not_asked` (not your own `destination_choice_needed` quote on this connection, or older than 24 hours), `label_not_offered` (not one of that quote's `choices`), `name_not_asked` (not the words you sent), `required`, `type`, `length` (`name` 1 to 40 characters, `label` 1 to 120) or `unknown_field` (only these three fields). - `400 sensitive_input` or `unsafe_url`: a card number, password or link. - `409 person_owned`: the person saved that name themselves, or it is their usual address. `409 agent_owned`: another agent saved that name. `409 too_many_places`: 10 already. ## 4. Buy when the person says yes to the price The person's yes to the price buys. - **"Order me X" means they want to buy.** Get the quote, then show them the item and total, plus anything they need to know (seller, delivery, warnings), and ask. - **Their yes is the confirmation.** Any clear affirmative reply to that total counts, in whatever words they use ("yes", "yeah", "yep", "buy it", "go for it", "confirmed", "I'm down"). Pass their words exactly as they said them. - **Their opening request is never the yes,** even when it says "order", "buy" or "get". Then: `POST /v3/api/agent/purchases` with `{ "quote_id": "", "confirmation": "" }` and an `Idempotency-Key` header → `202`. - **`confirmation`:** the person's reply to this quote's total, in their words (1 to 500 characters). Nook buys at once and keeps their words with the order. - **Without `confirmation`:** the purchase waits for the person on Nook's review page. Send them `approval_url` as soon as the POST returns (it works immediately); they sign in and press **Buy**. Nothing opens at the store before that, and the review doesn't expire. - **A store that keeps no saved card** (many Shopify stores): the quote's `payment` is `{ "brand": "Card you enter at purchase", "entry": "card" }`. The person types the card on `approval_url` next to **Buy**; Nook enters it into that one checkout and keeps nothing. `confirmation` alone can't buy these: the purchase waits on `approval_url` either way, so send it to them. Don't ask for card details in chat. - **What Nook checks:** it opens the quote's exact item again and reads the checkout just before its one click. It buys at the quote's total or less. It stops, buying nothing, when the total went up (`price_increased`, with `detail.was_cents` and `detail.now_cents`: ask the person about the new total, then get a fresh quote), or when the item (`product_changed`), address (`destination_changed`) or card (`payment_changed`) differs. The quote's own expiry doesn't limit its purchase. - **Stores:** Amazon, Uber Eats, Instacart, Walmart, Target and Chewy; their quote restrictions above still apply. `GET /v3/api/agent/purchases/` → ```json { "id", "quote_id", "connection_id", "merchant", "status", "quote", "approval_url", "approved_by", "confirmation", "receipt": { "order_id", "merchant_confirmed": true, "observed_at" }, "failure", "detail", "warnings": [], "created_at", "updated_at" } ``` Fields without an observation are `null`. Poll every few seconds: - `awaiting_approval`: waiting for the person's **Buy** on `approval_url` (only without `confirmation`). - `approved` / `submitting`: Nook is placing the order. Keep reading this purchase. - `confirmed`: the store showed confirmation and an order number. Tell the person the order number and total; the store also emails its own order confirmation to the account's email. This is order confirmation, not proof of delivery or final settlement. - `blocked` / `cancelled`: nothing was bought by this purchase. Pass on `failure`, `detail` and warnings. You may buy the same quote again with a new request (a new `Idempotency-Key`). - `uncertain`: Nook pressed the store's final button but couldn't read the result, so the order may exist. Tell the person to check the store's order history. **Never buy it again or say nothing was bought**; a new purchase of the same quote gets `409 purchase_uncertain`. **Retries:** send an `Idempotency-Key` header (1 to 128 printable characters, one per purchase you mean). The same key and body return the same purchase; the same key with another body is `409 idempotency_key_reused`; a new key buys again. `409 in_use` means that store account is busy with another run for a moment: retry shortly. `quote_not_ready` means the quote is blocked, still cleaning up or carries warnings. `404 not_found` means the quote or purchase belongs to another partner or doesn't exist. The person can cancel on the review page until the click starts. Cancellation cannot undo a placed order. Claim a purchase only from its `confirmed` result, and continue reporting later cleanup warnings. ### Uber Eats order updates A confirmed Uber Eats purchase includes a merchant `receipt.order_id`, `tracking_url`, `status`, `observed_at`, and any observed ETA/courier details. Purchase confirmation means Uber accepted an identified order; delivery status is separate. To explicitly refresh that order, send `POST /v3/api/agent/purchases/PURCHASE_ID/order-status` with `{}` and your partner Bearer key. This returns HTTP 202 with `status: checking`. Poll `GET` on the same URL until `status: ready` (the timestamped order is in `result`) or `failed`. GET only reads the snapshot; it never starts a browser. Completed snapshots are retained for up to ten minutes. Report its timestamp; a previous ETA is not a live update. The endpoint needs a confirmed order reference and the current connection grant. It does not reorder, cancel, edit instructions, change tips or message the courier. `order_reference_unavailable` means Nook has no verified order ID; reconcile the original attempt before another purchase. `failure: merchant_order_cancelled` means Uber cancelled an identified order after submission; Nook did not cancel it. Report the receipt’s cancelled status. Refund and charge status must be verified with Uber; do not automatically reorder.