# Assist With Buying — full agent contract Last updated: 2026-10-26 (link a Place of the person's own to its shared directory entry: buying_link_place {venueId, publicVenueId, replace?, dryRun?} sets one field and keeps everything on their row, buying_unlink_place undoes it; purchase lines: fees, paymentLabel, missionId, the Item's Type inherited; place kinds antique_mall and booth, a booth in its mall via parentPlaceId; a Place photo never replaces its main picture unless makeMain; read-only buying_find_prior_sightings: photo/text earlier evidence lookup, paged and access-filtered; buying_list_places carries coordinates {lat, lng, source} for the person's own Places — null on shared rows, outside a narrowed selection, and for a photo-learned point when media is withheld; begin_batch lists each slot's uploadRef and URL in its text, one line per slot; the Trip's stop table: buying_save_trip takes stopView {columns, sort} — the columns the person's stop table shows and its order, clearFields stopView for the defaults; a stop's fitNote is its Why go line and categories its tags; favorite on a stop is the person's star; community means an organized multi-home event; F2: batch sale imports and photo attachments with explicit per-record receipts; F3: safe single-stop field patches with read baselines; F4: bulk stop review gaps summarized once; F1: geocoded coordinate provenance, 2000-character verifiedNote, original-post date verification; Buying WO4/5/7/8: directory match hints, return-note intent, fetchable field conventions, explicit purchase windows; preferences: every new preference of every kind starts open to the person's AI, and a row they switch to Only me stays closed; 2026-10-17: sale day maps: buying_save_trip takes maps, every map link of the day main first, and buying_get_trip returns them; a stop takes its own mapUrl and partOfStopId or partOfKey, a house inside a community sale on the same Trip; saleType community; new buying_save_trip_stops saves up to 30 stops in one all-or-nothing call; stop replies carry key; trips hold the whole day, AWB-WO-028 passes 2 and 3: sale sources with search guides in buying_get_brief; possibilities, honest addresses, listing checks, the plan and a decisions log; arrive and leave in one call, closing names stops nobody closed; a companion's buys are their own line on a Trip; buying_import_sale takes a sale with no company and retries held listing photos; discounts by time of day and by category; where to look up prices on a Type; 2026-10-16: follow-ups A: buying_search_workspace finds the entries inside Lists by name — listEntries with listName, holding owned or wanted, state and wantLevel, hit kind listEntry, a narrowed connection only its Lists; wave 5 R: buying_save_item takes listingScreenshotBase64, a listing screenshot kept as dated evidence in the same save — always attach a Marketplace or online find's listing evidence (photoUrls, listingScreenshotBase64, sourceUrl); wave 4 P: in-store mode — when the person says "check", buying_store_check claims every store batch they snapped on Out with the Place, today's price, their wants and sizes and owned candidates, then buying_save_store_results saves one Item and appraisal per thing and closes the batch with its card, two calls; wave 3 F: an answered ask can carry an answer card — buying_update_capture_queue_item complete accepts answer {headline, keyNumbers, verdict}; an answered ask the person sent back arrives Waiting with followUp {kind more|not_right, words}; Queue rows say the style chosen for that ask; buying_get_brief carries waitingOnPerson by kind; workStyle may be ballpark or focused; wave 3 G: appraisal verdictKind; a criterion the person turned down stays rejected, keptRejected; price rows say correctedBy person; Items carry itemChecks; openResearch leaves out automatic source lists; wave 3 H: buying_save_trip_stop records the person's stated choice in one call: tripId, stopId, selection and userInstruction, no read first; earlier 2026-10-16: every Queue listing says what waits on the person: queueCounts and waitingOnPerson; buying_get_trip names each stop's lines in day.purchasesByStop; a Place's tags carry their low–high, pricingProfile.tags.range; 2026-10-15: an Item filed by mistake is archived, never deleted: buying_save_item archived true with archivedReason, archived false restores, buying_list_items includeArchived, buying_get_item archived; buying_get_item adds priceLadder, the value ladder as rungs to quote; a price says what condition it is for: condition new, sealed, used, as_found or for_parts beside scenario's words, and priceSummary groups by condition instead of by each wording; a seeded Context entry that copies a field the page already shows says where: origin.shownOn overview, compare or appraisals; an estimate says what it is of: valueBasis melt, replacement, resale or insurance, cleared when a correction changes the kind; 2026-10-14: price sheets: rows dated to the day so a date range finds them, validUntil and current, regularCents, sku and brand, state inferred by default, newTypesWithoutOverview; comparables carry sourceLabel and state; a same-name warning on buying_create_place; plant prices live here; sheet rows carry sheetId and evidenceFileIds and buying_get_place lists priceSheets; buying_get_type priceSummary; one first call: buying_get_brief; a home base and how far the person will drive on a reach preference, lookingFor.homeBase; calendar days are YYYY-MM-DD; operationId advice only where a tool takes one, and a requestId on every error; a request to change the site goes to buying_report_issue kind request, not the Queue; Place links are /places; GET /mcp/about; 2026-10-12: collections: owned or wanted on List entries, want level and price ceiling with vsBudget, sizes per person, tag-colour discounts at a Place, reading a Library document's text; receipt lines carry their printed code and are matched to tags on the same Trip; lines to identify; a receipt total checked against its lines; corrections say what changed and a replay says it is one; 2026-10-06: the Queue reads Waiting on AI · AI working · Answered · Everything, same stored states; sold to someone else is its own availability, apart from your judgement; 2026-10-05: how to file what you find; a Marketplace or store sweep prompt; connect over MCP rather than editing the website) Appraisal tag history uses last-save time, with the optional Place and cutoff applied before the cap. A tag-reading date is unknown; appraisal timestamps never count as sighting dates or shelf duration. Explicit owned lifecycle participates in alreadyOwned/what-do-I-own even without a receipt or ownershipOrigin. With no recorded quantity or matching purchase, declaredOwned=1 is a stated quantity assumption; it implies neither a purchase nor a before-app acquisition. Matching purchases are not double-counted. Existing grant filters still apply. Item corrections checked against your read (2026-10-24): buying_save_item with findingId takes baseValues — the values you read for each field you change (title, summary, condition, sourceUrl, askingPrice, lifecycle, stage; null = it was empty). If any changed since, nothing is saved; the refusal STALE_FIELDS carries current {findingId, ...those fields} — only to a connection with a mission.read grant covering that Item's Mission; a write-only connection is told which fields changed, not their values. Every changed field must be named. Prices keep their own expectedRevision on buying_save_prices. Item history qualification (2026-10-24): Legacy acquisition history is retained, never silently hidden. When no active owner receipt supports acquisition, acquired status entries carry a read-only qualification and a caveat in note for existing web history renderers; originalNote retains the exact stored note (null if absent). Actors, dates and stored history are unchanged. This is unconfirmed owner acquisition, not a claim that the historical event did not occur. Companion and voided purchase timeline rows are explicitly labelled. MCP retries: operationId and photo opId are scoped to the authenticated grant (authenticated client ID fallback), never a connection identity supplied in tool input. Another connection can reuse an operation ID independently. Reusing an operation ID with different content in the same connection is refused. Legacy receipts without a writer replay only an identical request hash; a mismatch starts a fresh scoped operation. Stable record aliases (capture/batch row keys, appraisalKey, artifactKey, datasetKey) retain their workspace meaning. Pending photo uploads created before target binding must begin again with a fresh opId; their old records and bytes are preserved. buying_get_item item.writtenBy and buying_get_queue_activity items[].writtenBy name the recorded connection label. Legacy agent rows without attribution say "Connection not recorded (before Oct 23 2026)". Security: opaque OAuth tokens must identify their client through provider-verified azp or client_id. Unidentified legacy clients must reconnect with a supported client. Research image rendering may return 429 with Retry-After; retry after that interval. Trusted List recipients must sign in with a verified invited email. Ownership enrichment respects the connection’s Mission, Place and List selections. ## Coordinate provenance and listing verification (2026-10-23) Trip stop writes (single, batch and research import) and `buying_import_sale` accept `coordinateSource` as an existing `http`/`https` URL or exactly `geocoded:`: `geocoded:nominatim`, `geocoded:census`, `geocoded:google`, `geocoded:mapbox`, or `geocoded:manual`. Other providers and schemes are refused. Send it with both latitude and longitude, and keep `locationPrecision` honest: geocoding does not verify a sale address. Stop `verifiedNote` accepts up to 2000 characters with normal text sanitization. **yardsale.club date ranges can be wrong. Verify dates against the original post before saving or routing; do not treat an aggregator's date range as verified evidence.** ## Saving a sale day (2026-10-17) When you research a day of yard, garage, estate or community sales — Facebook, Craigslist, the sale sites — save the whole day here, in about three calls. Buying is where the sales, their evidence and your map links are kept; it never fetches, embeds or draws a map. 1. `buying_save_trip` (mission.write) — one Trip for the day: `date` (and `endDate` for a range), `timezone`, `area`, and `maps`: every map link of the day, main first, each `{url, title?, note?}` — the Google My Maps you made for the whole city, then one per community sale or neighbourhood. Sending `maps` replaces the list (`[]` removes them all); `mapUrl` alone replaces only the main map, and stays the main map's url for older readers. Only `http`/`https` links, at most 20. **Always save the map link you make.** The reply echoes `maps`; `buying_get_trip` returns `maps` too. 2. `buying_save_trip_stops` (mission.write, NEW) — up to 30 stops on one Trip in one call, each exactly what `buying_save_trip_stop` takes without `tripId`, saved in order in one transaction. All or nothing: a refusal names the stop as `stops[i]` (its field as `stops[i].field`) and nothing from the call is kept. Repeat creates return the stop unchanged. The reply lists `{index, stopId, key, version, created, partOfStopId?, gaps?, warnings?}` per stop. Unreviewed stops without recommendations are summarized once in top-level `gaps` for this batch; all other gaps and warnings stay on their stop. A single-stop save still returns its review gap. Every sale is a stop with its verified `date`/`endDate`, `hours` and `address` (an honest `locationPrecision`; the seller's words in `listedLocation`), what is there (`summary`, `categories`, `fitNote`), the listing links in `sources`, and `verifiedStatus` after you re-read the listing. 3. Listing photos go on each stop with `buying_attach_photo`, target `trip_stop` and the `stopId` from step 2 (`fetch_url` for a listing's photo address, `begin_batch` for several). A COMMUNITY SALE HOLDS ITS HOUSES. Save the community or neighbourhood sale as one stop with `saleType: "community"` (new) and its own `mapUrl` — its sub-map. List it BEFORE its houses and give each house `partOfKey` (the community sale's `key`) or `partOfStopId` (its `stopId`). One level deep, on the same Trip: a stop is never part of itself, of a house, or of a stop on another Trip, and a stop that holds houses is never a house; each of those is refused as `INVALID_INPUT` naming the field. Houses are ordinary stops — address, hours, photos, sources, notes, verified. `clearFields` takes `mapUrl` and `partOfStopId`. Every stop reply now carries its `key`. The person sees the community sale as one row with its own Map ↗ and "N houses", which opens to the houses in place; the Trip's header shows Map ↗ and "+N maps". WHICH SALE TYPE. `community` is an organized multi-home event the post itself describes. A single sale whose post mentions nearby or multiple sales is not one by itself: look for the community sale it belongs to and, if one is posted, save it as a house of that sale; with no organized event, save it as `garage` (or `yard`) with a nearby-sales tag in `categories` and a note. Church and rummage sales are `other`. THE STOP TABLE (2026-10-24). The Trip's Overview lists its stops as a table whose columns suit the kind of Trip — a sale day, antique or ordinary shopping, websites to check. Set them with `buying_save_trip` `stopView: {columns?, sort?}`: `columns` in display order from `why`, `tags`, `when`, `timeLeft`, `where`, `type`, `choice`, `visited`, `link` (the name is always shown); `sort` is `route`, `opens`, `timeLeft` or `name`. Leave it out to keep what is there; `clearFields: ["stopView"]` goes back to the defaults for the Trip's kind. The person can change it on the page; `buying_get_trip` returns it as `trip.stopView`. A stop's `fitNote` is its Why go line (keep it under 140 characters) and its `categories` are its tags — short theme words the person filters by. `favorite: true` on `buying_save_trip_stop(s)` stars a stop for the person (set it only when they ask; `false` removes it): starred stops sort first, pin gold on the map and get their own folder in the KML download. ## In-store mode: "check" (2026-10-16) The person is in a store, snapping photos on the site's Out page. Each photo is stored at once into a STORE BATCH at the Place they are at (an ordinary Queue ask). When they tap "Tell your AI" they paste a short phrase such as "Check my AWB store batch". Answer it in about two calls, plus the image reads: 1. `buying_store_check` (queue.write). One call claims every waiting store batch and returns, per batch: `batchId`, `claimToken`, `place {venueId, name, today}` (today's price, the tag colours that apply today and notes written there today), `tripStop`, and `photos[]` — `fileId`, when and where it was taken, the person's `note`, and `candidates` (matching purchases and List entries, found from the note). `standing` carries their wants, collections, habits and sizes. `imageReads` lists the exact `buying_get_capture_images` calls to look at the photos (media.read; without it `url` is null, as in the Queue listing). Nothing waiting: `batches: []` and `nextStep` says what to tell them. A batch they sent back comes with `followUp` and `earlierResults`. 2. `buying_save_store_results` (mission.write and queue.write). Send `batchId`, `claimToken`, one row per THING (photos of the same thing share a row): `fileIds`, `title` (what it is), `headline` (one line for the aisle), `verdict` (`good_buy`, `fair`, `over`, `walk_away`, `unsure`), `worthLowCents`/`worthHighCents` (your resale estimate), `tagCents` (the tag you read), `typeName`, and `itemId` to link an Item they already have; plus `answer {headline, keyNumbers?, verdict?}` for the batch. Each row becomes an Item at the Place with its photos, the tag as its asking price (no tag read: today's price there, marked as such) and your appraisal with the worth range; the batch closes Done with your card. `owned` and `wantedListId` are checked for you when left out. A retry with the same `batchId` and `claimToken` returns what was saved and saves nothing twice. Then reply in at most 4 short lines, leading with anything worth buying (what it is, worth $X–Y, tag $Z). Do not ask follow-up questions in the store unless a photo is unreadable. End with "Send more when ready." `buying_what_do_i_own` answers more about what they already own. The person sees your rows land on Out as you save them. ## Asking, answering and sending back (2026-10-16) - THE ANSWER CARD. When you complete an ask with `buying_update_capture_queue_item` (`complete` or `resolve_answered_elsewhere`), also send `answer`: `headline` (the answer in one line, at most 120 characters), up to three `keyNumbers` `{label, value}` in your own words (`{label: "Sold on eBay", value: "$15–20"}`), and `verdict` (`good_buy`, `fair`, `over`, `walk_away` or `unsure`) when the ask was whether to buy. The person's page draws it above your full `note`; never put a number in the card the note does not support. - SENT BACK. The person can send an answered ask back with "Ask more" or "That's not right". It arrives Waiting with `followUp {kind: more | not_right, at, words}` and their words in `userResponse`; your earlier note, answer and results are still on it. Read them, fix or extend, and complete it again. - HOW DEEP, PER ASK. A Queue row's `style {name, source, chosenFor: "this_ask"}` is the work style the person chose for that ask — a starter (Ballpark, Focused, Thorough) or one of their own. It outranks their saved default. `styles.workStyle` in the brief and preferences may now be `ballpark` or `focused` too. - WHAT WAITS ON THEM. `buying_get_brief` carries `waitingOnPerson [{kind, count, capped}]`: asks, suggestions, criteria, preferences, sameThing, receipts. Their Home shows the same set as "Your turn". Mention it at a quiet moment, and do not propose more of a kind that is already waiting. - SEEN BY YOUR AI. Listing an ask stamps when an AI first saw it; the person's ask page shows Sent · Seen · Working · Answered and says when nobody has picked an ask up for days. Claim what you can. ## The person can now correct what you wrote, and what that means for you (2026-10-16) - VERDICTS ARE A WORD AS WELL AS A SENTENCE. `buying_answer_appraisal` and `buying_log_appraisal` take `verdictKind`: `good_buy`, `fair`, `over` (over what it is worth), `walk_away` or `unsure`. The page draws it as your verdict mark; `verdict` stays your one-sentence reason beside it. Without `verdictKind` the page shows your sentence in plain ink and never guesses a colour from your words, so send both. - "NOT THIS" IS FINAL UNTIL THEY SAY OTHERWISE. The person can accept, change, turn down or drop the criteria you propose, several at once. A criterion they turned down stays rejected: re-sending its `criterionKey` to `buying_save_comparison` does not propose it again, and the reply lists it in `keptRejected { criterionKey, label, reason }` with their reason. Only `userConfirmed: true` (they told you they want it) brings it back. The compact `buying_get_mission_context` criteria carry `proposalState` and `rejectedReason`. - A PRICE THE PERSON FIXED SAYS SO. They can add a price or correct one on an Item's Prices tab ("That's wrong"); the old value is kept as a correction revision. Price rows in `buying_get_item` and `buying_get_type` read back `correctedBy: "person"` when the person's correction is the latest. Do not re-assert the value they replaced; ask first. - THEY SET WHAT YOU COULD SET. The person can change a Mission Item's stage (shown as Not decided, Want, Pass), set a Mission's status (Mark done archives it), void or edit a purchase line, merge two Types, take a picture off a record, and say who an Item is for. Each is recorded as theirs; read the record again before writing over any of it. - CHECKS DONE IN THE STORE. An Item carries `itemChecks [{ label, result: "pass" | "fail", at, by }]`: the Type's "check before paying" list ticked on that Item. Treat a failed check as a fact about the thing. - FEWER FALSE "UNFINISHED" ROWS. `buying_get_brief` `openResearch` no longer lists the automatic "Sources checked for this Mission … (recorded by the older source path)" passes; they stay readable as passes. ## Price sheets and store flyers (2026-10-14) - A SHEET ROW HAS ITS DAY. `buying_save_price_sheet` writes each line dated to the sheet's `day` at day precision (`observedTime {kind: "day"}`, `precision: "day"`), so `buying_find_comparables` with `fromDay`/`toDay` now finds sheet rows instead of excluding them as date unknown. - A FLYER HAS AN END DATE. Send `validUntil` (YYYY-MM-DD), the last day its prices hold. Sheet rows read back by `buying_get_type` (`prices`, `branchPrices`) and `buying_find_comparables` carry `validUntil` and `current` — false after that day, in the workspace's own day; both null when the sheet printed no end date. - "WAS" PRICES, SKU AND BRAND. A line's `regularCents` is written as a second `listed` row (key ending `:regular`, scenario "regular price, "). `sku` and `brand` go into the row's note as "SKU …" and "brand …". - `unit` IS STORED AND READ BACK AS `scenario` (comparables calls it `condition`). Put the size first: "5-gallon, each". When each size needs its own price history, give it a child Type ("Citrus trees" › "5-gallon citrus trees"); otherwise one Type with the size in the unit. - HOW WELL IT IS KNOWN. Rows you save default to `state: "inferred"`; send a line's `state: "verified"` only when the person confirmed that price. Rows the person saves on the site are `verified`. A sheet with `fileIds` says on each row that it was transcribed from a photo (`basis`, `basisDetail`). Rows saved before this are unchanged. - NEW TYPES NEED AN OVERVIEW. The reply lists `newTypesWithoutOverview`; write each one's overview with `buying_save_type` or `buying_save_types_batch` in the same pass. - COMPARABLES say more per candidate: `sourceLabel` and `state`. - THE SHEET'S PHOTOGRAPHS ARE REACHABLE. Every sheet row (`buying_get_type`, `buying_find_comparables`) carries `sheetId` and `evidenceFileIds`, and `buying_get_place` lists the Place's recent `priceSheets` (`priceSheetId`, `sheetKey`, `title`, `day`, `validUntil`, `current`, `lineCount`, `fileIds`). Open a photo with `buying_get_capture_image(s)`. - WHAT THIS ONE IS WORTH, AS THE PERSON SEES IT. `buying_get_item` adds `priceLadder` (2026-10-15): the Item page's value ladder built from the same rows as `prices`, one rung per kind (listed, comparable, sold, estimate, paid…), size, `condition`, `valueBasis` and currency, newest first, each `{kind, quantityBasis, unitQuantity, condition, valueBasis, currency, count, lowCents, highCents, latestCents, latestAt}`. Quote a rung by its kind ("tagged $12; sold comparables $24.99") and never two rungs as one range. - WHAT THIS USUALLY COSTS. `buying_get_type` adds `priceSummary`: a summary of the rows in `prices`, not a valuation. Groups by `quantityBasis` with `unitQuantity`, `condition` and currency (never mixed), each `{unitQuantity, condition, scenario, scenarios[], count, lowCents, highCents, latestCents, latestAt, currency, kinds[], currentCount}`, with `drawnFrom` naming the window it read. A row with no `condition` is grouped by what its `scenario` words state ("new retail" and "new, Amazon low" are one `new` group); `scenarios` lists the wordings. A price-sheet group keeps the sheet's unit as `scenario`, because a unit is a size (2026-10-15). - SAME NAME AS A SHARED PLACE. `buying_create_place` still creates the Place, and adds `warning: { sameNameShared: { venueId, name, isChain }, hint }` when the name matches a shared directory row: reuse it with `buying_use_public_place`, or set `parentPlaceId` to the chain. - PLANT PRICES. Prices live here. When the person also uses Assist With Plants, record the price here and, if asked, put a one-line dated price note on the plant or species there that names this site. ## Home base, one first call, and clearer errors (2026-10-14) - ONE FIRST CALL. `buying_get_brief` is it. Its `notesForYou` leads with up to six saved standing Preferences; its `contract` carries six summary rules and the resolved work style (full playbook: `buying_get_preferences`), and its `lookingFor` carries what they look for. `buying_get_preferences` is the detail read for Preferences; its `contract` is now a pointer, `{ see: "buying_get_brief", note }`, so the rules travel once. Its `styles` are unchanged. - HOME BASE, IF THE PERSON WANTS ONE. `buying_save_preference` takes `kind: "reach"` (how far they will go) with `homeBase { label?, area, address?, lat?, lng?, precision: "area" | "exact" }` and `travel { usualMinutes?, maxMinutes?, note? }`, only when they asked you to save it. `area` is words ("central Tucson near Reid Park"). Precision `area` (the default) rounds the coordinates to two decimals, about 1 km; `exact` keeps them, only when the person chose it. `kind` may be left out with `homeBase`, and a person keeps ONE home base: a save without `preferenceId` revises it. A reach row is open to you by default and closes like any other. Both reads return `lookingFor.homeBase { preferenceId, title, label, area, address, lat, lng, precision, precisionNote, travel }` or null. Use it to judge distance, drive time and pickup; never repeat the exact address back unless they ask. It never appears on a shared List, a public Place, a record shared by link or the directory. - CALENDAR DAYS AND INSTANTS. A calendar day (`date`, `saleDates`, `purchasedOn`, `fromDay`, `neededBy`, `dueOn`) is a `YYYY-MM-DD` string; an instant (`*At`) is epoch milliseconds. A day is shown as that day everywhere on the site, never shifted by when it was saved (2026-10-24). A purchase line sent with no `purchasedOn` is dated on the same clock as the day's Trip: the Trip's zone, else the Place's, the person's, or their latest Trip's, with the shopping day turning over at 4am local time; with no zone known, the UTC day. An appraisal whose `askingPrice` changes later records when the new tag was seen (`askingPriceObservedAt`). - ERRORS. "Send it again with the SAME operationId" now appears only on a tool whose input takes an `operationId`; a read is told to send the call again. Every error envelope carries `requestId`, one per HTTP request (also the `x-request-id` header); quote it in a report. An `INTERNAL_ERROR`'s `reference` is the same id. - A REQUEST TO CHANGE THE SITE IS NOT QUEUE WORK. The Queue is for work an AI can do through these tools. When the person asks for a feature, a fix or a docs change to Buying itself, file it with `buying_report_issue` and `kind: "request"` (severity optional); it lands in Admin → Issues for the people who run the site. `kind` defaults to `"bug"`, which still needs a severity. - RETRIES. `buying_add_trip_entry`: the same `operationId` replays, and the same `key` with the same content returns the saved entry; the same key with different content is refused, so a changed entry takes a new key. `buying_withdraw_location_contribution` takes no key: withdrawing twice returns the same withdrawn reply. - LINKS ARE PAGES. Place replies say `/places/` (they said `/venues/`, which now redirects). A searched purchase opens its Item, else its Trip, else its Place; a searched note opens the record it is on. `buying_list_notes` refuses a target id that names no record here (`NOT_FOUND`, field `targetId`) instead of returning an empty page. - `GET /mcp/about` is public JSON: `name`, `title`, `version`, `websiteUrl`, `resource`, `transport`, `toolCount`, `contractVersion`, `firstCall`, `authorization`, `documentation`, `humanSetup`, `links { llms, llmsFull, connect, ai, protectedResourceMetadata }` and `scopes [{ scope, label, writes, toolCount }]`. ## At the shelf: do they own it, and is it under what they'd pay? (2026-10-12) Two questions an AI could not answer in a thrift store, now answered by the records: - OWNED OR WANTED IS A STATUS ON A LIST ENTRY. `buying_save_list` takes `defaultHolding: "owned" | "wanted" | null` — a collection ("Erin and Scott's movies") says owned for every entry, a wish list says wanted — and `sourceUrl` / `sourceLabel` to point at where a collection is really kept (How You Rank for board games) instead of copying it. An entry's own `holding` (on `buying_update_list` changes and `buying_save_list_item`) beats the List's default; `null` falls back to it. `buying_get_list` returns `defaultHolding`, and per entry `holding` and `effectiveHolding`. - THE RUN RULE: ticking a wanted entry bought makes it owned (`becameOwned` on its reply row); un-ticking it (reset or skip) on the same run puts it back to wanted (`backToWanted`). An owned you set by hand never changes with a tick. `state` (needed · bought · skipped) is still the shopping run. - A CONNECTION THE PERSON NARROWED TO SELECTED LISTS reads, searches and writes those Lists only: `buying_list_lists`, `buying_get_list`, `buying_search_workspace`, the Lists on `buying_get_place`, `alreadyOwned` and `buying_what_do_i_own` leave every other List out, and a write to one (`buying_save_list`, `buying_update_list`, `buying_save_list_item`, `buying_record_list_price`) — or creating a new List — is refused with `OUT_OF_LIST_SELECTION`. An entry is changed only through its own List. The same holds for a Trip's Lists (`buying_get_trip` / `buying_list_trips` show only selected ones and withhold another List's run snapshot; `buying_save_trip` links only selected Lists and never drops one it cannot see), notes on a List, List titles and Recaps, and a purchase, capture or decision linked to a List. - BULK: `buying_update_list` takes up to 500 changes per call (measured against Convex's per-transaction limits), so a 393-title collection is one call of `add`s. - "DO I ALREADY OWN THIS?" `alreadyOwned` (on search, Item saves, capture rows and appraisals) and `buying_what_do_i_own` now also read List entries: `onList { owned, wanted, other }`, each `{ match: "on_list", how: linked | same_name | close_name, listName, entryName, holding, wantLevel, budgetCents, vsBudget, path }`. `ownedOnLists` counts the owned ones; `fillsGap` is the first wanted one ("fills a gap on your wish list"); `buying_what_do_i_own` answers `owned: true` for an owned entry. A name match counts only on an entry that is owned or wanted; an entry linked to the Item by `findingId` counts on any List. Bounded, and `bounds` says how. - COLLECTION DOCUMENTS COUNT AS OWNED (2026-10-24). A Library record (a document or a Source) whose title says collection, inventory or owned ("Erin and Scott's movie collection") lists its titles: one per line (bullets, numbers, or a table's first column), or a paragraph per letter with titles separated by " · " ("**T:** Taken · Titanic · Toy Story 1, 2, 3 (DVD+BR)") — the letter label and format notes come off, and "Toy Story 1, 2, 3" (as Toy Story 1, 2 and 3) or "The Bourne Identity / Supremacy" are read as each title. A line that is the asked title exactly — case, punctuation, a leading "the", a trailing year like "(1997)", "[1997]" or " - 1997", and a format word like DVD ignored; "Titanic II" is not "Titanic" — answers in `alreadyOwned.inCollections` / `buying_what_do_i_own` `inCollections` as `{ match: "in_collection", documentTitle, entryName, source, path }`, counted by `ownedInCollections` (every matching document; at most 5 listed, `inCollectionsMore` when there are more), and makes `owned` true. At most the 25 most recently updated collection documents are read per call; `collectionsUnread` says how many were not read in full (`collectionsUnreadAtLeast` when that is a floor), and a "no" then covers only the ones read. `sources` lists every record that said so: `purchase`, `owned before the app`, `on list: ` (owned), `wanted on list: `, `in collection: `. Only this workspace's documents; a connection narrowed to some Places, Lists or Missions reads none. - WANT LEVEL AND PRICE CEILING. `priority` on an entry is the want level: `high` = really want it, `normal` = want it, `low` = only if it's a great deal (`wantLevel` carries the words). `budgetCents` is the most they would pay. `buying_save_list_item` now sets `priority`, `budgetCents`, `why` and `holding`. Wherever a price meets an entry the reply carries `vsBudget { budgetCents, priceCents, currency, verdict: under | at | over, byCents, sentence }` — `buying_record_list_price`, and Item saves, capture rows and appraisals that match an entry (also lifted to the top of the reply or row). Say the sentence: "$2.50 is under your $3 limit by $0.50." - SIZES PER PERSON. `buying_save_preference` takes `kind: "size"` (free text: title "Jeans", text "Women's 8 long") and `forPersonId` beside `forPerson`. Sizes are open to you by default, like wants and collections, and come back in `lookingFor.sizes` grouped by person (`buying_get_preferences` and `buying_get_brief`). A size is found by its title and who wears it, so saving "Shoe size" for Erin again revises hers. A Person the owner keeps to themselves is never named by id in any reply, fresh or replayed: that `forPersonId` comes back null, and the plain name stays. - WHAT TO STORE ON A PLACE: standing facts as context entries; the weekday ladder as `priceSchedule`; today's price as `todayPrice`; and TAG-COLOUR DISCOUNTS as `tagDiscounts: [{ color, percentOff, note?, validFrom?, validUntil? }]` on `buying_create_place` or `buying_add_place_observation` (the whole set, replacing what was there; `[]` clears). Told "yellow tags are 50% off", save it. Every `today` block — `buying_get_item`'s and `buying_log_appraisal`'s `venue.today`, `buying_get_place`, and `buying_capture`'s new `venue.today` — carries `tagDiscounts` valid today (each with a sentence: "Yellow tag = 50% off today.") and `notesToday`, with `source: "tags_and_notes"` when no price says anything. - READ A LIBRARY DOCUMENT BACK. `buying_read_library_document` (`mission.read`) — `{ interestId? | title?, offset?, maxChars? (≤ 50,000) }` returns a Library record's Markdown body one page at a time: `text`, `offset`, `nextOffset` (null when `done`), `totalChars`, `sections`. Workspace-only. On a connection that withholds media, every picture — inline, by reference (its address too), or as HTML — is replaced by its description, the reply says `mediaWithheld: true`, and a kept file's id is withheld; otherwise a Source that keeps a file names `sourceFile.artifactId` for `buying_get_research_file`. ## Read first: connect, then file what you find OPERATING THIS WEBSITE IN A BROWSER? Stop and connect over MCP instead: https://assistwithbuying.com/connect (endpoint https://assistwithbuying.com/mcp). Edits typed into the website are signed as the person, not as you, and they lose the prices, statuses and screenshots the tools keep. How to file what you find: - One Item per thing you looked at seriously, the rejected ones too. A rejected one gets `stage: "ruled_out"` and a one-line reason in `summary`, so the person can see it was checked. Search first (`buying_search_workspace`) and correct the existing Item by `findingId` rather than making a second. - An Item filed by mistake (a decision saved as an Item, a duplicate) is ARCHIVED, never deleted: `buying_save_item` with its `findingId`, `archived: true` and a one-line `archivedReason`. It leaves Mission Compare, the Mission's counts and lists, the Items index and search, still opens in full (`buying_get_item` says `archived`), and `archived: false` restores it. `buying_list_items` leaves archived Items out unless `includeArchived: true`. A real candidate you judged against is `ruled_out`, not archived. - Every price you see is a price row (`buying_save_prices`, or `observations` on a `buying_save_items_batch` row). A price written only in a note is not a price. An asking price read on a listing is kind `listed`, learnedBy `listing`, with that listing's `sourceUrl`. - Listing changes are field changes. Relisted: send the Item its new `sourceUrl`. Sold to somebody else: `availability: {state: "sold_elsewhere"}`. Vanished or expired: `availability: {state: "gone"}`. Back on the market: `availability: {state: "available"}`. Availability is what the MARKET did; your own judgement stays `stage` and is never changed by it. (`buying_set_item_status` `listed`/`sold` is the person's own resale, not a listing someone else bought.) - Save a screenshot of each listing you judge on its Item: `buying_attach_photo`, `mode: "inline"`, `role: "evidence"`, so the person never has to open the site. A crop to the product can be the main photo. - Your judgement is a note with `kind: "context"`: the verdict in one sentence as `text`; short facts one per line, then the full URL on its own line, in `detail`. No run-on paragraphs, and always full URLs. - A sweep's candidates are Items linked to the Mission, never URLs in a Mission note. The steps are the "Marketplace or store sweep" prompt: `buying_get_prompt {slug: "marketplace-or-store-sweep"}`, or https://assistwithbuying.com/prompts/marketplace-or-store-sweep. October 12: RECEIPT LINES, TAG CODES AND CORRECTIONS (AWB-WO-056 Package R). - A receipt line takes `code`: the code printed on it, exactly as printed. Many stores print the same code on the tag (Goodwill: tag `20A7RUB005990`, receipt line "Electrical $5.99"). When the line is on a Trip and names no Item, the server looks for the code among THAT TRIP'S OWN records only — Items on its stops or saved at its Places during its days (an `item_code` property, an attribute, a note, or a barcode decoded from one of the Item's photos) and photos on the Trip that are on no Item (on a stop, on a Queue ask filed on the Trip, or taken at a stop's Place that day) — with every read capped. The reply's `matched` lists `[{line, by: "code", code, to}]`: `to.kind: "item"` links that Item (and `titleFilled` says whether a stand-in title was replaced by the Item's — a title you sent is never replaced); `to.kind: "photo"` sets the line's `tagFileId`, the tag photo to open to identify it. This is decided PER PLACE: a line's candidates are those at its own Place (or with none known), so another stop's Items never fill or block it. `ambiguous` lists codes that fit more things at a Place than lines there, and links nothing at that Place: on Goodwill tags the code is a department and a price, so two $3.99 "Misc" tags share one. - AUTOMATIC LINKS ARE WRITTEN ONLY WHEN CERTAINLY RIGHT. On a bundle line (the Item's quantity would change how the total is split), or at a Place where ANY read the decision rests on was cut short (`matchCapped: true` — the Trip's existing lines, its stops, photos or asks, or that Place's Items, photos, or an Item's properties, notes or photos), the same answer comes back as `suggested: [{line, by: "code", code, to}]` and the line is left unlinked: resend it with `findingId` and it goes through the normal allocation. With `matchCapped`, no match is not proof there is none. Resending a line with a changed `code`, `tripId` or `venueId` drops the tag photo the old match found and matches again. One call has one read budget across every Trip it touches: matching it cannot afford comes back as suggestions with `matchCapped`, and a Trip join or visited-stop update it cannot afford is left for the next save (`readBudgetCapped: true`). - Printed tag text is not read for you. A tag whose code is a barcode is decoded into the photo's `codes[]`; a code that is only printed, you read, and save on the Item as an `item_code` property (never `tag_code`: a key containing "tag" is read as a price), then resend the receipt with the same purchaseKeys. - A line nobody has identified ("Misc $3.99") sends `needsIdentification: true`, and may then leave `title` off (the server writes "To identify · "). It is listed under "to identify" on the Trip page and in `buying_get_trip` `day.toIdentify` until an Item is linked — by a code match, or by you resending the line with `findingId`. Send `needsIdentification: false` to take it off the list without an Item. - The receipt itself: `receiptFileId` (its photo, already in the workspace — or attach it to the Trip's stop with `buying_attach_photo` target `trip_stop`, role `evidence`), `receiptTotalCents` (tax included) and `taxCents`. The total is kept on the stop the lines were bought at and checked against that stop's lines: the reply's `receiptCheck` `{linesSumCents, differenceCents, agrees, lines, unpricedLines, sentence, stored}` — "Lines add up to $37.68; the receipt says $38.68 — $1.00 not accounted for." (`differenceCents` is signed: positive is receipt money the lines do not account for, negative is lines adding up to more — "… $1.00 more than the receipt"; lines in more than one currency give `complete: false, reason: "mixed_currency"`) — and `buying_get_trip` `day.receipts` and the Trip page say the same. The sums are of the lines as written (a bundle's allocated shares, a price kept on a pending resend), read from that stop's own lines; a stop with more lines than the server reads gives `receiptCheck` `complete: false`, `reason: "too_many_lines"` and null sums — never a gap — and on a Trip read with many such stops, those after the read's budget say `reason: "budget"`. Resending the receipt without `taxCents` or `receiptFileId` keeps the ones already kept; `null` clears either. A photo sent without the total is put on the receipt already kept (`receiptFileSet`), and `receiptFileId: null` clears it from this call's lines and the stop's receipt (`receiptFileCleared`). With no `receiptKey`, the call is about the receipt its lines already belong to; lines on different receipts are refused until you name one. Giving a `receiptKey` later to every line of a receipt kept without one moves that receipt — total, tax and photo — to the key (`receiptRekeyed: true`); moving only some of its lines keeps both receipts, each checked against its own lines. Two checkouts at one stop: a `receiptKey` each. - CORRECTIONS SAY WHAT CHANGED. Each line replies `outcome` `created`, `corrected` or `unchanged`; a corrected line carries `changed` (field names) and `changes` `[{field, from, to}]` — $2.00 → $1.99 is `totalCents` from 200 to 199. The call counts `created`, `corrected` and `unchanged`; `replayed` now counts only resends that changed nothing (a correction used to be counted as one). `buying_save_prices` rows carry the same `changes` beside `changed`. - A REPLAY SAYS IT IS ONE. Any write answered from its stored reply — the same `operationId` and content sent again — now says `receipt.replay: true`; it used to say false. - `buying_list_purchases` rows carry `code`, `needsIdentification`, `tagFileId`, `receiptFileId` and `receiptKey` when set. October 17: TRIPS HOLD THE WHOLE DAY (AWB-WO-028, pass 2). - A stop not yet visited is a POSSIBILITY. `buying_save_trip_stop` takes `saleType` (`company_estate`, `family_estate`, `garage`, `yard`, `moving`, `other`), `freshness` `{dayNumber, ofDays, photoCount, photosPostedAt, note}`, a `fitNote` against what the person looks for, and `listedLocation` (the seller's own words for where it is). The Stops tab shows each on the stop's card and row. - HONEST ADDRESSES. When `locationPrecision` is not `exact`, the page shows "Approximate — look for signs near …" from `listedLocation` and never prints a pin's numbered street address as the sale's. A save pairing a numbered street address with a non-exact precision is kept and comes back with a `warnings` line naming the risk. - VERIFY BEFORE ROUTING. After re-reading a listing, save `verifiedStatus` (`live`, `expired`, `removed`, `address_released`, `changed`) and a `verifiedNote` (up to 2000 characters, with normal text sanitization) ("Sat only, from 9am"). The site stamps `verifiedAt` now (or the `verifiedAt` you send) and `verifiedBy`. `buying_get_trip` returns `needsVerifying`: Going stops on their day whose listing nobody has checked in the last three hours. Check those before routing. - THE PLAN. A stop takes `plannedArriveAt`, `plannedLeaveBy` (local 24-hour `HH:MM`) and a `skipRule`; a Trip takes `startAt` and `doneBy`, the day's limits. `buying_get_trip` returns `timeLimits` and `visitLengths`: per sale type a `quickMinutes` (the look that disappoints) and `goodMinutes` (worth staying), seeded at 2/10 for garage, yard and moving sales and 10/20 for estate sales, replaced by the person's own recorded times once they have three of a type (`source` says which). - THE DECISIONS LOG. `buying_add_trip_entry` takes `kind: "decision"`: the route and why, a sale dropped, a change of plan. Read them with `buying_get_trip` `section: "decisions"`; the person sees them as the Trip's Decisions. October 17: RECORDING FITS REAL SALES (AWB-WO-028, pass 3). - ARRIVE AND LEAVE IN ONE CALL EACH. `buying_save_trip_stop` takes `arrivedAt` and `leftAt` (epoch ms, or `"now"`). Arriving marks the stop visited unless the person already said what happened there (the reply's `derived` says so), and opens a private visit at the stop's Place; leaving closes that visit. The minutes between become the person's own `visitLengths` once there are three of a sale type. - CLOSING THE DAY. Saving a Trip with `status: "closed"` while stops are still `not_visited` closes it and returns `stillNotVisited` (each `stopId`, `name`, `selection`). Ask the person which they went to and save `outcome` visited or skipped. October 17: A COMPANION'S BUYS ARE THEIR OWN LINE ON A TRIP (WO-028, pass 3). - `buying_get_trip` `day.total` and each `day.stops[]` add `companionPieces` (how many things somebody with the person bought) and `companionUnpriced` (their lines with no price yet). Both sit beside `companionLines` and `companionCents`; companion money stays per currency in `byCurrency[].companionCents`, and none of it is ever in `spendCents` or `pieces`. The Trip page shows it as "Bought by others with you". - The site's Your stats page gains a Money section (prediction accuracy by company, share of the tag paid, spend by Trip, person and Type). It is a site read only; no tool changed. October 17: A SALE WITH NO COMPANY; HELD LISTING PHOTOS ARE RETRIED (AWB-WO-028, pass 3). - `buying_import_sale` no longer needs a company. For a garage, yard, moving or family-run sale, leave `companyName` out: the sale is saved on its own as a private Place with `shape: "event"` and no company, the reply carries `companyVenueId: null` and a `company.note` saying so. Never invent a company to fill the field. Importing the same `listingUrl` later WITH `companyName` links the sale rather than adding a second one. A `discountSchedule` sent with no company is reported in `problems` (a schedule lives on the company), and a `companyWebsite` with no company in `notApplied`. - New optional `saleType`: `company_estate`, `family_estate`, `garage`, `yard`, `moving` or `other` - what the listing says, never a guess. It sets the new sale Place's kind (estate sale, garage sale, yard sale, other) and is echoed in the reply. - Any http(s) listing link is a valid `listingUrl`: EstateSales.NET, Craigslist, Facebook Marketplace, a neighbourhood page. - `description` takes up to 4,000 characters, and the schema and the server read the same number. Past the 1,000-character summary, the opening becomes the summary and the whole write-up is saved as the Place's description. - HELD PHOTOS ARE RETRIED. Importing the same listing again - any `operationId`, with or without `photoUrls` - retries up to 12 listing photographs still held only as addresses, and never fetches one that already has a stored copy (it is reported with `alreadyStored: true`). `listingPhotos.retried` is `{tried, landed, stillHeld, remaining?}`. Many listing hosts block a server fetch; do not loop on it. - Attribution: a record you write is no longer stamped with your connection's raw client id as `provenance.clientName`. A readable client name is kept; an id is stored as nothing, and `producerKind: "agent"` still says an AI wrote it. Every reply also drops an id-shaped `provenance.clientName` from older rows. October 17: DISCOUNTS BY TIME OF DAY AND BY CATEGORY; WHERE TO LOOK UP PRICES (AWB-WO-028, pass 3). - A `discountSchedule` row may now carry `startTime` (local 24-hour `"HH:MM"`: that day from then on) and `category` (the markdown covers only that kind of thing). "25% in the morning, half off after noon" on day 1 is two rows: `{day: 1, percentOff: 25}` and `{day: 1, startTime: "12:00", percentOff: 50}`. "50% off furniture only" is `{category: "furniture", percentOff: 50}` — a category row may name no day, meaning the whole sale — or add `day` to limit it. `startTime` needs a `day` or `weekday`. Same tools: `buying_create_place` and `buying_import_sale`; stored rows read back with both fields on `buying_get_place`. - An Item's tag then reads "tag $120 · likely $90 now · $60 after noon". A category row is APPLIED only when the Item's Type, or a Type it is filed under, names that category ("Furniture", or "Dressers" under "Furniture"); otherwise it is said as words beside the price ("50% off furniture") and never folded into a number. Typical, never promised. - A company's Place page lists its usual pattern one row per line: "Day 2: 25% off", "Day 2 from 12:00 PM: 50% off", "Furniture: 50% off". - A Type carries `priceSources`: where to look up prices for this kind of thing, `[{name, url?, note?}]`, at most twelve — "eBay sold listings", "WorthPoint". Write it on `buying_save_type` (and each `buying_save_types_batch` row); it is sent WHOLE, `[]` clears it, leaving it out leaves it alone. `buying_get_type` returns `priceSources` (`[]` when none). A Type with none gets a line in `gaps`. October 6: SOLD TO SOMEONE ELSE IS NOT "PASSED ON" (AWB-WO-055). - An Item now carries an optional `availability`: `{state, seenAt, by, note?}`, where `state` is `sold_elsewhere` (it sold to somebody else) or `gone` (the listing vanished or expired). Absent means available, or not known. It is independent of `stage` and of the person's decisions: setting it never moves the stage, and an Item can be "Passed on" and "Sold to someone else" at once. - Write it on `buying_save_item`, each `buying_save_items_batch` row, or a `buying_capture` row (each replies with the same receipt): `availability: {state: "sold_elsewhere" | "gone" | "available", seenAt?, note?}`. `seenAt` is epoch ms, now when left out. `available` means back on the market and clears it; the cleared reading, and any reading a new state replaces, is kept in `availabilityEarlier`. Sending the same state again keeps the EARLIEST sighting, so a re-run sweep never stretches how long it took to sell. The reply's `availability` says `{state, seenAt, changed}`. - It is NOT `buying_set_item_status` `sold`, which is the person selling their own thing. - Reads: `buying_get_item` returns `availability` (null when not set) and `availabilityEarlier`; `buying_list_items` rows and the compact `buying_get_mission_context` findings carry `availability` as `{state, seenAt}` or null. - MARKET DATA: `buying_get_type` and `buying_get_item` (for the Item's Type) return `soldElsewhere` — the Type's Items marked `sold_elsewhere`, newest sighting first: `count`, `lastAsking {minCents, maxCents, currency, from}`, `otherCurrency`, `medianDaysListed` with `daysKnown`, up to ten `rows` (each with `lastAskingCents`, `priceFrom` `listed`, `asking` (the tag, only when there is no listed price) or `unknown` (price null), `quantityBasis` `item`/`lot`/`per_unit` with `unitQuantity`, `seenAt`, `daysListed`), `otherBasis`, `window` (50) and `capped`. Null when nothing has sold elsewhere. `lastAsking` is the LAST ASKING PRICE BEFORE IT SOLD — the newest listed price observed no later than the sighting, else the Item's tag — and never a confirmed sale price; say so when you quote it. It covers whole-item prices only: a lot or per-unit price stays on its row and is counted in `otherBasis`, never mixed into the range. `daysListed` runs from when the listing was first recorded to when it was seen sold, so it is at most how long it took. September 29: Place and Trip evidence corrections. - Sale imports use the normalized listing URL as the event identity within the workspace, independently of the title. Different listing identities stay separate; re-importing the same identity reuses its event. Existing Place prose is retained (`description.retained: true`); use `buying_save_place_description` explicitly to replace it. - `buying_get_trip` returns `day.total.byCurrency` and `day.stops[].byCurrency`, each containing `currency`, `spendCents`, `companionCents`, `retailCents`, `retailLines`, `paidAgainstRetailCents`. Legacy money totals and `day.currency` are null for mixed currencies. Retail comparisons use only purchases in the same currency. Companion lines remain attributed evidence and are excluded from personal spending. - Taking an Item off a stop keeps the Item and records an exclusion. Its Place cannot add it back through the Scorecard fallback; explicitly linking it again clears the exclusion. Scorecard day and stop totals report `companionLines`, and Item rows retain companion names in `boughtBy`. - Organizer matching uses exact normalized cores, never arbitrary company-name containment or suffix letters inside a name. Multiple core matches are refused with candidate IDs for an explicit choice. Future markdowns stop at the event's final day, and a one-day event has no future sale-day markdown. September 29 (review fixes): parking an ask for an existing Item preserves any newly supplied listed price and Place. Mission counts start at zero and count each distinct Item once, including when a linked Item becomes the origin. Mission comparison selects recent readable property facts and judges structured quantities in their stored units while showing their original words. September 29 (later): A PHOTO'S LOCATION IN WORDS (AWB-WO-050). - Photos carry `locationWords` on Queue rows (`files[]`) and on `buying_get_capture_image(s)` once worked out (null before, usually seconds): `{town, neighbourhood?, region, regionCode?, country, countryCode, nearTown?, address?, street?, postcode?, featureName?, featureKind?, featurePoint?, featureDistanceM?, source: "geonames" | "osm", attribution, detail, at, line}`. `line` is how the person sees it: "Goodwill · 13825 W Bell Rd, Surprise, AZ". - It is worked out by CODE from the photo's location, never read from the picture and never by a model. The town comes from a GeoNames list the site carries (nothing is sent anywhere). The street address and the business at the spot (`featureName`, its OpenStreetMap tag `featureKind` such as `shop=charity`, and `featurePoint`, where the map puts it) come from OpenStreetMap — only when the photo is NOT at one of the person's own Places and its ask names no Place. `detail` says which: `looked_up`, `at_own_place`, `ask_has_place`, `filed_at_place` (the photo is filed on a Place), `ask_unknown` (the ask it is on could not be established, so nothing was sent; town only), `service_off` (town only), `service_unavailable` (town only; tried again later). The ask's business in `photoLocationWords` needs at least half of ALL its located photos to name the same branch (one still waiting for words does not count as agreeing). - A Queue ask carries `photoLocationWords` `{featureName?, featureKind?, featurePoint?, street?, address?, town?, photos, located, attribution}` — the business most of its located photos name, else the town most were taken in. An untitled ask's temporary name falls back to it when no Place of theirs fits: "4 photos at Goodwill · Fri 2:14 pm", "6 photos · Surprise, AZ · Fri 2:14 pm". A name made from a map business carries `temporaryNameCredit` ("© OpenStreetMap contributors (ODbL)"): repeat it with the name. - The person is asked "Taken at Goodwill (W Bell Rd)?" on the ask's page; yes adds it as a private Place with the map's point and makes it the ask's Place (you are told as a person edit). To do the same yourself: `buying_create_place` with `coordinates` = `featurePoint`. `buying_get_place` `coordinates.source` may now be `map`. - It is a strong hint, not the person's word: a map can be out of date and a big store's car park is near other shops. Say which Place you filed work under. - PRIVATE, like the location: readable only by the person and their own connected AI. Never repeat an address, a business visited or coordinates anywhere public. Credit the names when you show them: "© OpenStreetMap contributors" (ODbL), "GeoNames" (CC BY 4.0). September 29: A PHOTO KNOWS WHICH PLACE IT WAS TAKEN AT, AND A SALE READS "BY" ITS COMPANY (AWB-WO-049). - A photo whose own location puts it at one of the person's Places carries `placeMatch` `{venueId, name, distanceM, method: "gps_nearest"}` on Queue rows (`files[]`) and on `buying_get_capture_image(s)`: the nearest of their OWN Places that has a point, within about 150 m (up to 300 m for a poor fix). Chains, companies and online Places never match; a sale only matches photos taken on its dates, a day either side. It is a SUGGESTION from the photo's location, not the person's word: file work at that Place when it fits and say so, and ask when it matters. - A Queue ask that names no Place of its own carries `photoPlace` `{venueId, name, photos, located, nearestM}` — the Place most of its located photos were taken at. The person is asked "Taken at ?" on the ask's page; yes makes it the ask's Place (you are told as a person edit), no stops the question. An untitled ask's temporary name says "at " from its photos when it has no Place or Trip. - A Place may hold a PRIVATE POINT, used for one thing: matching photos to it. `buying_create_place` takes `coordinates {lat, lng}` — send it when you genuinely have the pin (a listing, a map page the person sent), never invented from an address. A point you send fills an empty one or replaces one learned automatically; a point an AI already sent is kept (`notApplied`). `buying_get_place` returns `coordinates` `{lat, lng, source: ai | listing | photos | map, at}`. Places also learn a point on their own from two or more fresh photos the person files at them that agree (never from one), and from a pinned Trip stop. When the person forgets a Place's point, its matches go and nothing re-learns one until you send `coordinates`. Matching sends nothing anywhere, and coordinates never go anywhere public. - A PLACE CAN HAVE A PARENT PLACE: a sale is run by a company (shape `organizer`), a store belongs to a chain (shape `chain`). `buying_create_place` takes `parentPlaceId`, which SETS the parent — on a new Place, or re-parenting the one the name matched, with `parent.replaced` saying what it replaced. Send it alone, not with `companyVenueId`, `companyName` or `chain` (those still only fill a blank). Anything else as a parent is `PARENT_NOT_ALLOWED`. - Every Place row carries `parent` (`{venueId, name, relation: "company"}` for a sale, `{venueId: null, name, relation: "chain"}` for a store) and `displayName`, how the person sees it listed: " · by ", or the name alone when it already names the company. Name a new sale for the sale itself ("Old Buckeye kitchenalia sale"), not for its company. `buying_get_place` on a sale returns `parent` with the company's website, recap and `discountSchedule` — inherited, read from the company every time, never copied; on a company it returns `sales {upcoming, past, undated}`. September 28: PRO NAMES UNTITLED ASKS FROM THEIR PHOTOS (AWB-WO-044). - On a Pro account, an ask with no words and no title may carry a `temporaryName` written by a small model from its stored 320px thumbnails (at most six) and any voice transcript — "Marantz stereo receiver" instead of "6 photos · Sat 9:05 am". Queue rows say which: `temporaryNameBy: "model"` (with `temporaryNameSummary`, one sentence) or `"facts"` (the plain name). It is STILL NOT A TITLE: `titleBy` stays null, the ask is still listed by `buying_list_records_needing_titles`, and you give it a real title when you claim or complete it. A model's name is an automatic guess at what the photos show: look at them before relying on it. The person's or your title always wins. - The site still does not read photos for you (`readsPhotos: "no"`): the naming call asks for a name and a sentence only — no codes, values, prices or text from labels. - A Pro ask with photos and a recording is named this way (the transcript goes along); an ask that is only a recording is still titled from its transcript (`automaticTitleFrom: "voice_note"`). Pro voice notes are still transcribed the moment they are stored (`files[].transcript`, `automatic: true`). September 28: CONNECTIONS LAST UNTIL THE PERSON REMOVES THEM (AWB-WO-045). - A connection has no end date and nothing to renew. It lasts until the person removes it at https://assistwithbuying.com/connect. Connections made before today that showed as expired answer again on their next call. A client that cached an empty tool list shows the tools once it lists them again (a new chat, or reopening the connector). - A connection that holds nothing is never listed an empty tool list. Waiting for the person's approval, removed by them, or over the connection limit, it lists exactly `buying_manage_connection`, whose description starts with which of those it is and the one link that fixes it. Read that sentence and tell the person. - `buying_manage_connection describe` returns `connection.state` (answering, waiting_for_the_person, revoked, too_many_connections, not_connected) and a `connection.stateText` sentence. There is no "expired" state and no `expiresAt`. - The person's access page is https://assistwithbuying.com/connect everywhere, and an approval link is `/connect?request=`. The old settings address still redirects. September 27: YOUR AI READS THE PHOTOS — THE SITE DOES NOT (AWB-WO-043). The person's decision: Assist With Buying no longer runs a model on anybody's photos. You do the looking, on the person's own AI subscription, and the site makes it efficient. - NEW TOOL `buying_get_photo_sheets` (`media.read`): name ONE set — `askId`, `itemId`, `tripId` (the photos on a Trip's asks, its stops and the Trip itself) or `fileIds` (up to 600) — and get numbered contact sheets: up to 30 thumbnails a sheet, 6 across, each number burned into its cell's top-left corner, inline, with a legend line per photo (`#31 (ask …; Item …; taken …; codes …)`) and the same in `structuredContent.sheets[].cells[]`. Numbers are stable for a set. PAGED: `page` (from 1) and `sheets` (1–3 per reply, about 3 MB of pictures at most); follow `nextPage` until it is null. A 500-photo sale listing is 17 sheets. Only this workspace's stored photos are ever drawn; an id that is not one is counted in `leftOut`, never described. Read only; nothing is stored. - THE EFFICIENT WAY THROUGH A SET: contact sheets → the chosen photos at 1600px with `buying_get_capture_images` → `region` for fine print → `buying_attach_photo` mode `save_region` to keep a useful close-up on its Item. Every photo's barcodes are still decoded for free in `codes[]`. - NO NEW AUTOMATIC READINGS. New photos have `reading: null` and `readingStatus: "none"`. A reading made on a Pro account before today stays on its photo as an OLDER AUTOMATIC DRAFT — a hint to overrule, never the person's words; `readingSkipped` and `automaticReadings` describe only those. The September 25 and 26 notes below on automatic photo readings are superseded. - NO ASK IS TITLED FROM ITS PHOTOS (a Pro account's may now be NAMED from them — see September 28 above; still not a title). An ask with no words and no title carries `temporaryName` — a plain name the site composed from facts, like "6 photos at Golden Ticket · Fri 2:14 pm" — which is NOT a title: `titleBy` stays null, it is still listed by `buying_list_records_needing_titles`, and you give it a real title when you claim or complete it. On a Pro account an ask that is only a recording may still be titled from its transcript (`automaticTitleFrom: "voice_note"`), text only. September 26: PHOTOS OF ANY SIZE, THEIR SIZES MADE AT UPLOAD, SAVED CLOSE-UPS AND "YOU ALREADY HAVE THIS PHOTO" (AWB-WO-041). - A photo's 320px thumbnail and its 1600px copy are made by the site the moment the photo is stored, so `buying_get_capture_image(s)` return the 1600px copy (`variant: "agent"`) of a large photo without waiting; nothing about the call changed. The person's own photos may now be up to 100 MB (a 48 MP or 200 MP phone photo is taken as it is); the original is never changed. - KEEP A CLOSE-UP WITH ITS ITEM: `buying_attach_photo` mode `save_region` with `fileId` (a photo already in the workspace), `target`, `targetId`, and `region` (`{quadrant}` or `{x, y, w, h}`, the same box you read with) or `readingItem` (the position of a thing in that photo's Pro `reading.items[]` — its box is used and its label becomes the caption). ONE call, no bytes from you: the site cuts the box from the stored original at full resolution (up to 3000px on the long edge) and keeps it as a new photo on the record, replying `closeUpOf: {fileId, box, readingItem?}`. `makeMain: true` makes it the Item's main image. The photo it was cut from is never changed. Use it when one thing in a busy photo becomes its own Item, or when a label, mark or tag you zoomed into should stay with the Item. A resent `operationId` replays. - A photo that looks like one already in the workspace — the same file, or the same picture resent or re-encoded — carries `alreadyInWorkspace: {fileId, match: "bytes" | "picture", where: {kind, id, title} | null, note}` in `buying_list_capture_queue` `files[]` and in the photo reads; the person sees "Looks like a photo already on …" under it. Both photos are kept and nothing was merged: if it is the same thing, work on that record instead of starting a new one, and say so. A close-up read later carries `closeUpOf`. Each key is present only when it applies. September 26 (SUPERSEDED September 27 — no photo is read automatically any more; see above): AUTOMATIC PHOTO READINGS COVER A HANDFUL OF PHOTOS; BIG JOBS ARE YOURS (AWB-WO-042). - The site pays for Pro photo readings, so they are a small convenience: by default the first 6 photos on an ask, 30 per account in any 24 hours and 300 in 30 days. Busy photos read item by item are bounded per day as well; past that a busy photo is read whole and its `note` says so. - A photo a limit left unread says so rather than staying silent: `readingStatus: "skipped"` and `readingSkipped` (`ask_limit`, `daily_limit` or `monthly_limit`), in `files[]` and in `buying_get_capture_image(s)`; its `instruction` says it is yours to read. - Each Queue row carries `automaticReadings: {photos, read, pending, failed, skipped, skippedReason, limits, instruction?}` (null when no photo was ever in line for a reading): how many of its photos were read and why the rest were not. - A MEDIUM OR BIG JOB — a haul, a sale listing with dozens or hundreds of photos — is yours to read, on the person's own AI subscription. Work efficiently: look the photos over in batches with `buying_get_capture_images`, then ask for a `region` (a full-resolution crop) only where a label, code or mark matters. September 25 (SUPERSEDED September 27 — kept as history for the older readings still on some photos; see above): PRO PHOTO READING (AWB-WO-039). On a Pro account (an internal plan an administrator sets) photos on a Queue ask were read automatically when they were stored, within the limits of September 26 above. - Each photo in `buying_list_capture_queue`'s `files[]`, and each photo from `buying_get_capture_image(s)`, carries `reading: {automatic: true, whatItIs, title, visibleText[], codes[], method, itemCount?, items?[], note?, models, at}` and `readingStatus` (`ready`, `pending`, `failed`, `skipped` or `none`; `readingError` says why one failed, `readingSkipped` which limit left one unread — see September 26). It is a vision model's draft made by the site — what the photo shows, its text copied exactly, its codes — not the person's words and not yours. Work from it instead of opening every photo; open the photo when something matters or looks wrong, and overrule the reading whenever the photo says otherwise. - A BUSY PHOTO (a bin of many things) is read item by item: `method: "items"`, and `items[]` lists each thing with `label`, `whatItIs`, `visibleText`, `codes` and `box: {x, y, w, h}` in fractions of the upright photo — pass that box as `region` to `buying_get_capture_image` to look closely. - WHERE THE DETAIL IS. `buying_get_capture_image(s)` carry the full reading, `items[]` included. `buying_list_capture_queue` keeps each photo's reading compact so a page stays small: the first 8 lines of `visibleText` (`visibleTextCount` when there are more), the key codes (the photo's own, then any an item read that was verified), and for a busy photo `itemCount`, `itemLabels` and `detail` — no `items[]`. - A reading that never came back reads `readingStatus: "failed"` with `readingError: "the reading did not finish"`; look at the photo yourself. - EVERY CODE IS CHECKED BY ARITHMETIC, never taken on the model's word: `status` `matches_scan` (the site's barcode scan read the same code), `valid` (its check digit — UPC, EAN, GTIN-14, ISBN — or, for FNSKU, ASIN and LPN, its shape with spaces and dashes ignored; `check` says which), `doubtful` (check it against the photo before relying on it) or `unchecked` (no arithmetic applies). - `codes[]` holds only codes that identify a product: carrier tracking numbers (UPS "1Z…", USPS, labelled FedEx), a store's own SKU or shelf sticker and return-to-vendor numbers are left out. A code read on more than one item of a busy photo is taken off those items and kept once in the photo's own `codes` (which thing carries it is uncertain: look at the photo). Names, street addresses, phone numbers and emails from shipping labels are never kept in a reading. - An ask with no words and no title may arrive titled automatically from its photos, its voice note or both: `titleWrittenAutomatically: true`, `automaticTitleFrom` (`voice_note`, `photos` or `voice_note_and_photos`), `automaticSummary`. Send a better `title` when you claim it; a person's own title is never replaced. - Any other account: `reading: null`, `readingStatus: "none"` — look at the photos yourself, as before. September 24: PHOTOS YOU CAN ALWAYS READ, AND THE CODES ON THEM (Package M of AWB-WO-037). - NO PHOTO IS REFUSED FOR SIZE. Large photos are returned downsized automatically; ask for a region to read fine print. `buying_get_capture_image(s)` return a photo over 2 MB as its 1600px JPEG copy (under 1 MB), marked in `files[]` with `variant: "agent"`, `originalBytes` and `servedBytes`; the original is never changed. A photo that fits comes back as stored (`variant: "original"`), EXIF included. - `region` (optional, on both tools): `{quadrant: "tl"|"tr"|"bl"|"br"}` or `{x, y, w, h}` as fractions of the upright photo. It comes back at FULL resolution (`variant: "region"`, under 1 MB): read a size on a tag, a model number, a sticker. - THE BATCH PAGES INSTEAD OF REFUSING. One reply carries about 3 MB of pictures; in a batch any photo over 1 MB is sent as its 1600px copy so more fit. The rest come back as `nextCursor` and `remainingFileIds`: call again with the same `fileIds` and `cursor`, or with `remainingFileIds`. Eleven phone photos take two calls. - `codes[]` ON EVERY PHOTO: barcodes decoded from the pixels on the server (free, no service): `{format, value, kind}` with kind `upc`, `ean`, `fnsku` (Amazon warehouse label, `X00…`), `asin` (`B0…`), `lpn` (return sticker), `qr` or `unknown`. An ASIN has `amazonUrl` (its product page), a UPC or EAN an Amazon search `amazonUrl`, an FNSKU a quoted `searchTerm`. `codesRead: false` means not scanned yet, not "no code". They ride every capture-image read and the `pictures` of `buying_get_item`, `buying_get_type` and `buying_get_place`. Check codes[] first — a barcode beats a visual guess. When a code resolves to a product, record its source link and a price row from it. - `identifiedFrom` on `buying_save_item` and each `buying_save_items_batch` row: `barcode`, `label_text` or `visual` — how you knew what it is. `buying_get_item` shows it. - A PHOTO FROM THE CHAT IS ONE CALL: `buying_attach_photo` mode `inline` takes up to 1572864 bytes (1.5 MB) of JPEG, PNG or WebP as `imageBase64` (about 2 MB of base64), with `target` and `targetId`. Shrink a phone photo to about 1600px first if you can. If that picture is already in the workspace — the same bytes, or the same picture re-encoded, which is what a chat copy of a Queue photo is — the existing file is linked instead of stored twice, and the reply says `linkedExisting: true`. September 22: PICTURES. Several photos are two calls, a Queue photo is one, and a screenshot never leads. `buying_attach_photo` gained three modes and a target: - `begin_batch` opens up to 12 upload slots in ONE call. Each slot: `target`, `targetId`, `contentType`, `sizeBytes`, `sha256`, and optionally `caption`, `capturedAt`, `role`, `makeMain`. PUT each file to its own slot `uploadUrl` (at most three at once), then ONE `finish_batch` with every `uploadRef` confirms and attaches them all, one result per slot. A slot whose PUT never landed says so (`BYTES_NOT_STORED`, retryable) and never fails the others; PUT it again to the same URL and finish just that `uploadRef`. Ten photos: 2 calls plus 10 PUTs, where one-at-a-time `begin_upload`/`finish_upload` is 3 calls a photo, 30 for ten. - `link_existing` files a picture ALREADY in the workspace — a Queue ask's `fileIds` from `buying_list_capture_queue` — on a record: `fileId`, `target`, `targetId`. ONE call, no bytes move, and the picture keeps its own history (one the person took stays theirs). Linking it where it already is answers `alreadyLinked: true` and changes nothing. A picture filed on a different Item, Type or Place is refused (`FILED_ELSEWHERE`), not moved. Never re-upload a Queue photo. - `target: "type"`: a Type can hold photographs of what that kind of thing looks like, and leads with one, like an Item. - WHICH PICTURE LEADS. The first photograph attached to an Item or Type with no main image becomes its main image. EVERY attach reply says `isMainImage` (`finish_batch` per slot), `mainImageFileId`, and how to change it. Screenshots, listings, receipts and price pages are EVIDENCE: send `role: "evidence"` and they are kept on the record but never lead, so a price screenshot sent first can no longer become the Item's picture. They should not be the main image unless cropped to the product; crop first with your own tools if you can, and attach the crop as the photo. `makeMain: true` on any attach, or on `link_existing` for a picture already on the record, changes the lead; asked of evidence, it is declined by name in `makeMainDeclined`. - `photoFileIds` on `buying_save_item`, `buying_save_items_batch` and `buying_capture` now files EVERY id on the Item's Pictures (it used to set only the first as a thumbnail and drop the rest), fills the main image only when the Item has none, and never replaces one it already has. The reply's `photos` says what was filed and which picture leads. - Inline `imageBase64` is one call for a photo up to 1572864 bytes (1.5 MB, raised September 24 — see above). A photo already in the workspace is linked, not re-sent. September 16 design rebuild. Two things changed for you. FIVE PLACE TOOLS WERE RENAMED, and there are no aliases. Any tool name you hold that contains "venue" is the old spelling; the same tool now says "place" — list, get, create, the observation and the description. So `buying_list_places`, `buying_get_place`, `buying_create_place`, `buying_import_sale`, `buying_add_place_observation` and `buying_save_place_description`. The word in this product is Place; "venue" was an internal table name. ARGUMENT NAMES DID NOT CHANGE — it is still `venueId`, and still `findingId` for an Item — because renaming a field clients already send would break working calls for a vocabulary's sake. If you hold a cached catalogue and a call comes back as an unknown tool, re-list; the new name is there. September 19: FIVE THINGS A WHOLE MORNING IN THE FIELD PROVED WRONG. No new tools and no changed signatures — five behaviours that now match what this document already promised. Each was filed through `buying_report_issue` from the trip that hit it. 1. A PHOTOGRAPH THE SERVER FETCHES IS MEASURED AGAINST THE UPLOAD CEILING, 8 MB. `buying_attach_photo` with `mode: "fetch_url"`, and `photoUrls` on `buying_save_item`, `buying_save_items_batch` and `buying_capture`, were applying the INLINE ceiling of 131072 bytes and so refused every ordinary listing photograph. That ceiling exists only because base64 inside a JSON-RPC request is capped by the MCP envelope, and a fetched picture never touches the envelope. Two or three megabytes is now an ordinary fetch: do not downscale before sending a URL. `imageBase64` is capped at 1572864 bytes (1.5 MB since September 24), and the refusal says both numbers. 2. A BUNDLE WEIGHS PER-UNIT TAGS TIMES QUANTITY, using whole-lot tags once. `quantity` comes from the receipt line or its Item; `totalCents` is a whole-line weight fallback. With no tags anywhere the split is even PER THING rather than per line. Every line's reply now carries the `weightCents` and `quantity` the split actually used, so a wrong quantity is visible instead of baked silently into a dozen prices, and `/bundles/{bundleKey}` shows the count beside each line. 3. A PREDICTION AND A LISTED PRICE PAIR BY ITEM, and by nothing else. Neither row has to carry the Place: the Place a pair is FILED under is taken from the listed row, else from the prediction it answered, else from the Item. `buying_get_prediction_accuracy` and a Place's `pricingProfile` now reconcile their summary against the pairs they list, so `accuracy.pairs` is never smaller than the list beneath it; `countedPairs` and `from` say where the figure came from. 4. `companyName` MATCHES A COMPANY FORGIVINGLY. A trailing "Estate Sales", "Estate Sale", "Sales" or "LLC" and any punctuation are stripped before comparing, and a company already saved is matched when its core name equals, begins or ends the one you send. "All Under The Sun Estate Sales, LLC" therefore finds "All Under The Sun" instead of creating a second company. Two genuinely different companies are never merged — no edit distance, no similarity score. Naming a `companyVenueId` for a sale that already exists now LINKS it rather than silently doing nothing. 5. SMALL READS THAT WERE LYING. A row of `buying_list_items` carries its `recap` and `recapSource`. A Place's `counts.items` counts the Items saved there — nothing had ever moved that number. Every reply naming a Trip or a stop carries a `path` and a `url` to open, and the auto-link reply adds the Trip's own `tripDate`. A stored connection id is never read back as a name, in a tool reply or on a page. And an Item saved with a `venueId` files onto a stop on any OPEN Trip within ±1 day of today in that Trip's timezone, rather than only a Trip dated today. September 18: PLACES ARE MINE, EVERYONE'S, OR BOTH, and one new tool goes with it. Every row from `buying_list_places` carries `scope` — `mine` or `everyone's` — and the list takes `city` (alias `near`) as well as `kind`. `buying_use_public_place` takes a shared directory `venueId` and returns a Place of the PERSON'S OWN that REFERENCES it: the shared recap, what-it-has, address and hours stay on the directory row and stay current, while their notes, visits, prices, Items and Missions attach to their row. Nothing written on their row ever reaches the shared one. Do not create a second private "Goodwill" by name — that is the duplicate this prevents. A shared row now carries a `publicProfile` (recap, `whatItHas`, `priceLevel`, `bestTimes`, `website`, `phone`, `city`, `mapUrl`, `notes`), with address and hours in `location`. A chain still has no address and no hours, and none are invented. FOURTEEN NEW TOOLS WERE ADDED for the rebuild's blocks: Preferences, Types, Purchases, Notes, Decisions, and the batch writes. The two that matter most are `buying_get_preferences`, which gives you the person's standing guidance in one call at the start of a session, and `buying_save_items_batch`, which turns a sweep of two hundred listings into one call instead of two hundred. September 17: VOICE NOTES. A person can now record an ask instead of typing it, and `buying_get_voice_note` (`media.read`, no new permission) returns one inline as MCP audio content. Every attachment in `buying_list_capture_queue` now carries `media`: "image" or "audio". The site stores the recording and plays it back. (Since 2026-09-25 a Pro account's recording also carries an automatic transcript in `files[].transcript`; any other recording has no text, and none is being withheld from you.) If you cannot process audio, tell the person and ask what they said — never present your own guess at the words as the site's. September 17: THE QUEUE. `buying_queue_for_later` (`queue.write`) writes down work the person ASKED to have done later, as ordinary Waiting asks marked "Left by your AI, at your request" — never your own ideas, which stay suggestions. Completing an ask with `buying_update_capture_queue_item` now links any records the work changed (`results`: Item, Type, Place, List, Trip, Mission, Library), or for a workspace-wide cleanup carries a note and `recordsTouched`. The Queue's history names each AI by its connection's readable name, never its id. Details under "Queue and private media". September 21: A PRICE KEEPS ITS OWN HISTORY. Correcting a price used to keep a partial copy of what it said before — the amount, the date, a few words — and after twenty corrections it refused the twenty-first outright. Now every correction writes a COMPLETE immutable snapshot: the source URL, the fees, the seller, the Type, the unit basis and everything else travel with it, and there is no limit on how many times a price can be fixed. Prices written before this gain a complete baseline the first time they are touched, with whatever the old in-row array kept imported beside it and LABELLED as partial — nothing is filled in from the current row to make the past look better than it was recorded. What this gives you. Every price row reads back `revision` (the version it is on; 0 means nothing has corrected it yet), `revisionId` (the exact immutable version that produced these values), `recordStatus` and `historyAvailable` (how many versions exist — not how many that read showed you). Send the `revision` you read as `expectedRevision` when you correct, and a row somebody else changed in the meantime is refused `REVISION_CONFLICT`, carrying the record's current id and revision so you can look at what it says now instead of overwriting a change you never saw. Leaving `expectedRevision` out still works and is what an older client does: the correction lands with its full history kept, marked `legacy_unconditional_correction` — a declared limitation, not a warning. It means the server could not tell whether you had seen the value you replaced. Resending a row that says exactly what the record already says is recognised as a replay BEFORE any version check, so a retry after somebody else's correction is never refused as stale. `retract: true` with a `reason` takes a price out of the active record — a misread tag, a listing for a different thing — and out of every seller and prediction summary it was counted in. Nothing is deleted: its whole history stays readable, and `retract: false` restores it with the retraction still recorded. Use it instead of correcting a wrong row into a different claim. September 21: A CORRECTION RE-COUNTS. A corrected price used to leave the Place's pricing profile and its prediction score exactly as the first write recorded them, so a $500 tag corrected to $50 went on averaging as $500. Now the contribution is REPLACED: one price counts as one price before and after, the sums are right, retracting removes the contribution and restoring puts it back. The prediction pair itself is never re-paired — a prediction is a claim made at a moment — only its error is recomputed against the corrected tag, and the score as it stood is kept in the revision written at the time. September 21: A DATE-ONLY PRICE STAYS A DATE. Most prices are known to a day and no closer, and until now that day had to be given a time of day nobody observed. Price rows take `observedTime`: `{kind:"day", day:"2026-09-03"}`, `{kind:"month", month:"2026-09"}`, `{kind:"instant", atMs}` for a moment you genuinely observed, or `{kind:"unknown", originalText:"a while back"}` when the date is not known. A `zone` (an IANA name such as `America/Denver`) is accepted when you know it. SEND THE PRECISION YOU ACTUALLY HAVE: the read-back carries `precision` and `observedAtIsDerived`, so a day is reported as a day rather than the site claiming you saw the tag at noon. The older `observedAt` still works exactly as it did. September 19: AN HONEST PRICE ROW, OR NO ROW. A price row now has to say what it rests on, and a row that will not is REFUSED ON ITS OWN while every other row in the same call still saves — you get the reason and the fix under that row's key, and the person keeps their work. `sold` means a COMPLETED SALE and needs the sale record: a `sourceUrl` pointing at the sold listing or auction result, or `learnedBy: "receipt"`. An item still for sale is `comparable`; that is what the word is for. A researched price — `comparable` or `estimate` — needs a `sourceUrl` or a new `basis` field: one sentence saying how you reached the number ("educated guess from three similar pieces at this mall"). An existing non-empty `note` counts as the basis, so nothing that works today stops working. A price seen in person — `listed`, `paid`, `offer`, `prediction`, `list_price` — needs neither: how it was learned is its evidence. `basis` is stored on the row, comes back from `buying_get_item` and `buying_get_type`, and shows on the Prices tab where a sourced row shows its link. The rule holds on every write path: `buying_save_prices`, an appraisal's `prices`, an Item's `observations` and `buying_capture`. September 18: EVERY PRICE SAYS HOW IT WAS LEARNED AND WHERE IT WAS FOUND. Price rows — in `buying_save_prices`, in an appraisal's `prices`, and in an Item's `observations` — take two new optional fields, each a small fixed vocabulary. `learnedBy`: `tag` (it was marked on the thing), `listing` (you read it in a listing or an ad), `told` (a person said it), `receipt` (a completed sale — a sold listing, an auction result, a receipt), `worked_out` (you reasoned to it). `channel`: `retail_store`, `online_retailer`, `online_marketplace`, `auction`, `thrift_store`, `yard_sale`, `estate_sale`, `antique_store`, `flea_market`, `pawn_shop`, `classifieds`, `other`. Sending `venueId` DERIVES `channel` from that Place and replaces what you sent; the reply names the market it stored and whether the Place or you decided it (`channel`, `channelFrom`). Leave either off rather than guessing. Both become filters and sorts on the Prices tab of an Item and of a Type. September 18: THE PRICE VOCABULARY IS ONE COHERENT SET, AND THE SEPARATE PARK-A-QUESTION TOOL IS RETIRED. Scott: "I gave a directive to create some classifications of types of prices; you should feel like you can create the whole set as you think best. 'Tag' maybe isn't the best word, or 'asking', because there are many types of asking." So `kind` now says WHAT THE PRICE IS in seven plain words — `listed`, `comparable`, `sold`, `paid`, `offer`, `estimate`, `list_price` — and every screen says Listed, Comparable, Sold, Paid, Offer, Estimate, List price. `asking`, `asking_elsewhere`, `estimated`, `msrp`, `observed` and `calculated` are ACCEPTED FOR ONE MORE RELEASE ONLY: the save lands under the replacement word and the reply carries `deprecated: [{field, sent, storedAs, note}]` naming what you sent, what was stored, and what the stored word means. Send the new word from now on; after the next release these refuse. `list_price` also left `learnedBy`, because it is a kind of price and not a form of evidence — send `kind: 'list_price'` and the row is stored with no `learnedBy` at all. And "queue this for later" is the ONE way to leave work, with all the insights and pictures: the separate park-a-question tool is gone and its work is the optional `thing` block on `buying_queue_for_later`. The catalogue is 96 tools. and its work is the optional `thing` block on `buying_queue_for_later`. The catalogue at that point held every tool listed above; it is 96 tools today, with the two retained-research-file tools, `buying_save_identity`, `buying_find_comparables` and `buying_prepare_dataset` added on 2026-09-21, and `buying_void_purchase`, `buying_archive_type`, `buying_merge_type` and `buying_what_do_i_own` added on 2026-09-24, `buying_get_photo_sheets` added on 2026-09-27, `buying_read_library_document` added on 2026-10-12, and `buying_store_check` and `buying_save_store_results` added on 2026-10-16, all described below. September 18, later the same day: PREDICTIONS, SALE DAYS, BUNDLES AND COMPANIONS. `prediction` is an eighth price kind, and it is not an estimate. An estimate says what a thing is WORTH; a prediction says what THIS SELLER will have tagged it at, and it is only a prediction while the tag is unseen. Record it BEFORE the tag is turned over, with `observedAt` set to when you predicted. When a `listed` price later lands on the same Item the pair is scored automatically and the running score is kept per Place, per estate-sale company and per Type. `saleDay` and `discountPercent` ride on every price row and every purchase, because an estate sale discounts as it runs and the same lamp carries three true prices in one weekend. A Place may hold the schedule it runs, and an Item then reads "tag $120 - likely $60 tomorrow" from arithmetic rather than from a guess. A BUNDLE is one total across many things. `buying_save_purchases_batch` takes `bundle {bundleKey, totalCents}` and splits it across the lines in proportion to their per-unit tags times quantity, using whole-lot tags once, and falling back to whole-line `totalCents` weights. Quantity is stored separately. Replays preserve person-adjusted shares, require the original complete purchase-key set and total, and count seller bundle statistics once with contribution differences on correction. `boughtBy` names a companion who paid: real evidence about the seller, never counted in this person's spending. `forPerson` on a Mission or an Item says who the hunt is for. PRIVATE BY DEFAULT: prediction scores and seller pricing profiles are one person's own read of a company, and nothing in them reaches the shared directory. September 21, later the same day — WHAT AN INDEPENDENT REVIEW OF THAT BUILD CHANGED. Everything here is something a connected assistant can see. PAGING WAS FIXED, AND EVERY CURSOR ISSUED BEFORE THIS RELEASE IS REFUSED BY NAME. Rows written in one call share one timestamp, and a page used to resume by counting rows already returned at that timestamp — so once the count passed the scan window the page came back empty with an unchanged cursor, and the rest of that batch was unreachable. Eight captures saved in one checkpoint, read one at a time, lost five. Paging now resumes at an exact index position. If a cursor is refused, start the read again without one: nothing was lost, and following the new cursor reaches every row exactly once. `section: 'assessments'` NOW PAGES. It returns at most twenty rows and a `nextCursor`, where it used to return up to two hundred and no way to reach the rest. A row resting on more inputs than a list page can clear is withheld there and names its own `analysisId` so you can read it directly, where every input is checked. READING THE PACKET IS READING THE ANSWER. `section: 'packet'` and the single-assessment read both carry an `exposure` block now, because a packet's header names the question and a frozen earlier assessment carries its conclusion verbatim. Under `reviewView: 'evidence_first'` both withhold the question and any earlier conclusion and say that they did. This matters to your own honesty: `not_served_before_save` is only ever claimed when this site can show it served you the evidence first. A CAPTURE OF A FILE YOU MAY NOT SEE KEEPS ITS SECRETS. The row still appears — `available: false`, or `mediaWithheld: true` — with no artifact id, mime type, size or hash. A hash identifies a file as surely as its bytes do. `buying_attach_research_file` action `begin`, SENT AGAIN for an upload that is still open, now resumes it: `replayed: true`, what has already been verified, and fresh capabilities for exactly the parts still missing. You no longer need to have kept the `artifactId` to recover from a dropped connection. A CONNECTION LIMITED TO SELECTED MISSIONS CAN DO RESEARCH ON THEM. Name one of those Missions in `subjects[]` (or read with that `subject`) and the research tools work. Research on anything else, or on no subject at all, is refused with a message that says so — it no longer claims you named a different Mission when you named none. A SOURCED BASIS CANNOT ALSO DECLARE THAT IT USED NOTHING. `source_reported`, `calculation` and `comparison_estimate` are refused beside `input: {kind:'no_external_inputs'}`, not only when the selection is empty. Answer from what you knew and say `general_knowledge`, or name the rows you read. A RETRACTED PRICE IS GONE FROM EVERY DERIVED NUMBER — prediction accuracy, the seller profile, an Item's price summary, a Trip's totals. It stays in its own history, and it keeps the prediction pair it earned through a correction and a restore. September 18, later the same day — WHAT A FIVE-STOP ESTATE-SALE TRIP CHANGED. Everything in this section comes from one person working five estate sales through an assistant on a phone, with sellers waiting. Nothing here is a theory about what an assistant needs; each item is something that actually went wrong in a driveway. **ONE CALL FOR A WHOLE SALE — `buying_capture`.** Up to fifty things in one call, each with its prices (the shared price row), its photographs, and what the person actually did about it — `bought`, `passed` or `come_back` — plus, optionally, one purchase covering several of them. Reach for it whenever the person is somewhere rather than at a desk. The reason is not cost, though it is also cheaper. FOUR SAVES WERE LOST AT ONE SALE to "No approval received": the client asked the person to approve each write, they were holding something with a stranger waiting, and the prompts went unanswered. The work did not fail — it was never sent. Which is also why there is no "we will hold it for you". "No approval received" means the CLIENT NEVER SENT THE CALL. Nothing reached this server: there is no request to hold and no draft to reconcile. A server cannot store a message it was never sent. The two real fixes are fewer calls (this tool) and standing approval (below), and Buying will not pretend to a third. **ASK FOR STANDING APPROVAL, ONCE, AND SAY IT PLAINLY.** If the person is being asked to approve each Buying write, tell them: in Claude, open the Assist With Buying connector, find its tools, and set the write tools to **Always allow**; in ChatGPT, approve the first call and choose to keep allowing it for this connector. Then say what it means — their assistant can save to their Buying workspace without asking each time, it still cannot contact a seller, spend money, publish anything or delete anything, and they can change it back whenever they like. It is the single change that makes an assistant usable at a sale. **A REFUSAL NAMES THE FIELD AND SAYS WHETHER TO RETRY.** Bad input comes back as `INVALID_INPUT` with `field`, a message naming what is wrong, a `recovery` describing what a good value looks like, and `retryable: false`. Retrying it unchanged cannot succeed. `buying_save_trip_stop` with `latitude` and `longitude` and no `coordinateSource` used to answer `INTERNAL_ERROR` with "retry once" — which is how an assistant spends a sale retrying a call that will never work. The reverse holds too: `INTERNAL_ERROR` now means only a server fault, never your input, and `REFUSED` is the server's own sentence for a check it has not classified. See "Failure recovery" for every code. **A CORRECTION IS NOT A REPLAY.** In `buying_save_items_batch` and `buying_capture`, a row carrying `findingId` is applied as a correction whatever its `key` has already done, and `notApplied` says the key was overridden. A `key` alone is still an identity and still replays. This is the bug that made a $250→$90 correction land only under an invented key. **EVERY WRITE REPLY CARRIES `url`.** A full `https://assistwithbuying.com/...` link beside the `path` that was already there, for every write — `buying_save_trip` and `buying_save_trip_stop` included, which used to return neither. Give the person the link; a relative path in a chat window is not something they can tap. **ONE PRICE ROW, EVERYWHERE A PRICE IS SENT.** `buying_save_prices`, an appraisal's `prices`, an Item's `observations`, a Mission's and `buying_capture`'s are one shape, field for field — `shippingCents` and `feesCents` included, which an appraisal's prices used to refuse. `buying_log_appraisal` also takes `typeId`/`typeName` now, so the thing gets filed under a kind of thing in the same call that answers the question about it. AND EVERY PRICE ROW CARRIES THE SALE'S SCHEDULE. An estate sale is not one price, it is a schedule — full price on Friday, half off on Sunday, a dollar a box on Monday afternoon — so a row that records $250 and not which day loses the most useful thing about it. `saleDay` is which day of this sale, 1 being the first. `discountPercent` is the markdown in effect as a whole percentage: 50 is half off, 0 means tags were being honoured in full, and LEAVING IT OFF means you do not know, which is a different claim from 0. `companyVenueId` is the estate-sale COMPANY behind the sale, and is only needed when the sale's Place and the company running it are two different Places — it is what lets the same company's pricing schedule be recognised at its next address. **PHOTOGRAPHS REACH ITEMS AND PLACES, AND THE SERVER CAN FETCH ONE.** See "Photographs". **TELL US WHEN BUYING ITSELF IS WRONG — `buying_report_issue`.** See "When Buying itself is wrong". **`buying_save_types_batch`** writes up to a hundred Types in one call, each row exactly a `buying_save_type`. September 18: NOTHING WAITS ON A RECORD. An appraisal either answers or it does not exist — there is no "asked", "pending" or "in process" appraisal, no Item status that means waiting, and no Item screen that says one. Waiting is a fact about the QUEUE and the MISSION. A question you are genuinely leaving open goes to `buying_queue_for_later` with a `thing` block instead of to an appraisal: the object becomes an Item at once, and the question becomes a Waiting Queue ask naming that Item, marked "Left by your AI, at your request" and carrying the photographs. Answer it with `buying_log_appraisal`, passing that `findingId` and the ask's `queueItemId` — that writes the appraisal, sets the Item to Appraised, and closes the ask with the Item as its result. `minutes`, `intent` and `booth` are not taken there; they describe an answer. September 17: EVERY APPRAISAL LIVES ON AN ITEM, AND EVERY ITEM HAS PRICES. `buying_log_appraisal` and a queued `thing` now create the Item the question is about (never a Mission) or attach to the Item you name with `findingId`; the reply returns `findingId`, and the person reads your answer on that Item's Appraisals tab. Answer with how you got there — `reasoning`, `sourcesChecked` (empty and blocked ones too), `researchNote`, and `prices`. `buying_save_prices` (new, `mission.write`) saves up to 100 dated prices on Items in one call, with or without a Mission; resending a price's key corrects it and keeps its earlier value instead of being skipped. September 16 route move: four workspace surfaces changed URL. `/marketplace` is now `/places`, `/research` is now `/library`, `/context` is now `/preferences` (the page is called Buying Preferences), and `/agent` is now `/connect`. Every old path returns a redirect to the new one and no record or ID changed, so a link you already hold still works. Links returned by tools now use the new paths. September 4 Items/Lists/Places revision: `/items` combines existing Findings and Appraisals, with prominent Appraisals and Purchased views. This is not a database migration: existing IDs, detail routes, evidence, and published tools remain. Places is the user-facing name for the directory at `/places`. Overview, visits, item appraisals, and gallery distinguish observed prices from AI valuations. Venue kind additionally accepts `antique_store`, `garage_sale`, and `estate_sale`. The latter describes an individual sale, distinct from `estate_sale_company`. Existing shop/yard_sale/other records are not silently reclassified. Private place photo IDs may appear on the profile returned by `buying_get_place`; they are not public URLs or permission to expose the photographs. The owner can upload place photographs through the gallery. `buying_attach_photo` still targets a Queue item or an appraisal on an Item (`target` `queue_item` or `appraisal`), never a place. `buying_save_list_item` additionally accepts `quantity` (integer 1–9999) and `checked` (boolean). Explicit values update checklist state; absent values preserve it. Reads carry quantity, checked, and checkedAt. A checked line does not create a price, confirm a receipt reading, or change a linked Item's ownership status. `buying_record_list_price` requires any tripListId to name a trip directly beneath the item's list. At the 100-price cap it refuses the new write, preserving all history. Price comparisons group matching currency and pack size. New entries and write replies carry entryId; owner confirmation selects that stable entry rather than confirming multiple entries with an identical timestamp. `buying_get_list` also returns `tripLinesPartial`. When true, its trip lines and any derived subtotals cover only the newest 200 parent-list items, not the whole trip. ## Canonical AI compatibility and media contract Contract version: 2026-10-26-link-place-purchase-lines Family source: assistwithlife 3df255d93c15511dbd02ba575c9a9a335286b139 MCP endpoint: https://assistwithbuying.com/mcp Human setup: https://assistwithbuying.com/ai Concise AI guide: https://assistwithbuying.com/ai.txt First-party upload origin: https://assistwithbuying.com First-party upload path: /api/ai/uploads/{capability} Limits: 8388608 bytes per normal photo; 1572864 bytes inline; at most 3 independent upload lanes. First call: buying_get_brief Field conventions (also fetched with buying_get_brief → contract.fieldConventions): FIELD CONVENTIONS, the same across every tool here: a general free-text remark goes in `note`; a time is epoch milliseconds in a field named for what happened and ending `At` (`observedAt`, `checkedAt`, `decidedAt`, `capturedAt`) — never a date string; a calendar day (`date`, `saleDates`, `purchasedOn`, `fromDay`, `neededBy`, `dueOn`) is a `YYYY-MM-DD` string; most writes carry a top-level `operationId`, a lowercase stable key of at least 8 characters, and one that does not says in its own description what makes a retry safe. Purpose-named text fields (`rationale`, `summary`, `answer`, `question`) keep their own names; `note` is for the remark that has no better name. Money is integer cents with an explicit currency. Unknown keys are refused by name rather than ignored, so a wrong guess tells you what it was. Trip stop writes (single, batch and research import) and buying_import_sale accept coordinateSource as an http(s) URL or exactly geocoded:, where provider is nominatim, census, google, mapbox or manual. Keep locationPrecision honest; geocoding does not verify an address. verifiedNote accepts up to 2000 characters with normal text sanitization. yardsale.club date ranges can be wrong: verify dates against the original post before saving or routing. Place creation reports directory lookup limits, capped, matchCount and ambiguity even when no candidate is found. Counts cover the bounded sample only; capped or ambiguous lookups never offer an adoption action. Only a unique verified name/address match offers buying_use_public_place or parentPlaceId. Adoption refuses saturated, ambiguous or unverified private matches before creating or linking. Owner-only venues.previewDirectoryPlaceLink is dry-run only and proposes a publicVenueId link only after name and address verification. Positive return-intent Place notes infer comeBack true only when omitted; negative or ambiguous wording does not infer. Explicit false is retained. Pass comeBack:false on NEW writes to prevent inference; this does not alter this saved note. No existing-note MCP correction tool is available. Owner-only venues.previewReturnIntentRepairs lists proposed repairs with last-six IDs and never changes notes. buying_list_purchases returns window from/to and defaulted plus a plain summary. With no dates it covers the last 90 days; partial dates report the actual effective bounds. Preferences in detail, when lookingFor is not enough: buying_get_preferences Normal appraisal save: buying_log_appraisal Normal Mission save: buying_save_complete_result Normal sweep of Items: buying_save_items_batch Normal price history save: buying_save_prices Normal receipt: buying_save_purchases_batch Purchase resends: Resend the same purchaseKey to correct a saved line or attach it to a Trip; use a new operationId, its title and totalCents (or pricePending: true), and tripId for attachment. Omitted Item, Place, List, receipt, bundle and annotation fields keep their saved values. Resend the original quantity, currency and purchasedOn to keep those values. An identical resend creates no second purchase; a genuinely new purchase, even of the same thing, needs a new purchaseKey. Research pass authority: A selected-Mission connection must cover every existing subject of a continued pass and every supplied subject on the same grant. Supplying an allowed Mission does not authorize a different pass. An unreadable, empty or incomplete existing subject set is refused; the boundary reads up to 50 subjects and refuses larger sets for a narrowed connection. Connection lookup: Active approvals from before connections lost their expiry date remain usable, however many historical grants the account holds. Discovery, connection status and authorization use the same complete client lookup, including eligible unbound approvals; revoked grants and grants bound to another client never supply authority. Appraisal price corrections: Appraisal prices follow the same correction rule as buying_save_prices: omit sourceUrl or venueId to preserve it, or send null explicitly to clear the saved source or Place. Required price evidence still applies after a clear. Listing photograph copies: buying_import_sale retains each supplied listing-photo address and, with media.write, tries to store its image through the checked fetch path. Each successful fileId is linked to that sale's matching listingPhotos row. Failed images keep their remote addresses; retrying the same operationId repairs missing links to existing copies without duplicating files, and importing the same listing again (any operationId) retries up to 12 photos still held only as addresses. Buying does not read the listing page. Normal list run: buying_update_list Media retrieval: buying_get_photo_sheets to scan a big set as numbered contact sheets (up to 30 thumbnails a sheet, up to 3 sheets a reply, paged); buying_get_capture_images for a bounded set; buying_get_capture_image for one photo; buying_get_voice_note for one voice note. Transcription: Pro accounts only. For a Pro account a transcript arrives automatically in files[].transcript (automatic: true) and in the voice-note read; otherwise a voice note comes back as audio only, Buying holds no text of it and none is being withheld — transcribe it yourself or ask the person what they said. Automatic transcription requires a running time safely measured from the stored audio, at most five minutes; browser duration is advisory. An unmeasurable or overlong recording keeps its audio and reports transcriptError. Silence metadata is omitted when the phone could not measure it reliably. Photo reading: none. Assist With Buying does not look at photos for you — you do, on the person's own AI subscription: scan a set with buying_get_photo_sheets, open the photos that matter at 1600px with buying_get_capture_images, zoom with region for fine print, and keep a close-up on its Item with buying_attach_photo save_region. Every photo's barcodes are decoded in codes[]. A reading made before 2026-09-27 on a Pro account is an older automatic draft: a hint to overrule. Names for untitled asks: an ask with no words and no title carries temporaryName. Usually it is composed from facts (temporaryNameBy "facts": what was sent, where, when); on a Pro account it may be written by a small model from the ask's thumbnails and any voice transcript (temporaryNameBy "model", with temporaryNameSummary). Either way it is NOT a title — titleBy stays null — so give the ask a real title when you claim or finish it, and look at the photos before relying on a model's name. Media upload: buying_attach_photo begin_batch (up to 12 photos) → first-party PUTs → finish_batch; one photo begin_upload → PUT → finish_upload; a photo already in the workspace link_existing, no upload; a close-up of part of a photo already here save_region, no upload. The PUT does not redirect to or expose Backblaze. Fewer calls, same result: - Every buying_list_capture_queue reply carries queueCounts for the whole Queue and waitingOnPerson, one sentence when any ask waits on the person. You do not need a second listing to learn that something is waiting on them; tell them. - buying_queue_for_later handles each ask atomically: a refused ask creates no Item, Place or attachment. Replay the same operationId with the complete original ask, including its thing fields; changed content conflicts. A legacy thing ask without its original payload cannot safely replay. - buying_capture keeps a saved Item and its receipt when only an outcome is refused: saved[].outcomeRefused names that refusal and saved[].outcome is absent. A saved come_back outcome also records the Trip return intention. Bundle splits use accepted active Listed prices as tags, estimates only when no tags exist, otherwise quantity-based even shares; predictions, comparables and list prices never become tags or tag discounts. - Property corrections: properties[] accepts factId and expectedRevision from the read/save reply. observedOn selects the dated observation before its revision is checked; send factId when correcting the date. A guarded write cannot create a missing observation. - Dataset property_predicate bounds with unitCode are converted using the property's dimension; unknown or incompatible units are refused. Omitted units mean canonical bounds. - Purchase replays preserve omitted metadata and companion attribution. For bundles, tagCents uses the Item's per-unit or whole-lot basis, unitCents is per unit, and totalCents is a whole-line weight fallback. Quantity is recorded separately. Replay the complete original purchase-key set; person-adjusted shares and the recorded total are preserved, and incompatible replays are refused. Each bundle contributes to private seller statistics once; corrections apply the contribution difference. - Type writes refuse a child whose parent has the same normalized name, and refuse moving a subtree when its bounded descendant walk is incomplete; no partial ancestry rewrite is saved. - Photo attachments return stored capture-time and location fields on fresh replies, replays, link_existing and batch slots; on replays and link_existing only when the connection holds media.read and media is not withheld (otherwise those fields are null). AI copies stay within 1 MiB; oversized page previews are not reused. Image-URL fetches validate all resolved destinations and pin an approved public address while verifying the HTTPS hostname; redirects remain refused. - Read once at the start: buying_get_brief, the one first call, whose notesForYou carries up to six saved standing Preferences, whose contract carries six summary rules and the resolved work style, and whose lookingFor carries what they look for and their home base. Read buying_get_preferences for the full work-style playbook, more Preferences and documentation-only suggested defaults. Do not re-read standing guidance between saves. - Batch every sweep. A sweep of listings, a receipt, and a run of list ticks are one call each, never one call per record. - At a sale, use buying_capture: a whole sale — the things, their prices, their photographs and what the person did about each — is ONE call. One call is also one approval prompt, and somebody holding an object with a seller waiting cannot answer twenty. - Ask for a page and follow its cursor. Do not raise a limit until a whole table comes back. buying_list_types with a parent uses pageInfo.continueCursor and pageInfo.isDone, including when tag or visibility filters leave a short page. - Contact-sheet capped can mean the Trip source-record window was truncated, not that 1,000 eligible photos were found. A source-window warning means photos outside that window may remain; narrow the request to particular asks, Items or fileIds. - On buying_save_library_document, explicitly correcting kind to research clears the Source file as its body and uses the Markdown body instead. The original artifact remains evidence. Omitting kind preserves the existing file pointer. - buying_list_places resolves a saved shared Place's city for city/near filtering before suppressing the duplicate directory row. buying_get_place includes eligible private profile gallery photos, deduplicated with targeted photos; picturesCapped applies to the combined window. Private profile descriptions satisfy Place description gap checks. - Item missing-price guidance checks active sourced price observations, not condition or availability records or basis-only estimates. If the newest 100 observations are inconclusive, the guidance says older observations were not checked. - Visit-derived Trip stops use the visit's stored startedAt; purchase-derived stops use purchasedOn. Backfilled evidence keeps its occurrence date, and an explicitly named Trip remains authoritative. - Link in the same call that saves. Every save tool accepts the parent record it belongs to. - Prefer the one tool that summarises a block over assembling that block from five calls. - Several photographs are ONE begin_batch and ONE finish_batch on buying_attach_photo (up to 12), not a begin/PUT/finish per photo. A photo already in a Queue ask is ONE link_existing call and moves no bytes. Inline is only for a small image. - Save a measurement in the SAME call as the price it belongs to. properties[] rides buying_save_item, buying_save_items_batch, buying_capture and a buying_save_research checkpoint, and a property saved alongside a price is joined to that price; one saved in a later call about a different encounter is not. Permission layers: 1. Research assertions obey their referenced capture's live access. Erased dependencies are withheld during cleanup, and outcomes naming a Person obey that Person's current workspace and visibility. 2. Research-file Mission scope comes from the stored pass subjects, including when reading by artifactId. Both file and ordinary research permissions must be held on the same grant; media restrictions still apply. 3. Buying identity and OAuth consent prove the person and create the connection. 4. Buying grants and record checks decide which workspace records and actions the connection may reach. buying_answer_appraisal and buying_log_appraisal require queue.write as well as mission.write when queueItemId is supplied; ordinary appraisal saves without queueItemId need only mission.write. buying_save_complete_result likewise requires queue.write when a queue block closes an ask. 5. Queue metadata does not grant media access. Without effective media.read, or when media is withheld, Queue rows omit transcripts, legacy photo readings, camera locations and relations, automatic titles and summaries, temporary names and media URLs. The person's ask text and ordinary Queue/file metadata remain available. 6. Buying keeps separate confirmation for destructive, public, sharing, payment, permission, and outside-world actions; current tools do not perform those actions. 7. The AI client controls whether it can read a local file selected on the person's machine. 8. The AI client, account, or organization controls whether its code-execution sandbox may send the upload HTTP PUT. Many-photo sequence: 1. Preflight the entire photo set first: calculate the exact byte length and lowercase SHA-256 for every file before opening any upload session. 2. Call buying_attach_photo ONCE with mode begin_batch, one stable operationId, and one slot per file (up to 12): target, targetId, contentType, sizeBytes, sha256, and optionally caption, role ("evidence" for screenshots, listings, receipts and price pages) and makeMain. Each slot's uploadUrl stays on https://assistwithbuying.com and never redirects to Backblaze. 3. PUT each file's exact bytes to its own slot's uploadUrl with that slot's Content-Type and Content-Length, at most three at once. 4. Call buying_attach_photo ONCE with mode finish_batch and every uploadRef. Each slot comes back on its own with isMainImage; a slot whose PUT failed says so and can be PUT again to the same URL and finished alone. 5. A photo that is already in the workspace (a Queue ask's fileIds) is never uploaded again: file it with mode link_existing. A single photo may still use begin_upload → PUT → finish_upload; never cross one file's operationId, uploadRef, uploadUrl or bytes with another's. Terminal recovery: - Appraisal saves return pricesRefused alongside accepted prices, including on operation replay. Each refusal retains its reason and recovery; fix only the refused rows. Source-check summary and limitations accept up to 2,000 characters each. - Price corrections validate the merged range and derive channel from the linked Place even when venueId is omitted. expectedRevision guards corrections, retractions and restorations; a same-status retry is unchanged. Restored revisions carry settled scoring, and withdrawn predictions are never paired with a new listed price. - A photo awaiting removal or claimed by cleanup cannot be filed on another record. Use another available photo or upload a new one; never retry the removed fileId to create a reference. - buying_merge_type reports childTypesDone as well as notesDone and repointDone. Repeat with a new operationId while finished is false; remaining children keep the source unarchived. Supported vocabulary is copied completely, keeping the target's own properties and the source history. - upload_incomplete: finish saw no stored bytes yet; complete the existing PUT and retry finish while the session is live. - session_expired: stop using that capability and begin a new session; an expired session is never revived. - operation_conflict: the operationId belongs to different bytes or metadata; reuse the original bytes, or use a new operationId only for genuinely new content. - invalid_image: replace or re-encode the bytes; do not blame storage or retry unchanged bytes. - media_unavailable: stop and wait for the product operator; never fall back to an old store. - PASS_STOPPED / PASS_ERASED: the person closed that research pass. The whole save is refused and nothing was written. Send it again without passId, with the same question and rows, to keep the material under a follow-on pass; never retry the same passId. - grant or record refusal: reconnect or ask the person for the named Buying authority; changing a client network allowlist cannot grant product access. - Every error envelope carries requestId, one per HTTP request (also the x-request-id header). Quote it in buying_report_issue; an INTERNAL_ERROR's reference is the same id. Advice to resend with the SAME operationId appears only on tools that take one. - A request to change the site itself (a feature, a fix, a docs change) is not Queue work: file it with buying_report_issue kind request, which lands in Admin → Issues. - client network block: stop minting expiring sessions until that client's outbound-network policy is corrected; reconnecting OAuth does not change it. Claude-specific observation (2026-09-02): For Claude code execution, allow only assistwithbuying.com in Settings → Capabilities when the first-party upload PUT is blocked. All domains also works but is broader and unnecessary. Start a new Cowork conversation after changing the setting. This controls Claude's outbound HTTP request, not Buying OAuth or grants. Never allowlist Backblaze. This is dated troubleshooting evidence, not a claim that Claude has completed Buying's full deployed lifecycle. - Source: https://support.claude.com/en/articles/12111783-create-and-edit-files-with-claude - Source: https://support.claude.com/en/articles/13455879-use-claude-cowork-on-team-and-enterprise-plans ## Field conventions Read this once and you can guess the rest. Every tool description repeats it, so an agent that opens any single entry learns it there. - **Free text.** A general remark goes in `note`. Purpose-named text fields keep their own names — `rationale`, `summary`, `answer`, `question`, `trigger`, `headline`, `excerpt`, `claim`, `limitations` — because each says something `note` does not. `note` is the field for the remark that has no better name. - **Calendar days.** A day on the calendar — `date`, `saleDates`, `purchasedOn`, `fromDay`, `toDay`, `neededBy`, `dueOn` — is a `YYYY-MM-DD` string. An instant is the next rule. - **Times.** Every time is epoch milliseconds, in a field named for what happened and ending in `At`: `observedAt`, `checkedAt`, `accessedAt`, `decidedAt`, `openedAt`, `capturedAt`, `lastSeenAt`, `askedAt`, `nextAt`, `retryAt`. Never a date string, never a bare `at`, never `observedOn`. One published exception you will meet on a comparison cell and a product attribute: `asOf`, which means the same thing and is kept because those tools shipped with it. - **Replay keys.** Most writes carry a top-level `operationId`: a lowercase stable key of at least 8 characters, matching `^[a-z0-9][a-z0-9:._-]*$`. Resending a failed call with its ORIGINAL `operationId` replays instead of writing twice. One published exception: `buying_manage_connection` also still accepts the older spelling `opId`, and honours either; send `operationId`. The writes without one are safe to retry another way, and each says how in its description: `buying_update_capture_queue_item` (`expectedVersion` and the claim token), `buying_queue_for_later` (an `operationId` per ask), `buying_attach_research_file` (`artifactKey`), `buying_prepare_dataset` (`datasetKey`), and `buying_find_or_create_public_location` and `buying_withdraw_location_contribution` (repeating them changes nothing). `buying_add_location_contribution` takes an optional `operationId` (2026-10-04): send one, or a retry posts the same public note twice. - **Money.** Integer cents in an `...Cents` field, plus an explicit three-letter `currency`. Never floats, never a currency symbol inside a string, never two currencies in one range. - **Stable keys.** The per-record identity keys — `missionKey`, `productKey`, `optionKey`, `criterionKey`, `observationKey`, `sourceKey`, `decisionKey`, `suggestionKey`, `appraisalKey` — use the same lowercase stable-key shape as `operationId`. - **Unknown keys are refused, by name.** Every input schema is strict. A field name you guessed wrong comes back as an input-validation refusal that lists the unrecognized keys and the required ones you missed — for example `Unrecognized keys: "kind", "observedOn"`. That refusal is the correction; read it and re-send rather than retrying the same shape. These conventions were made uniform on 2026-08-29 across the eleven newest tools. The long-published tools were NOT renamed — the convention was read off them. ## Recaps, Context and titles Added 2026-09-17. Every building block — Item, Type, Place, Mission, List, Trip, Library record — now uses one page shape, and three of its parts are reachable from here. - **`recap`** is one short paragraph saying what the record is, in plain sentences. It is an optional input on `buying_save_item` (and each row of `buying_save_items_batch`), `buying_save_type`, `buying_create_place`, `buying_update_list` and `buying_save_mission`, and it comes back on `buying_get_item`, `buying_get_type`, `buying_get_place`, `buying_get_list` and `buying_get_mission_context` as `{ text, by, at, source }`. - `source` is `you`, `ai` or `objective`. `objective` means nobody has written a Recap and the page is showing the Mission's own Objective instead. - A Recap whose `source` is `you` is NEVER overwritten by an assistant. The write is declined quietly and the reply carries `recapSaved: false` and `recapDeclined`; the rest of the save is unaffected. - **Context entries** are the statements on a record's Context tab. They are statements, never questions. Write one with `buying_save_note` and `kind: "context"`: - `text` is the statement. - `detail` is the longer paragraph the row expands to. - `origin` is `{ kind, label, href? }` where `kind` is `you`, `ai`, `library` or `record`, `label` is the line printed under the text ("your AI researched this and it was saved here"), and `href` is offered only when there is somewhere real to open. - `targetKind` now also accepts `library`. - Each of the five reads named above returns `context: [...]` and `contextWindow`. Entries derived from the record itself — an Item's attributes and evidence, a Type's checks and price note, a Place's classification and hours, a List's description and cadence — come back with a synthetic `noteId` beginning `seed:` and `readOnly: true`. Change the field they come from and Context follows; there is no path that edits them as text. - Seeded entries also carry the research already stored in other tables, read-only, each with its origin: an Item's summary, each linked appraisal's answer (the entry's `text` is the verdict, `detail` the full answer) and one entry per appraisal evidence line; a Mission's criteria, decisions, source checks and sourced comparison cells; a Place's private description and notes; a List's entry reasons; a Trip's stop recommendations, instructions and research entries; a Library record's dossier sections, active claims and source checks. An entry may carry `more` — `question`, `confidence`, `attribution`, `sources` (http links), `lines` and `links` (records to open). Read it before re-researching: what is there was already found. Each linked read is a bounded prefix (for example the 10 newest appraisals). - A seeded entry that copies a field the record's own page already shows carries `origin.shownOn`: `overview`, `compare` or `appraisals`. It is that field again, not a separate note: a Type's overview, checks and price note; a Mission's Objective, criteria, decisions and Compare cells; an Item's appraisal answers; a Library record's dossier, sections and claims. An Item's own summary and evidence excerpts are drawn on no other tab, so they carry none. The person's Context tab counts these instead of listing them twice, so the notes you write there are what they see first (October 15). - A context entry is not counted as a note: `counts.notes` and `buying_list_notes` stay about what the person wrote. - **Queue titles.** `buying_update_capture_queue_item` accepts `title` (`aiTitle` is the same field under its older name) alongside any action, and `buying_queue_for_later` takes `title` on every ask (September 22). It stores the title and records that YOU wrote it, which is what the Queue row prints as "titled by your AI". A title the person wrote themselves wins and is not replaced; the person's own `text` in the ask is never rewritten by anything. ## Nothing is dropped silently: notApplied and warnings Every write tool either applies a field it accepted or tells you it did not. Two optional lists ride on the reply, and both are absent when empty: - `notApplied: [{ field, reason }]` — the save happened, but this field did not. The reason says why and where the change can go instead. Typical rows: a Recap the person wrote (an assistant's never replaces theirs), a name the person gave a List, a public directory fact an assistant may not change, a create-only setting sent to a record that already exists, a note sent with a status the Item already had. Nested rows are named where they were sent: `mission.recap`, `options[2].venueName`, `items[4].evidence[0].by`. - `warnings: [{ code, field, message }]` — the save went ahead in full, but something looks wrong enough to check. Today: `TITLE_IS_PLACE`, `TITLE_MATCHES_SAVED_PLACE` and `DESCRIPTION_SUMMARIZED`. - `gaps: [string]` — plain sentences naming what is still thin on the record you just saved: a Type with no overview or no checks, an Item with no sourced price or no Type, a Place with no hours or no description, a sale with no dates, a stop nobody has reviewed or located. Each sentence names the tool that fills it. Fill them in the same pass, or tell the person why not. It is a named list and never a score, so there is nothing to raise by writing filler. On a batch, each saved row carries its own `gaps`. - `derived: [string]` — plain sentences naming what the SITE worked out and wrote down itself, so you never have to keep this bookkeeping in your head and the person can always see why something changed. Today: a trip stop marked visited because a purchase was recorded at that Place (`buying_save_purchases_batch`, `buying_capture`), because a visit was recorded there (`buying_record_visit`), or because a price marked on the thing was recorded in person that day; and a new stop given the next position in the day when you sent no `order` (`buying_save_trip_stop`, `buying_import_sale`). Derivation never argues with a person: a stop the person marked `skipped`, or marked `not_visited` themselves, is left exactly as it is. Say what was derived when you report the save. Read all four after every write and tell the person anything that matters to them. Resending is safe. On `buying_save_complete_result`, `buying_save_mission`, `buying_save_option` and `buying_save_comparison`, a Mission's `status`, an option's `stage` and a criterion's `status` are KEPT when left out — only a new record takes the default (active, lead, active). Resending a paused Mission without `status` leaves it paused; resending a ruled-out option without `stage` leaves it ruled out; a dropped criterion stays dropped. `mission.recap` on `buying_save_complete_result` is saved on create and on an existing Mission. ## Titles say what a thing is; places say where An option's or Item's `title` is WHAT THE THING IS — maker, model, object — never where it is. Where it is sold or seen has its own fields: - `venue` (options) — free text: "estate sale", "eBay", "floor model at Costco". - `venueId` — a saved Place, from `buying_list_places` or `buying_create_place`. - `venueName` — a Place by name. Found in this workspace by name, or created as a private Place of this person's if there is none; the reply says `placeCreated` true or false. Works on `buying_save_option`, each option of `buying_save_complete_result`, `buying_save_item` and each row of `buying_save_items_batch`, the same way `typeName` works for Types. A Place made this way never reaches the public directory. A title that equals the place sent with it, or the name of a saved Place, is still saved — the reply carries a warning so you can correct it. ## Product and authority split Assist With Buying is the person's private durable buying workspace. It owns structured memory, permissions, workflow state, provenance, and records the person can inspect later. The person's chosen AI owns conversation, web research, synthesis, and judgment. The connector never authorizes outside-world action. No current tool contacts a seller, sends a message, negotiates, makes an offer, reserves an item, places an order, moves money, deletes records, publishes private research, or reveals a stored seller contact. ## Connection contract Endpoint: `https://assistwithbuying.com/mcp` The page written for you, generated from the live toolset, is `https://assistwithbuying.com/for-your-ai` (plain text at `https://assistwithbuying.com/for-your-ai.txt`). What the person wants remembered lives at `https://assistwithbuying.com/preferences`, and the connection itself — what is attached and what each assistant may do — is at `https://assistwithbuying.com/connect`. Transport is remote Streamable HTTP MCP. Authentication is OAuth in the person's browser. The endpoint publishes RFC 9728 protected-resource metadata at both `/.well-known/oauth-protected-resource` and `/.well-known/oauth-protected-resource/mcp`, served identically because real clients disagree about which to request, and returns `401` plus `WWW-Authenticate` before authentication. Every accepted bearer token must be a Clerk OAuth access token. JWT access tokens are verified against Clerk's JWKS, issuer, expiry, and access-token shape; an omitted audience is accepted, and any audience that is present must equal the exact Buying MCP resource. Clerk's current scoped-access JWTs omit an audience, so that compatibility path also requires Clerk's `at+jwt` type, client id, and `openid` scope and does not accept a browser session JWT. Buying uses a dedicated Clerk issuer rather than a shared portfolio authorization server. Existing opaque access tokens are verified online by Clerk's issuer-hosted userinfo endpoint. Token verification has three outcomes, never two: accepted, rejected (`401`, a statement about your token), and unreachable (`503`, a statement about Clerk, meaning your token was never judged — wait, do not re-run OAuth). A deployment with no authorization server configured answers `500`, because waiting will not fix that either. The server derives the subject/workspace on every request; tool schemas contain no user or workspace id. The modern 2026-07-28 protocol is stateless and returns no MCP session id. Older Streamable HTTP tools/list and tools/call clients remain compatible in stateless mode. No persistent gateway tool registry or registry-refresh step is part of the current runtime. If a client caches the previous tool list, disconnect/reconnect it. Send one JSON-RPC request per call. Batched arrays are refused, because a batch cannot be checked against the person's grant as a single call. `/ai` and `/ai.txt` describe setup by client capability rather than by product name. No AI client is named as supported until its own environment completes the full lifecycle under the grant boundary. Any client needs remote Streamable HTTP MCP plus browser OAuth. Some clients ask the person to approve each tool call; use the one-call save tool to reduce approval fatigue. ## Authorization: signing in is the approval Signing in to Assist With Buying and allowing your client IS the approval. On your first authenticated call Buying records the connection and grants it the ordinary toolset until the person removes it — every permission in the vocabulary, not a starter subset, with no end date and nothing to renew. `https://assistwithbuying.com/connect` is where the person sees what their AI did, narrows a connection if they want it to do less, and revokes one — not a gate they must pass first. A person who signs in, clicks Allow, and stops has finished. OAuth still proves only *who* is calling. The stored grant decides *what*, it is re-read from the database on every call — so revocation takes effect on the connection's next request without any token expiring, cache clearing, or deployment — and your token's own scopes are never read. One tier, as of 2026-09-01. The two-tier model this section used to describe is retired: - Granted by signing in: ALL TEN — `mission.read`, `mission.write`, `queue.read`, `queue.write`, `media.read`, `media.write`, `places.read`, `places.contribute`, `research.files.read`, `research.files.write`. - NEVER GRANTED THAT WAY: nothing. The list is empty, and that is the ruling rather than an oversight. On an Assist With site a connected AI has all of its abilities from the moment the person signs in. A person may opt into MORE restriction — they can narrow a connection at https://assistwithbuying.com/connect, or switch on a barrier that makes each new assistant wait for them — but nobody is ever made to opt into capability. So: never send a person to a permission screen to make ordinary work possible, and never describe a capability of this connector as locked pending their approval. - `places.contribute` WAS THE LAST ONE OUTSIDE, and moved in on 2026-09-01. Its argument was good and is answered rather than dismissed: a contribution lands in the shared public directory strangers read, and no scope in this product deletes anything. What answers it is the shape of the write — the note is privacy-scrubbed on the way in, it carries no link back to the person's Missions, and you can withdraw a note you contributed yourself. That care lives in the public-location path, not in a screen the person has to find. - A REQUEST IS NOW ALMOST ALWAYS THE WRONG MOVE. `buying_manage_connection request_access` still exists, because a future product decision could put a permission behind one and the barrier above can hold a whole connection, but asking for something this connection already holds is answered "already held" and files nothing. Make the call instead. - AN OLD CONNECTION CATCHES UP BY ITSELF. A grant issued under an earlier starting policy and never narrowed by its person is raised to the current default on its next call, and the person's authority ledger records which policy versions were involved. A permission you did not have yesterday may simply be present today. A connection the person narrowed is never widened back — their choice stands. - REFUSALS STILL HAPPEN, and they mean the person narrowed this connection deliberately. Relay that plainly. It is not a broken feature and not an invitation to ask them to undo it. - WHAT NO APPROVAL CAN EVER ALLOW is a separate question and did not move. Those ceilings are not permissions anyone can be granted; no request, grant, or ruling reaches them. - `media.write` MOVED INTO THE DEFAULTS ON 2026-08-29, and the argument it replaced is worth stating because it was a good one. Attaching writes stored bytes rather than sentences: a photograph is durable, it uses the person's storage, it may show their home, and you cannot take it back. What answered it was a live field appraisal: this is a product for photographing things at sales, and the assistant writing the Appraisal is the one holding the picture of what it appraised. Splitting those two across permission tiers meant the ordinary act the whole product is for failed halfway through, on a permission nobody had been given a reason to expect. The care is still enforced — a hard byte ceiling, the bytes re-read and hashed server-side (and, since 2026-09-24, kept exactly as sent, location included, private to the workspace), the file private and filed as attached BY YOU with your name and the date — but by the tool rather than by the tier. The person removes a photo in the product and narrows the permission in one click at https://assistwithbuying.com/connect. Ten scopes, each mapped to the smallest set of tools it needs: - `mission.read` — `buying_get_brief`, `buying_search_workspace`, `buying_read_library_document`, `buying_get_mission_context`, `buying_list_items`, `buying_get_item`, `buying_list_places`, `buying_get_place`, `buying_get_prediction_accuracy`, `buying_get_prompt`, `buying_list_suggestion_decisions`, `buying_list_lists`, `buying_get_list`, `buying_list_trips`, `buying_get_trip`, `buying_get_preferences`, `buying_list_types`, `buying_get_type`, `buying_find_comparables`, `buying_list_purchases`, `buying_list_notes`, `buying_list_records_needing_titles`, `buying_get_research`, `buying_prepare_dataset`, `buying_what_do_i_own`, `buying_find_prior_sightings` - `mission.write` — `buying_save_mission`, `buying_save_option`, `buying_save_comparison`, `buying_log_sources`, `buying_save_decision`, `buying_save_complete_result`, `buying_propose_suggestion`, `buying_set_item_status`, `buying_create_place`, `buying_import_sale`, `buying_import_sales`, `buying_use_public_place`, `buying_link_place`, `buying_unlink_place`, `buying_add_place_observation`, `buying_answer_appraisal`, `buying_log_appraisal`, `buying_record_outcome`, `buying_update_appraisal`, `buying_record_visit`, `buying_save_place_description`, `buying_save_list_item`, `buying_record_list_price`, `buying_save_trip`, `buying_save_trip_stop`, `buying_save_trip_stops`, `buying_add_trip_entry`, `buying_save_store_results`, `buying_save_item`, `buying_save_items_batch`, `buying_save_prices`, `buying_save_purchases_batch`, `buying_update_list`, `buying_save_type`, `buying_save_price_sheet`, `buying_capture`, `buying_save_types_batch`, `buying_report_issue`, `buying_save_note`, `buying_save_preference`, `buying_propose_preference`, `buying_save_item_decision`, `buying_save_titles_and_recaps`, `buying_save_list`, `buying_set_library_membership`, `buying_save_library_document`, `buying_save_prompt`, `buying_save_research`, `buying_save_identity`, `buying_void_purchase`, `buying_archive_type`, `buying_merge_type` - `queue.read` — `buying_list_capture_queue`, `buying_get_queue_activity` - `queue.write` — `buying_update_capture_queue_item`, `buying_queue_for_later`, `buying_store_check` - `media.read` — `buying_get_capture_image`, `buying_get_capture_images`, `buying_get_photo_sheets`, `buying_get_voice_note` - `media.write` — `buying_attach_photo`, `buying_attach_photos` - `places.read` — `buying_list_public_locations` - `places.contribute` — `buying_find_or_create_public_location`, `buying_add_location_contribution`, `buying_withdraw_location_contribution` - `research.files.read` — `buying_get_research_file` - `research.files.write` — `buying_attach_research_file` - no scope (it reaches no workspace record) — `buying_manage_connection` `research.files.read` and `research.files.write` joined the vocabulary and the defaults on 2026-09-21 with the durable research memory. They are deliberately NOT `media.read` / `media.write`: those govern the person's own photographs, and the consent sentence they carry says so. A research file is a page, a document or an original a source published, retained so a later assistant can reread the evidence instead of trusting a summary. EACH IS CHECKED AS A PAIR ON ONE GRANT. `buying_attach_research_file` needs `research.files.write` AND `mission.write`; `buying_get_research_file` needs `research.files.read` AND `mission.read`. Scopes are never summed across two connections, and a write-only connection cannot acquire source-reading authority by asking the server to hand a file back. `withholdMedia` withholds research files as well as photographs, and from 2026-09-21 that narrowing is ENFORCED on the server: a connection the person narrowed away from their media is refused `buying_get_capture_image`, `buying_get_capture_images`, `buying_get_photo_sheets`, `buying_get_voice_note` and `buying_get_research_file` alike, with a refusal that names the rule and says nothing about the file. `buying_manage_connection` maps to no scope at all. It reads this connection's own authority and files a request for the person; it reaches no workspace record, and requiring a permission for it would mean the connection holding nothing could never ask for anything. The catalogue is byte-identical for every caller. It is not filtered by what the person allowed, because a per-caller catalogue tells a token holder which permissions that token carries and cannot be cached. Seeing a tool listed is therefore not proof you may call it: a call outside the grant is refused with OAuth error `insufficient_scope`, naming the exact permission in the message and in a `required_scope` field. No refusal ever carries a `scope` parameter in `WWW-Authenticate` — that parameter means "ask the authorization server for this", and these permissions are the person's product grant, not Clerk's to issue, so re-running OAuth can never produce one. Rules that follow from this, and that a connected AI should behave well under: - A connection that holds nothing is never listed an empty tool list. Waiting for the person's approval, removed by them, or over the connection limit, it lists exactly `buying_manage_connection`, whose description starts with which of those it is and the link that fixes it (https://assistwithbuying.com/connect); the list also carries an `instructions` string saying the same. Relay that sentence; do not reconnect in a loop. - A refusal arrives as a normal tool result with `isError` and HTTP 200, carrying code `GRANT_REQUIRED`. Read it and relay it, including its `recovery` line; do not retry. - A refusal saying the connection was removed means the person revoked it, never "this capability does not exist". Reporting it as a missing feature sends the person to hunt for an outage instead of reconnecting. - HTTP 503 means Buying could not reach the authorization server, so your token was never judged. Wait and retry. Do not re-run OAuth; a 401 would have said the token was bad, and this is deliberately not that. - Revoking is a decision. Signing in again does not undo it; the person creates a new connection themselves. - A person may switch on a barrier holding each new AI until they approve it by hand. It is off by default, and turning it on does not revoke anything already granted. - Scopes are never summed across two grants. One grant must permit a call on its own. - A grant may cover only selected Missions. A call naming a different Mission is refused, and a call naming no Mission is also refused. An Item the call names (`findingId` on `buying_save_item`, `buying_save_item_decision` or a capture or batch row, or an Item in a Mission's `links`) must be on one of those Missions too: one on another Mission or on none is refused with `OUT_OF_MISSION_SCOPE` and left untouched, so a narrowed connection cannot move or adopt an Item into its Mission. Research keeps the same rule: an assessment's `selection` may not name an Item, a Mission, an Appraisal or Purchase of an Item, or a price that is outside those Missions (`OUT_OF_MISSION_SCOPE`), and an assessment resting on one is withheld from the connection. - A grant may be bound to one AI client, claimed on first use. Another client belonging to the same person does not inherit it. - `buying_save_complete_result` requires `queue.write` as well as `mission.write` when it carries a `queue` field. Without that scope, omit the field and save the Mission result. - A Queue directive never widens a grant. - No grant can allow seller contact, spending, purchases, permanent delete, export, publication of private research, or reaching another workspace or Assist product. These are server ceilings, not defaults. - The exact wording of each scope the person approved is stored with their grant, so a later rewording cannot change what the record says they agreed to. ## Prompts The site publishes a prompt library at https://assistwithbuying.com/prompts, one page per prompt at `/prompts/`, and the whole library as one plain-text fetch at https://assistwithbuying.com/prompts.txt. A prompt is text the PERSON hands to you, not something the site runs: there is no AI in Assist With Buying. The launch set is `plan-my-sale-day`, `thrift-store-showdown`, `write-up-this-sale`, `marketplace-or-store-sweep` and `first-session`, plus the two written research loops (`establish-a-fair-price`, `check-a-used-listing-before-you-drive`) rendered as prompts. Each carries `whenToUse` and the real tool names it leans on. `buying_get_prompt {slug?}` — with no slug, `{slug,title,whenToUse,kind,url,mine}` for every prompt plus a `count` and a `mineCount`; with a slug, `{slug,title,whenToUse,text, tools,source,url}`; with an unknown slug, a refusal carrying `validSlugs`. Read-only, `mission.read`, nothing written by reading one. THE PERSON'S OWN COPY WINS. `mine: true` on a listed prompt means they have their own version of it, and asking for that slug returns THEIRS — `source: "yours"`, with `forkedFrom` naming the library prompt it started from — rather than the library's words. `source: "library"` means you are reading the site's own text. A person who reworded a prompt did it so you would follow their version; say in chat which one you are working from. WORK STYLES ARE PROMPTS TOO (2026-09-21). Ballpark, Focused and Thorough are in the library with `kind: "work_style"`, at the slugs `work-style-ballpark`, `work-style-focused` and `work-style-thorough`, and so is any style the person wrote for themselves. A style is a standing instruction about HOW you research, not a job to run, and reading one returns its `styleRef` plus — for one of theirs — its stable `revision` and `revisionId`, which is the exact wording a saved result will quote. The three starters are read-only; `forkFrom` makes the person an editable copy. `buying_save_prompt {operationId, slug?, forkFrom?, title?, whenToUse?, text, kind?, researchDepthHint?}` (`mission.write`) — keep their own version. `forkFrom` names a library prompt to start from and everything you leave out is taken from it, so "that one, but always the north side first" is one call; `slug` alone files a NEW VERSION of a prompt they already have, and the wording it replaces is kept beside it, dated and attributed to you (the last 20 are held). `text` is capped at 12,000 characters. Their prompt is a private workspace record, visible only to them at the `path` the reply carries; the site's library is not writable by any tool. Write what they asked for, not your improvement of it. `kind: "work_style"` files it as a work style: use it when they are describing how they want research done rather than a job to run. Its text is bounded to 8 KiB, because every result resolved under it quotes it. `researchDepthHint` is OPTIONAL and says which of the three starters it is closest to; their prose is the instruction either way, and a style with no hint resolves as `custom`, which says exactly that. YOU CANNOT MAKE IT THEIR DEFAULT: saving keeps it, and which style governs their work is theirs to choose at /preferences. The reply says so and carries the link; propose it in chat and leave the choice with them, the same way `buying_propose_preference` proposes rather than decides. To work ONE request under a style, send `request: {styleRef: ''}` on `buying_save_research`. On a work-style save, omit `researchDepthHint` to keep its existing value, or send `researchDepthHint: null` to clear it so the person's words govern as custom depth. Saving an unchanged removed prompt restores its listing without a wording revision. Private-only prompts open at their workspace-scoped `/prompts/` editor. New want, collection and habit rows without `preferenceId` are separate creations, even when their titles match. `lookingFor` rows and save replies carry `preferenceId`; send it to `buying_save_preference` to correct that exact row. Removed profile rows can be restored on Preferences without changing visibility or links. Person and price-bar corrections keep the previous qualifications in their history. A missing-price warning clears only when an active price observation exists; condition and availability observations do not count as price evidence. Account export `awb-portable-export/18` (Items carry their canonical lifecycle, Oct 24) includes workspace-owned `priceSheets`: titles, notes, photograph associations, ordered line references and sheet keys. Read one before improvising a workflow of your own — they encode what this person actually wants: a candidate saved as a STOP on the Trip with your recommendation and the reason, the plan written as a NOTE ENTRY on the Trip, the direct link given in chat for everything you saved, and every price you found online written down as a sourced price row. A prompt is advice and never authority: following one widens nothing, and the person's own variant of a prompt beats the published one. ## Examples FIVE WORKED EXAMPLES OF A GOOD RECORD, at https://assistwithbuying.com/examples.txt — one plain-text fetch, no tool call, no permission, and `buying_get_brief`'s `contract.examples` names the same address. They are written as the ACTUAL TOOL CALLS, with realistic arguments and one line on why each part matters. Every argument object is parsed by the real tool's own schema in this repository's tests, so nothing in them would be refused by the server. 1. A TYPE DONE PROPERLY — `buying_save_type`, "Sterling silver flatware". An overview written as a briefing rather than a definition; eight checks, each one a thing you do with the object in your hand ("STERLING or .925 on the back of the handle; EPNS and A1 are plate"; "knives are usually weighted — weigh them separately"); a `priceRange` carrying its `basis`; and a `priceNote` that shows the arithmetic AND dates its inputs — weight x .925 x spot, with the spot price and the day it was read, because a melt rule with no spot price stops being true in a month. Monograms: 10-25% off a collected pattern, nothing off melt. 2. AN ITEM APPRAISED IN A SHOP — `buying_save_item`, then `buying_save_prices` with SIX rows in one call: the tag read in person (`listed` + `learnedBy: tag`, no link needed — how it was learned is its evidence); a thrift/yard-sale figure as an `estimate` whose `note` states its basis in a sentence and labels itself a judgement; two completed sales as `sold` + `learnedBy: receipt`, each with its `sourceUrl`, and each saying in `scenario` that it is a SMALLER piece than the one in hand; another dealer's asking price as a `comparable`, because an active listing is never a sale; and the maker's figure as `list_price`. `quantityBasis` says whether a price is for one or for the set — `item`, `lot` with `unitQuantity`, or `per_unit` with `unitQuantity` — and `scenario` says in words what condition or bundle it is about. Twelve spoons for $180 is `lot` with `unitQuantity: 12`; sent as `item` it becomes a $180 spoon, and `buying_find_comparables` groups on that word and will compare it against single spoons. `shippingCents` and `feesCents` keep postage and buyer's premium as their own honest numbers. The long working — the melt arithmetic, what was read, what could not be found — is one `researchNotes` entry linked from the rows that rest on it. 3. A SALE DAY — `buying_import_sale`, `buying_save_trip_stop` twice, then `buying_save_purchases_batch`. The listing you read is imported in one call with its hours and the markdown it published; the address stays `cross_streets` because that is what the listing gave. One stop is `going`, carrying the PERSON's `selectionReason` in their own words and your separate `recommendation`; one is `pass`, and its reason is about that sale and not a rule about garage sales. `sources[].checkedAt` says when you last looked. The receipt closes the day with `saleDay`, `discountPercent`, `tagCents` beside what was paid, and `boughtBy` for the line a companion paid for. 4. A RESEARCH PASS THAT KEPT WHAT IT READ — `buying_save_research` twice. The first call is a CHECKPOINT sent as soon as two sources are read, not saved up for the end: the auction lot's own words go in `captures[].text` with `completeness: 'complete_document'`, and the regional archive that demanded a login is kept as an encounter with `status: 'blocked'` and the reason, because dropping the sources that refused you is how a partial search comes to read as a complete one. The second call is `action: 'submit_assessment'`: `selection` names the exact capture and the exact price row (by the revision that was read), `basis.method: 'source_reported'` with its uncertainty stated, and a typed money `output` whose `rangeMeaning` says `estimate_interval` rather than leaving a reader to guess. 5. A BALLPARK IN ONE CALL — `buying_save_research`, `action: 'submit_assessment'`, and nothing else. `request: {depth: 'ballpark'}` records that THIS request asked for a quick answer; `basis.method: 'general_knowledge'` carries the three assumptions that would move the number; `input: {kind: 'no_external_inputs', reason}` says plainly that nothing was looked up. No pass, no sources. Manufacturing a research pass to have something to save is the failure this example exists to prevent. Read them before the first save of a session. The contract says what to do; these show what done looks like, which is the part a rule cannot carry. EXAMPLE RECORDS ARE A DIFFERENT THING FROM THESE. A person's workspace may hold a few invented records titled "Example - ..." — a Type, a Mission and a Trip with two stops — so the screens are not empty on day one. They are marked, badged as invented on the page, and removed in one click from Buying Preferences. There are deliberately NO example Purchases and NO example price rows, so nothing invented has ever been inside a money total, the stats page, an export of real spending or a seller's prediction record. If you meet one, treat it as what it says it is and do not cite it as evidence about a market. ## Exact 96-tool catalog ### Discovery and reads - `buying_get_prompt` — the Prompts library, listed or one in full by slug, with the person's own copies folded in: `mine: true` on a listed prompt, and `source: "yours"` when the slug you asked for is one they have reworded for themselves. - `buying_get_brief` — first call. Bounded recent Missions plus actionable Queue handoffs and the next recommended tool. SINCE 2026-10-07 it also returns `saleSources` `{rows, fromDirectory, note}`: where the person looks for estate, garage and yard sales — at most 25 Places, each with `name`, `placeId`, `isPublic`, `publicPlaceId`, `website`, `guideFrom` (`own` or `shared`) and a `searchGuide` `{covers?, urlPattern?, dateFilter?, quirks?, saleTypes?}` (sale types: `company_estate`, `family_estate`, `garage`, `yard`, `moving`, `other`). Their own Places with a guide and their saved copies of shared sale sources come first; when they have none, the shared directory's curated sources (EstateSales.NET, EstateSales.org, Craigslist garage & moving sales, Facebook Marketplace, gsalr.com, yardsales.net, Garage Sale Finder, AuctionZip) with `fromDirectory: true`. Follow each guide's `urlPattern`, `dateFilter` and `quirks` — Craigslist map pins are approximate, so read the seller's own location words; EstateSales.NET releases exact addresses late, often the night before. There is NO sources table: a sale source is a Place. Record one of the person's own by sending `searchGuide` on `buying_create_place` (send the same name to add a guide to a Place they already have, including their saved copy of a shared source); the reply says `searchGuideSaved`. A shared source's guide is curated by the site and is never written by a tool. It also carries three things that would otherwise each cost a call: `grantedScopes`, this connection's live permissions read from the grant itself, so you know before you promise whether you may attach a photograph; `standingBrief`, the compact criteria (label and target text, at most twelve) of a Mission the person tagged `read-me-first` or `standing-brief` — their price bars and standing rules, which apply outside that Mission too — with a boolean saying whether more were cut and the `missionId` for the full read; and `openQuestions`, the workspace's own open questions that are waiting on THEM rather than on you. Ask those at a quiet moment, never while they are holding an object with a stranger waiting. SINCE 2026-09-19 it also carries `contract`, which is how to work here before anything goes wrong: `styles` (see `buying_get_preferences` below), `rules` — eight to ten plain sentences adapted to the person's work style, including "after every save, tell the person in chat what you wrote and give the url from the reply", "put the substance in chat too — prices, reasoning, what you found — not only on the site" and "every price you find online becomes a sourced price row" — and `prompts`, pointing at the worked prompts (`buying_get_prompt`, `/prompts`). SINCE 2026-09-21 `styles` also carries `request` — the resolved contract, with the governing style's name, its EXACT words and where the choice came from — plus `availableStyles`, every other style this person could ask for as a name, a `styleRef` and a one-line "when to use", NEVER their full text; fetch one in full with `buying_get_prompt` if it is actually wanted. `interpretation` beside them is one honest sentence about how this site knows what was asked for, and it says "unknown" when nothing was conveyed rather than pretending the default was chosen. `guidance` is unchanged, for clients that already read it. - `buying_search_workspace` — ONE SEARCH ACROSS EVERY RECORD, and the thing to reach for instead of listing a table and looking through it. Since 2026-09-16 it also covers Types, Lists, Places, Purchases and Notes, and it returns `hits`: one page across all of them, newest first, with the record `kind` on every entry and a cursor to follow. The per-family arrays described below are the older shape and are unchanged, so read whichever suits you — `hits` is the cheaper habit. Purchases carry no search index, so they are matched over the newest rows only and `purchaseScanWindow` says how many were looked at: read an empty result there as "not in that window", never as "never bought". The per-family detail: compact search across Missions (title AND goal), reusable products, Mission-specific Findings/options (title AND summary), and Appraisals (title, question, answer, and verdict). An Appraisal comes back with its status, its `askingPrice`, its `venueHint`, its recorded outcome, and the `version` an answer needs, so finding one is enough to act on it. One appraisal per item per sale makes them the highest-volume record here; search before asking the same question about the same lamp twice. This is the same fan-out the person's own search uses, so a Mission findable on the website is findable here. Every returned Item carries the `missionId` it belongs to. Search before creating. - `buying_get_mission_context` — bounded Mission hydrate: criteria, products, Findings, comparison cells, dated observations, source outcomes, decisions, and recent events. **Read `comparison`, not `cells`.** `cells` is only what somebody wrote by hand with `buying_save_comparison`; on a Mission your research filled in it is usually empty, and empty is not evidence of an empty Mission. `comparison` is the board the person sees: `criteria` (each with `asked`, `must` and what it is bound to), `items`, and `cellsByKey` keyed `:`. Each cell carries `valueText`, `state` (how we know — the only thing that colours it), `verdict` (`pass` · `near` · `fail` · `unknown` against the criterion's range), `origin` (`saved` · `measurement` · `attribute` · `property` · `none`) and, when nothing is known and something could be, `settleHint`. `verdict` is a judgment and `state` is how we know — a measured failure and a measured pass are equally well known — and `not_judged` (real words that do not answer the number, or a criterion stating no number) is neither a failure nor `unknown` (nobody has recorded anything). Every ACTIVE criterion is a row, including ones an AI proposed and nobody has accepted; those carry `proposed: true` and `proposedBy`, and are never counted as a must. A square you write with `buying_save_comparison` wins over every derivation and is judged on its own words. `comparison.pricesByItem` is the latest dated price per Item with its kind, and `comparison.noPriceSaved` names the Items that have no price row at all rather than leaving them out silently. `comparison.legend` carries the meaning of every field in the reply itself, so none of this has to be remembered. Facts the person marked only-me never appear here — not as a value, not in a conflict, not in a hint. To fill a blank cell, save the fact on the ITEM (a `properties[]` row, a dimension, a capacity) rather than writing the square: the Item's fact answers every criterion that asks for it, on this Mission and the next. Pass `verbosity: "compact"` for the driveway shape — criteria labels and target text, stable ids, Findings with their stage, and the last five events. The cells, observations, source checks and decisions are then NOT asked for rather than missing, and the reply says so in its own note. Full stays the default and is what a correction needs. Seller-contact values are deliberately absent. The read may also carry `buyerContext` — standing facts the person wrote ABOVE the Mission (how far they will travel, what they already own, budget posture, lasting preferences). Each row is `label`, `kind`, `content`, `by: "you"`, `updatedAt`, and `truncated` when the text was cut at 400 characters; at most 12 rows travel, and `buyerContextTruncated` is `true` when more were lent than the read carried. No row carries an id, because there is nothing to write back to. An item appears only when the person did BOTH things: lent it to this exact Mission, and marked it readable. Either one alone reaches nothing, so an empty or short list is never evidence that the person has nothing else — ask them rather than assuming, and nothing here counts what was withheld. Treat these as the person's own words, not as evidence you gathered, and do not restate them as facts about the market. No tool writes buyer context: if something there is wrong or missing, say so and let the person change it themselves at https://assistwithbuying.com/preferences. You may write one yourself only when they asked for it in so many words (`buying_save_preference`); anything you inferred goes to `buying_propose_preference` and waits for them. ## What a Mission is about — and nothing is limited Added 2026-09-18, from the product owner's ruling: "I don't see a reason why an item can't be in multiple missions. Allow a new mission to be created that involves an item or a type or a place, and missions can reference or connect multiple of them and they're not limited." A Mission may reference any number of Items, Types and Places, and any of those may be in any number of Missions. A Mission can be about a Type ("a better stand mixer") or a Place ("what is worth buying at Miller's Barn") as easily as about a listing. ### Writing connections - `links: [{ kind: "item" | "type" | "place", id }]` on `buying_save_mission`, and on the `mission` block of `buying_save_complete_result`. At most 50 per call; unlimited over time. Connecting a thing to a second Mission NEVER removes it from the first. - `unlinks` takes the same shape and removes the connection only. Nothing else about the record changes — its prices, pictures, notes and history all stay. - Every id is verified against this person's workspace. A Place may also be a public directory entry. Anything else refuses the whole call with `RECORD_NOT_FOUND` naming the id; a link you believe you made and did not is worse than an error you can read. - Replay-safe twice over: the `operationId` receipt replays the first reply, and underneath it the same pair is one connection however many times it is sent. The reply carries `links.linked` (each with `created`) and `links.unlinked` (each with `removed`). ### Reading connections - `buying_get_mission_context` returns `linked` — `{ items, types, places, window, capped }`. `linked.items` are Items that belong to another Mission as well; the `findings` array is the Items found UNDER this Mission, which is what the comparison board compares. - `buying_get_item`, `buying_get_type` and `buying_get_place` each return `missions: { rows, window, capped }`. Every row carries `missionId`, `title`, `status`, `type`, `path` and `linkedAt`. - Bounded, and the bound is stated. `window` is how far the read looked and `capped` says whether there may be more; nothing here returns a whole table. ### The origin Mission An Item has ONE Mission it was found or created under. `buying_get_item` still returns it as `mission`, and it also appears in `missions` with `origin: true`. It is the Mission the option keys are unique within and the stages are recorded against, so it is a single field and always will be. Every other Mission the Item is in is an ordinary connection. Types and Places have no origin — they reach a Mission only by being connected to one. Growing an appraisal into a Mission is still the person's act, not yours, and it no longer refuses when the Item is already in a Mission: the Item joins the new one as well. ### Durable Mission writes - `buying_save_mission` — replay-safe create or optimistic update. `missionKey` prevents a duplicate create and `expectedVersion` prevents a stale overwrite. - `buying_save_option` — upsert one Mission-specific option and optional reusable product; append evidence, observations, and source outcomes. Products and offers stay separate. `title` is what the thing is, never where; the place goes in `venue` (free text), `venueId` or `venueName` (find-or-create a private Place; the reply says `placeCreated`). `stage` left out keeps an existing option's stage. The reply carries `stage`, `venueId`, and `notApplied`/`warnings` when there are any. - `buying_save_comparison` — upsert stable criteria and criterion-by-option cells. The cell state means how the value is known. An inferred criterion is a proposal unless `userConfirmed` is true. A criterion's `status` left out is kept: a dropped one stays dropped. - `buying_log_sources` — append checked, partial, blocked, requires-user, or failed source outcomes. A source that produced nothing is still evidence. - `buying_save_decision` — append a selected, deferred, no-purchase, or needs-more-research result with rationale and remaining uncertainty. Later decisions do not erase earlier ones. - `buying_save_complete_result` — preferred normal save. In one atomic approval it can save the Mission, up to 20 options/products, up to 100 observations and source outcomes, comparison criteria/cells, an optional decision, and an optional currently claimed Queue outcome. Use granular tools for partial work and corrections. `mission.recap` is saved on create and on an existing Mission (a person's own Recap is kept and reported); `mission.status`, `options[].stage` and `criteria[].status` are kept when left out. The reply carries `status`, `recapSaved`, and one `notApplied`/`warnings` list for the whole call, rows named like `options[2].title`. ### Items, places, Appraisals, and suggestions The four surfaces beside Missions. Every tool here is covered by `mission.read` and `mission.write`: each one reads or writes the person's own private workspace, carries your provenance, and can be corrected by whoever wrote it. `buying_list_places` is deliberately NOT under `places.read` — that permission is community data by its own definition, and this listing carries the person's private places too. - `buying_list_items` — bounded cursor page over every item in the workspace, which is what a Finding is called when it is seen outside one Mission. Filter by optional canonical lifecycle or legacy status (`researched`, `appraised`, `viewed_in_person`, `tested`, `purchased`, `listed`, `sold`) or by category. Seller-contact values are absent; a row reports only that one exists. - `buying_get_item` — one item in full: evidence lines, the whole append-only status history with who moved it and when, its Mission, its category, and the place it came from. AND ITS PRICE HISTORY (2026-09-18). `prices` is a bounded block — the newest 50 rows, newest first, with `window` and `capped` — and every row carries the `key` it was written under along with `kind`, `amountCents` or `minCents`/`maxCents`, `currency`, `observedAt`, `scenario`, `learnedBy`, `channel`, `state`, `venueId`, `sourceUrl`, `note`, `appraisalId` and `corrected` (true when the row has been fixed at least once). THIS IS WHERE THE KEYS COME FROM: `buying_save_prices` corrects a row by its key, and without reading them back a fresh session invents a new key and writes a duplicate. - `buying_set_item_status` — record what has happened to one item, with an optional note and an optional `venueId`. History is append-only, so `purchased` is added after `viewed_in_person` rather than over it, and the row is filed as moved BY YOU. Marking something purchased is bookkeeping about what the person already did: this call never spends money, makes an offer, or completes a purchase. A status the item already has adds no history row, so a `note` sent with it comes back in `notApplied`. Needs a stable `operationId`. - `buying_list_places` — the shared directory entries plus this workspace's own places, with an optional `kind` and a `scope` of `all`, `mine`, or `public`. **This workspace's own come first, then everyone's**, newest change first inside each group, and `ordering` in the reply says so. Every row carries `visibility`: `public` is community data, `private` is the person's own record. Every row also carries `shape`, `isChain` and `chain`. `isChain` true means the row is a **chain** rather than a store — Goodwill, Costco, eBay — with no address and no hours, because a chain has none; that is not a missing address, and nothing should fill one in. A private row's `chain` names the chain it is a branch of ("my Goodwill on 6th Street" names "Goodwill"). Link an Item to the person's own branch when they have one and to the chain when they do not. Notes on each row are the caller's workspace only; nobody else's notes about the same place are readable, ever. Each note reads back in the same words the write takes — `note`, `observedAt`, `actor`, and `price` — so a listing needs no translating before you write. **Mine, everyone's, or both (2026-09-18).** Every row also carries `scope`: `mine` is one of this person's own Places, `everyone's` is the shared directory — the same two words the Places page and the Place picker use. `city` (alias `near`) narrows to a readable city line, "Tucson, AZ" or "Glendale", matched against the city a Place records; there is NO RADIUS and no distance filter, so nothing here says how far away anything is (a Place's private point, when it has one, only matches photos to it). A public row carries enough of its profile to choose by without a second call: `recap` (what it is and what it has), `whatItHas` (short lines — "mid-century furniture", "tools", "vinyl", "booths by many dealers"), `priceLevel` (`thrift`, `budget`, `mixed`, `mid`, `high`) and `cityLine`. A row of the person's own that reuses a directory entry carries `usesPublicVenueId`, and the directory entry it reuses is then listed once — as theirs — rather than twice. A REFERENCING ROW REPORTS THE SHARED DESCRIPTION (2026-09-18). The private row holds no profile of its own — that is what keeps the shared recap, hours and address current — so `recap`, `whatItHas`, `priceLevel` and `cityLine` are filled in from the directory row it references, and `describedBy` says which side described it: `own` when the row carries its own words, `shared` when the directory does. Saving a directory Place into the person's own Places therefore never makes you see less about it than before. - `buying_use_public_place` — USE A SHARED DIRECTORY PLACE IN THE PERSON'S OWN PLACES, and get back the `venueId` to use everywhere else (2026-09-18 Places amendment). What it makes is a REFERENCE, NOT A COPY. The row returned is private to this workspace and points at the directory entry: the shared `recap`, `whatItHas`, address and hours stay on the directory row and stay current when the product owner corrects them, while this person's notes, visits, prices, Items, Missions and search areas attach to the row this returns. Nothing written on their row ever reaches the shared one, and this call writes nothing public — it is `mission.write`, not `places.contribute`. Use it whenever `buying_list_places` returned the place with `scope: "everyone's"`. Calling `buying_create_place` with that place's name instead gives the person two "Goodwill" rows and splits their history across both. IDEMPOTENT: a directory place they already saved, or a place of their own with the same name, is returned and linked rather than duplicated, and `created` says which happened. Returns `venueId` (theirs), `publicVenueId` (the shared row it references) and `created`. Needs a stable `operationId`. - `buying_create_place` — add a place to THIS WORKSPACE'S own list, so a note can be kept about it. The place is PRIVATE to the workspace: it is not added to the shared community directory, nobody else can see it, and nothing in this call can publish it — `chain` included. Contributing a place to the shared directory is a separate act under `places.contribute`, through `buying_find_or_create_public_location`. **Chain or branch.** The shared directory already holds chains as rows with no address (`isChain`). Do not copy one into the workspace. Create a place here when the person has a particular branch with its own hours and notes, and set `chain` to the chain's name ("Goodwill" for their Goodwill on 6th Street): the two pages link to each other and neither is merged into the other. When the person means only the chain, link the shared row's `venueId` instead of creating anything. A place whose name already exists here is handed back rather than added twice, and the reply says which happened in `created`; on that match a detail the saved place lacks (`where`, `next`, `nextAt`, `summary`, `url`) is filled in, and one it already has is kept and listed in `notApplied`. Returns the `venueId` that `buying_add_place_observation` needs. **A COMPANY AND ONE OF ITS SALES ARE TWO PLACES (2026-09-18).** An estate-sale company is a Place with `shape: "organizer"` — a name, a website, the markdown it typically runs, and no address, because an organizer runs its sales at other people's addresses. One of its sales is a Place with `shape: "event"` that names it in `companyVenueId`, with `saleDates` and `hours` of its own. `saleDates` is `{start, end?}` — ONE span, `end` absent meaning one day, and day N of the sale is start + N − 1. The markdown a company typically runs is `discountSchedule` on the ORGANIZER, `{rows: [{day?, weekday?, startTime?, category?, percentOff, note?}], source?, by, at}`, so every page that draws an expected discounted price can say whose figure it is and where it came from. **`discountSchedule` IS AN ARGUMENT TO THIS CALL.** Send the ROWS alone — `[{day: 1, percentOff: 0}, {day: 2, percentOff: 50, note: "half off Sunday"}]`, at most fourteen, each naming a day of the sale (1 is the first) or a `weekday` — and `by`, `at` and `source` are stamped server-side, because who recorded a schedule is not something a caller gets to claim. It is written to the organizer: this Place when it is or becomes one, otherwise the company named in `companyVenueId` or `companyName`. A schedule sent with a plain shop that names no company is NOT written and is reported in `notApplied` with what to do instead. The reply answers `discountScheduleSaved` either way, and `buying_get_place` returns the stored schedule with `saleDates` and `companyVenueId`. Send `companyName` instead when you do not have the company's id and this call finds their Place or creates one; send one or the other, never both, and `companyCreated` says which happened. A sale INHERITS the company's defaults for display rather than copying them, so correcting the company corrects every sale it ever ran. `shape` also takes `branch`, `chain`, `platform` and `online-store`; naming a `chain` still makes a row a branch, whatever else is sent. Needs a stable `operationId`. - `buying_import_sale` — ONE CALL for an estate, garage, yard or moving sale you have ALREADY READ on any listing site (EstateSales.NET, Craigslist, Facebook Marketplace). With no company running it, leave `companyName` out and the sale is saved on its own. **You read the listing; this site does not.** Buying never fetches, scrapes or parses a third-party listing page. You are already on that page and already allowed to be, so you pass what you FOUND there and this call writes it down. Send only what the listing actually said: an address it did not give is not a guess to make here, and `locationPrecision` is how you say that cross streets are cross streets. It writes four things in one transaction: the COMPANY as a Place with `shape: "organizer"` (found by name in this workspace, or created; skipped when `companyName` is left out); the SALE as a Place with `shape: "event"` naming that company, carrying `saleDates` (`{start, end?}`, built from the listing's `startDate` and optional `endDate`), `hours`, the address and `listingUrl`; a STOP on a Trip (`tripId` for one you already have, or `createTrip: true` with no `tripId` for today's); and the listing's photo ADDRESSES on the sale, so its page shows the grid you pick items out of. **THE MARKDOWN THE LISTING NAMED GOES IN THE SAME CALL.** When it says "50% off Sunday", send `discountSchedule` as rows — `[{day: 1, percentOff: 0}, {day: 2, percentOff: 50, note: "half off Sunday"}]`, at most fourteen — and it is written to the COMPANY this call finds or creates, sourced to `listingUrl`, with `by` and `at` stamped server-side. That is what makes the sale's Items read "tag $120 · likely $60 tomorrow" instead of nothing. The reply answers `discountScheduleSaved`. A PHOTOGRAPH KEEPS ITS ADDRESS AND A STORED COPY WHEN FETCHING SUCCEEDS. With `media.write`, Buying tries each direct image URL through `buying_attach_photo`'s checked fetch path and links its fileId to the sale's matching listingPhotos row. The grid uses the stored copy if the remote image disappears; refused fetches retain the remote address and are reported per image. The same operationId also repairs a missing association to an already stored copy without making a second file. PARTIAL FAILURE IS REPORTED: if the stop or the photographs fail, the company and the sale still stand and a `problems` array names the step and the reason. Fix the one argument and call again with the SAME `operationId`, which replays rather than making a second sale. Returns `companyVenueId`, `saleVenueId`, `tripId`, `stopId` and what happened to the photographs. Use `saleVenueId` everywhere afterwards: prices, Items and notes attach to the sale, not to the company. Needs a stable `operationId`. - `buying_add_place_observation` — one dated note about what a place was right or wrong about. The sentence goes in `note` and its time in `observedAt`. ALWAYS workspace-private, even on a shared directory entry, and nothing here moves it into the shared listing. Use `buying_add_location_contribution` for the separate, permissioned, anonymized path into the community directory. Needs a stable `operationId`. It also takes an optional structured `price`, so a price seen at a place becomes something that compares rather than prose somebody has to re-read: `item`, `amountCents`, optional `currency` (USD by default), the `unit` it was priced by (`each`, `per gallon`, `per cubic foot`), and an optional `containerSize` (`#15 can`, `5 gal`, `2 cu ft`). WRITE THE SENTENCE IN `note` AS WELL, ALWAYS — the price sits beside it, never instead of it. Buying derives `normalizedUnitCents` and `normalizedUnit` server-side at entry, from the container or the unit, and returns the stored price on the call so you can see what happened. Nursery trade sizes are converted using the published trade-size capacities: a `#15` can holds about 48 litres, roughly 12.7 US gallons, NOT 15 — so a per-gallon price derived from it is a comparison aid, not a measurement. Gallons, quarts, litres, cubic feet, and cubic yards are converted exactly; cans normalize to a per-gallon price and cubic measures to a per-cubic-foot price. A container the server does not recognize yields NO unit price at all, and the fields come back absent. That absence is deliberate and correct: report it as "no comparable unit price" rather than working one out yourself. Do not send `normalizedUnitCents` or `normalizedUnit` — they are computed here and anything you send for them is dropped. - EVERY APPRAISAL LIVES ON AN ITEM (2026-09-17). `buying_log_appraisal` and a queued `thing` create the Item the question is about — with NO Mission, ever — or, given `findingId`, attach to an Item that already exists (a second appraisal of the same thing belongs on the same Item). Answering sets the Item to Appraised, forward only: a Purchased Item stays Purchased. Every reply returns `findingId`, and `path` opens `/items/?tab=appraisals`, where the person reads the answer beside the Item's prices. `buying_answer_appraisal` takes `findingId` only for an old appraisal that is on no Item yet. - `buying_log_appraisal` — THE FAST PATH, and the one to reach for when the person is standing in front of something. It writes an Appraisal and its answer in ONE call, and it is also how a question you queued earlier gets answered — pass that ask's `queueItemId` with its `findingId` and the ask completes with the Item as its result: `title`, `question`, `answer`, plus the optional `verdict`, `confidence`, `evidence`, `askingPrice`, `venueHint`, `intent`, `booth` and `photoFileIds`. It runs the same two cores the person’s own path uses, so nothing is relaxed: the verdict stays optional, the answer stays attributed to you, evidence keeps its actors, and Buying certifies nothing. An `appraisalKey` makes a retry safe, and the version handed to the answer is read back from the record rather than assumed. Following the documented multi-call flow once took six round trips before a person in a stranger's garage saw a number; that is what this call exists to end. - SAY WHAT THE ANSWER RESTED ON (2026-09-21). Both `buying_log_appraisal` and `buying_answer_appraisal` take `analysisInputs` — the same `selection` shapes `buying_save_research` takes: `{kind:'price', observationId}` for a price this call or an earlier one wrote, `{kind:'capture', captureId}` for a page you retained, `{kind:'legacy_value', record, expectedValueHash}` for a Type, Place or preference. The same call then saves an immutable assessment beside the answer with those inputs frozen and points the Appraisal at it (`currentAnalysisId`). Answered from what you already knew? Send `analysisNoInputs: {reason}` with the assumptions that matter instead — one field, and honest. An answer that names neither still saves exactly as before, and the receipt carries `assessmentGap` saying what it did not record. RE-ANSWERING NO LONGER CARRIES THE PREVIOUS ANSWER'S EVIDENCE FORWARD as though it had just been rechecked. Send `evidence` again if it still holds, or `reusePriorEvidence: true` to say plainly that you are reusing what was there WITHOUT rechecking it. Sending neither drops it. The first re-answer of a pre-cutover appraisal also copies the old answer aside once, marked `legacy_unfrozen`: it is kept and readable, and it is NOT turned into an assessment, because its inputs were never recorded and inventing some would be the exact dishonesty this design exists to prevent. - `buying_record_outcome` — what the person actually did: `bought`, `walked`, `negotiated`, or `undecided`, with `pricePaid` in integer cents when there is a number and anything else in `note`. WITHOUT THIS NOTHING EVER CHECKS AN ANSWER AGAINST REALITY, and a workspace of confident appraisals cannot tell a good one from a bad one. A second call does not overwrite the first: every recording is kept with its time and who made it, so "walked, then came back and bought it for $20" survives as the two facts it is, and the reply says how many have been recorded. You are writing down their decision, not making one; `undecided` is a real answer and leaving it unrecorded is not the same thing. - WHAT HAPPENED MONTHS AFTERWARDS, through the SAME tool and no other. The planter cracks, the scarf turns out to be the only present she wore all winter, the trunk they walked away from is still the one they think about. Send `later: {kind, words, occurredOn?, forPersonId?, corrects?}` — `kind` is `satisfied`, `dissatisfied`, `returned`, `broke`, `repair`, `gift_response`, `regret_passing` or `other_reported`, and `words` are THEIR OWN SENTENCE kept verbatim. The kind is a filing decision and can be wrong; their words cannot, and a later and better reader should be able to reach past your classification to what was actually said. Keep it to one statement: this product does not store conversations. `later` needs the thing it is about — `findingId`, or a `purchaseId`/`appraisalId` attached to an Item. It lands on the ITEM, whichever record the report arrived through, so a question about planters next year reaches it. `occurredOn` is `YYYY-MM-DD`, `YYYY-MM`, or absent: a month you were told beats a day you invented, and "they did not say" is a real answer. `corrects` names an earlier event — both survive, and what was believed then is often the more interesting of the two. A LATER EXPERIENCE IS NOT A PURCHASE. Nothing sent in `later` is counted as a purchase, a total, a price or a decision. A return is not a negative purchase. The Appraisal's own bought/walked/negotiated outcome is untouched and every screen that reads it is unchanged. The events come back on `buying_get_item` as `outcomes`, oldest first, each with who recorded it and whether `reportedVia` was the person typing (`person_ui`) or your account of what you were told (`ai_reported_conversation`). A row with `corrected: true` was superseded by a later one; both stay. - AN OUTCOME IS EVIDENCE. A RULE ABOUT THE PERSON IS NOT. If what they said suggests something general, raise it with `buying_propose_preference`, naming the event in `evidenceOutcomeEventIds`, and it waits on their page holding no authority. ONE BAD PURCHASE NEVER BECOMES A CONFIRMED PREFERENCE. And a proposal they already declined is declined as an IDEA: the server derives the idea's identity from what is claimed — the kind, the Person it is about, the records it cites in `evidence` and `evidenceOutcomeEventIds`, and the words that carry the claim — never from your `operationId` or your phrasing. THE SAME IDEA ABOUT THE SAME THING IS NOT RAISED TWICE, EVEN REWORDED: hedges ("seem", "really", "generally"), adverbs, plurals, tense and word order do not make a new idea, and re-raising one from a fresh session comes back `suppressed` with the decision they already made. A GENUINELY DIFFERENT CLAIM IS A NEW PROPOSAL, and that is deliberate: this is deterministic word matching, not a judgement about meaning, so "you dislike heavy cameras" and "you prefer lighter cameras" are two proposals and both will be put to them. Two proposals resting on entirely different evidence are two proposals even when they share nouns. SEND `forPersonId` when the proposal is about somebody — without it the same sentence about two people is one idea, so declining it for one of them would answer for the other. What they reported still stands on its own record. - FOLLOW-UPS: OPTIONAL, CONVERSATIONAL, AND USUALLY NONE. `buying_get_brief`, `buying_get_item` and `buying_get_mission_context` carry a `followUps` block: at most three past purchases it would be reasonable to ask about, each with `because` — the connection to what is being read right now — and `ageDays`. Eligibility is computed at read time and is narrow on purpose: a known purchase between thirty days and a year old, with no later outcome, not already asked or dismissed, and RELEVANT to the current context (same Item, same kind of thing or an ancestor of it, same person, same Place). The brief has no context, so it offers at most one. `preference` is the person's own setting. `off` returns nothing at all — not a shorter list, nothing — and `ask_first` returns them with `askWhetherToAsk: true`, meaning ask whether they want to be asked before you raise any of them. THERE IS NO SCHEDULER BEHIND THIS FIELD and there is not going to be one. Assist With Buying never sends a message, an email or a notification about a purchase. Nothing here is a task and nothing is due. When you do ask, send `followUp: {purchaseId, state, until?}` — `asked`, `snoozed`, or `dismissed`, which is "don't ask me about this", is FOREVER, and carries no `until`. `snoozed` is "not now": send `until` in epoch ms when they named a moment, and leaving it off snoozes it for NINETY DAYS rather than for good, with the reply telling you the date it landed on. "Not now" is never stored as "never". SKIPPING RECORDS NOTHING. Silence is not dissatisfaction, the absence of a response is not an answer, and you must never write an outcome because a question went unanswered. - LEAVING A QUESTION OPEN? `buying_queue_for_later` WITH A `thing` BLOCK (2026-09-18). Scott: "I was referencing queue-this-for-later with all the insights and pictures and stuff. The user already understands that relationship of when and how the queue starts and runs, so we don't need any separate thing like a separate park-a-question." So the separate park-a-question tool is retired and this is the one way. An appraisal either answers or it does not exist; there is no "asked", "pending" or "in process" appraisal, and no record in Buying ever says it is waiting for you. Waiting is a fact about the Queue and the Mission. Give an ask a `thing` and two real things happen: the object becomes an ITEM at once — `title` (what the thing IS, never where), `photoFileIds`, `listedPrice`, `venueId` or `venueName`, `condition`, and no Mission, ever — and the `ask` becomes a WAITING ask in the person's Queue, filed through the composer's own core, marked "Left by your AI, at your request", naming that Item in its context refs and carrying the photographs. The Item's "In the Queue" connection is where the person sees it. Send `itemId` instead to ask about an Item that already exists; sending both `thing` and `itemId` is refused. The reply returns `findingId`, `itemPath` and the ask's `itemId`. `note` is your one line about what you and the person were discussing. Prefer `buying_log_appraisal` when you can already answer — do not queue a question you can answer. `listedPrice` is integer cents plus a currency — what the thing is offered at where it was seen, never a valuation — and it is the number an answer argues with and an outcome is measured against. `venueName` finds or creates a workspace-private Place by name; the reply says `placeCreated`. Those file ids may name pictures you attached with `buying_attach_photo`; naming an existing file stores no new bytes, so this needs no media permission of its own. The ask's `operationId` makes a retry safe: the same operationId replays the ask and never makes a second Item. `minutes`, `intent` and `booth` are NOT taken here — they are judgements about an answer, and a question nobody has answered has none yet. - ANSWER A QUEUED QUESTION with `buying_log_appraisal`, passing the `findingId` and the `queueItemId` the queue call returned (the QUEUE ROW, not an Item; the reply also repeats it as the deprecated `itemId`). That writes the appraisal on that Item, moves the Item to Appraised, and completes the Queue ask with the Item as its result, so nothing is left waiting anywhere. `buying_answer_appraisal` takes the same `queueItemId`. Answering twice with the same ask is safe: an ask already Done is left as it is. - `buying_answer_appraisal` — record your restatement of the question, the answer, and its evidence lines against the `expectedVersion` you last read. A `verdict` and `confidence` are OPTIONAL, passing neither is the ordinary case, and whatever you store is displayed attributed to you, named and dated. Buying never certifies what a thing is worth. Your restatement is kept beside the person's original words rather than replacing them. An evidence line may set `disagrees` when it argues against your own answer, and `calculated` when its number was derived rather than observed — the same two flags a suggestion's evidence carries, shown to the person as what they are. Cite the line that argues against you when there is one. There is NO promote tool: an Appraisal becomes a Mission only when the person says so. An `askingPrice` may come along here when the tag was read after the question was parked; omitting it leaves any price already on the record alone. - HOW YOU GOT THERE, on `buying_log_appraisal` and `buying_answer_appraisal`, in the same call as the answer: - `reasoning` — ONE plain sentence ("Three sold listings for this exact model sit at $110–140 used, so the $45 tag is well under."). Not a report. - `sourcesChecked` — every place you looked, INCLUDING the ones that came up empty or refused you: `sourceName`, `sourceType` (`marketplace`, `search_engine`, `retail`, `auction`, `reference`, `local`, `other`), `status` (`checked`, `partial`, `blocked`, `requires_user`, `failed` — the Mission source-check words), and optional `url`, `queryUsed`, `summary`, `limitations`, `checkedAt`, `resultCount`. `resultCount: 0` is "looked, found nothing", and the page shows it as that. `sourcesCount` defaults to how many you list. - `researchNote` — `{text, detail}`: a headline and up to 4,000 characters, kept as ONE Context note on the Item and shown collapsed under the answer. `researchNoteId` links a note already on the Item instead. - `prices` — up to 24 price rows (the `buying_save_prices` row shape without `findingId`; `key` optional, defaulting to its place in the list so answering again corrects the same rows). Each becomes a dated price on the Item, linked back to this appraisal and to its research note. - An evidence line may carry `basis` — `sold`, `listed`, `comparable`, `offer` or `estimate` — so "$120" is never read as paid when it was only asked. The page groups lines as supporting the answer, arguing against it (`disagrees`), and calculated. - `buying_update_appraisal` — the details pass, from the antique-mall field report: fill in what was learned after the question was logged. `askingPrice` (the listed price read late — in a mall the tag is often read after the answer, and this is where it goes rather than into a venue note), `intent` (`curious` or `want`: most of what gets logged in a mall is for fun, and the flag keeps the real leads from drowning), `booth` (the booth or dealer number in a multi-dealer Place, so "everything from booth 277" can be pulled later; a "Booth" prefix is dropped), `venueId`, or `venueHint`. ONLY THE FIELDS SENT CHANGE: an omitted field is left as it was, never cleared, and the question, answer, verdict, and every outcome are untouched. Versioned against `expectedVersion` like an answer, because the person may be filling the same fields in on the page. `buying_log_appraisal` takes the same `intent` and `booth` up front; a queued `thing` does not, because a parked question has no answer for them to describe. The reply names which fields changed in `updated`. Two more mall shapes live on venue notes rather than on Appraisals. `buying_add_place_observation` takes `booth`, `discountPercent` (a booth's standing markdown as a whole percentage, 0–100 — consignment malls run 20, 25, 50 per booth; note a change as a new note, because the newest note per booth is the one that stands), and `comeBack` (true marks the note as a reason to return: the bolo case in 277, the Kreiss table if it drops to half). `buying_list_places` takes a name `query` (check whether a place exists before creating it, in one small call) and hands back, on every row, `boothDiscounts` — the newest markdown per booth within the note window, to apply to a tag price when quoting what a thing would actually cost — and `comeBackFor`, the flagged notes newest first, at most ten. Both are bounded by `noteWindow` and say so. - `buying_get_place` — one place, and everything of this person's about it (card AWB-0082): `description`, the long-form story they or you wrote so an assistant understands the store before walking in, with its `version`; `visits`, each carrying the `appraisals` and `observations` made on it, so "what did I look at last time I was here" is answered by the visit rather than by date arithmetic; recent notes and appraisals here; `boothDiscounts`; and `comeBackFor`. Nothing of anybody else's is readable, whatever the venue's visibility. Bounded by `limit`; the reply says so. **What a shared Place holds (2026-09-18).** On a directory row (`scope: "everyone's"`), `publicProfile` is the community half: `recap` — what it is and what it has — plus `whatItHas`, `priceLevel`, `bestTimes` (best days, or the one thing worth knowing before you go), `website`, `phone`, `city`, `mapUrl` and `notes`, with the address and opening hours in `location` carrying their `hoursSource`. A chain row has no address and no hours, because a chain has none; nothing invents either. On a place of the person's own that REUSES a directory entry, `usesPublicVenueId` names it and `publicPlace` carries those same shared facts, read live from the shared row so a correction reaches everyone who saved it. To start using a directory place, call `buying_use_public_place`. Only the product owner writes a public profile — there is no AI path to one, and the privacy-scrubbed `buying_add_location_contribution` remains the only contribution route. - `buying_get_prediction_accuracy` — how well this person has read a seller. A prediction is what you think THIS seller tagged something at, recorded with `kind: "prediction"` BEFORE the tag is seen; when a `listed` price later lands on the same Item the pair is scored. Ask with `venueId` for one Place or estate-sale company, `typeId` for one kind of thing across every seller, or neither for the whole workspace. It returns the number of pairs, the average error as a percentage of the tag, the average SIGNED error (negative means predictions there have run under the tag), how many landed within 10%, and the last twenty pairs in full with both dates. Every figure carries its count; a figure with no pairs behind it is null rather than a confident zero, and the reply states the window it read. It is arithmetic over what was written down: it does not predict for you and it does not adjust your number. Private to the workspace; nothing here reaches the shared directory. - `buying_find_comparables` — what have things like this actually gone for, answered from THIS workspace's own saved prices and measurements. It never reads the open market and it never issues an appraisal; it hands back evidence and says what it spent getting it. Name a subject — `findingId` for one Item's own history, or `typeId` with `includeSubtypes` for the branch below it (the reply names every Type it covered, whether a merge redirect was followed, and whether the walk was capped). `priceKinds` is REQUIRED and every kind comes back in its own group: sold, listed, comparable, paid, offer, estimate, prediction and list_price are eight different claims about the world, and one blended list of them is the mistake this tool exists to avoid. `currency` is required too — one code, or `"any"`, which GROUPS by currency and never orders across them, because the site holds no dated conversion basis and a mixed ordering would be one it invented. `quantityBasis` is `item`, `lot`, `per_unit` or `any`, for the same reason. `fromDay`/`toDay` are INCLUSIVE LOCAL days, `YYYY-MM-DD`; a row whose observed time is genuinely unknown is excluded and counted rather than filed under today. Narrow further with `venueId` or `companyVenueId`. `constraints` are typed and use your own property keys: `{key, predicate, low, high, unit}` where predicate is `point_in` (one value inside a recorded range), `fully_inside`, `overlaps`, `equals` or `exists`. Units convert through the registry INSIDE the property's own dimension — `30–50 g` against a mass property, `15 gal` against a volume one — and a unit from another dimension is refused by name rather than guessed at. MISSING IS NOT MISMATCHED. A candidate nobody has measured comes back WITH that constraint marked `missing`; a candidate whose recorded value falls outside the range is excluded and counted. A property is joined to a price ONLY when the two share an encounter, so a mass read off one listing is never attached to a different listing's offer; a model-level `standing` specification may be used, and is then labelled an assumption rather than reported as a measurement of that example. Items CONFIRMED to be the same listing or the same physical object collapse to ONE candidate that lists its members; merely proposed matches are reported separately and are never excluded, so a count can say how settled it is. Contradictions — a confirmed group with an explicit rejection inside it — are shown, never repaired. A standing specification NEVER excludes a candidate. When it does not answer your constraint the candidate still comes back, with that constraint unanswered and `assumedFromStandingSpec` true — an assumption may be used and may never delete the evidence it was standing in for. The reply states the ONE index it used and why, documents scanned, candidates returned, every exclusion by reason, that retracted rows are excluded, and how many legacy rows still sit outside the comparison indexes. `excluded.time_not_known` counts rows whose date is genuinely unknown — outside any day window rather than filed under today — and a note says when that count is a floor rather than a total. THE CURSOR REACHES THE END. When the scan budget stops a pass it says `candidate_pool_limited` and returns a continuation cursor bound to this exact request — a changed subject, kind, currency, window or constraint is refused rather than silently paged — which resumes at the exact INDEX POSITION the scan stopped at. Following it to the end reaches every eligible candidate, each exactly once. AN EMPTY PAGE CAN STILL CARRY A CURSOR; that means keep going, not that there are none. `rank` is an ORDERING — constraints matched, then newest — and never a score, a probability or a valuation, and it orders WITHIN EACH PAGE'S SCAN WINDOW: a later page can hold a better match than an earlier one, so read them all before deciding, or narrow the question. Private to the workspace. - `buying_prepare_dataset` — all of them, with the completeness said out loud. Ordinary reads answer conversation-sized questions; this prepares thousands of selected rows as ordered JSONL parts you page through, labelled `selected_inputs`, `mixed_live` or `complete_at_cutoff` — a label the SERVER chooses and refuses rather than grants. `plan` · `start` · `status` · `resume` · `cancel` · `read_part`, over a closed list of eight selectors. Reading a part's bytes needs `research.files.read` on the same connection. The site records what it served you; say how many parts you actually read. Its own section below has the whole contract. - `buying_record_visit` — open a visit at a place (`startedAt` now unless given), or close one by sending its `visitId` with `endedAt` or `durationMinutes`. Pass the `visitId` on `buying_log_appraisal`, `buying_update_appraisal`, or `buying_add_place_observation` to file those on the visit. A visit is never required for a note or an appraisal to exist. Workspace-private, always. Needs a stable `operationId`. - `buying_save_place_description` — replace the person's own long-form description of a place: layout, floors, consignment rules, discount patterns, which categories are strong. THE WHOLE TEXT IS REPLACED, so read `buying_get_place` first and pass its `version` as `expectedVersion`; a stale version is refused rather than applied over a paragraph the person added on the page. Dated observations are untouched. Private to the workspace even on a shared directory entry — the shared row keeps its one-line summary and never learns this. Up to 20,000 characters. Needs a stable `operationId`. - `buying_list_suggestion_decisions` — what happened to suggestions already raised here (pending, accepted with the option chosen, declined with the person's reason) plus every standing rule a decline created, each with its materiality condition and whether it is still in force. Read this before proposing. - `buying_propose_suggestion` — raise ONE suggestion: the trigger, a headline, evidence lines, lettered options each carrying the consequence of choosing it, and your recommended option with a reason. The `headline` is one line; anything longer goes in `note`. Cite a line that argues against your own recommendation when there is one and set `disagrees` on it. THERE IS DELIBERATELY NO ACCEPT TOOL: the suggestion waits for the person, and accepting is theirs alone. Who raised it comes from this connection and cannot be sent as an argument. A subject the person declined is suppressed: the call is refused, and the refusal names the rule, their reason, and the one condition under which asking again is reasonable. When that condition is genuinely met, re-send with `reRaised: true` and a `reRaiseReason` saying exactly what changed. Reusing a `suggestionKey` updates the pending suggestion instead of raising a second copy — every field the person reads on it, the recommendation, its reason and the materiality condition included. Its `kind` and `missionId` are fixed when it is raised, and a suggestion the person already decided is never rewritten; either comes back in `notApplied`. ### Lists you name yourself, with price history Card AWB-0081 under Work Order AWB-WO-016, from the 2026-09-03 antique-mall report: "what do I buy repeatedly, and what did it cost last time?" A list is the person's own shelf ("Recurring purchases", "Costco run"); a trip is a dated sublist — one shopping run — inside a bigger list, so the parent accumulates history across trips. A list item is its own lightweight record (a pineapple is not research) that may link to an Item through `findingId` when the thing was also researched. - `buying_list_lists` — the top level, or one list's sublists with `parentListId`. WITH `query` IT ALSO SEARCHES ITEM NAMES across every list, so "what do I usually pay for pineapple?" is one call: `matchingItems` carry `listName`, `lastPrice` (the newest CONFIRMED price), `reading` (`high` / `low` / `about usual` against the item's own confirmed history, with `basis` — how many prices it stands on — or `null` when there are fewer than three), and `cheapestAt` (the lowest confirmed price per place). Every row carries a `path`. - `buying_get_list` — one list with its `ancestors`, `sublists`, and `items` (the same projection). For a trip, `tripLines` are the lines bought on it, read back from the parent list's items. `verbosity: "full"` adds each item's newest twenty prices with who recorded each, where, when, and `confirmed`. - `buying_save_list_item` — put a thing on a list. Send `listId`, or `listName` to find or create a list under `parentListId` (a trip is `listKind: "trip"` with `tripAt` and optionally `venueId`); leave `name` out to create the list alone. The item is found or created inside the list by name, so BUYING THE SAME THING AGAIN NEVER MAKES A SECOND RECORD; `listCreated` and `itemCreated` say what happened. `unit`, `size`, `findingId`, and `note` fill blanks and never overwrite what the person wrote first; a kept value comes back in `notApplied`. `listKind`, `parentListId`, `tripAt` and `venueId` shape a List this call creates: on a List that already exists a blank `tripAt` or `venueId` is filled and the rest is listed in `notApplied` (move a List with `buying_save_list`). `priority` (high = really want it, normal = want it, low = only if it's a great deal), `budgetCents`, `why` and `holding` (owned | wanted | null) are SET when sent, and the reply's `entry` says what the entry now reads. Needs a stable `operationId`. - `buying_record_list_price` — append one dated price: `amountCents`, `currency`, a `venueId` or free-text `where`, `size`, `observedAt`, and `tripListId` to file the line under a trip. NOTHING IS OVERWRITTEN; every price ever recorded stays in the item's history and the newest confirmed one becomes `lastPrice`. A PRICE YOU READ OFF A RECEIPT IS NOT A FACT YET: send `source: "receipt"` and it is kept, shown to the person dashed, and counted for nothing — not the last price, not the reading, not the cheapest place — until they confirm it on the page. There is deliberately no tool that confirms, because an AI confirming its own reading would make the flag meaningless. A price the person told you is confirmed as you record it. The reply carries `reading` when there is enough confirmed history and `null` otherwise — say "not enough history yet" rather than guessing. Needs a stable `operationId`. ### Queue and private media - `buying_list_capture_queue` — bounded cursor page for one of `needs_you`, `working`, `waiting_for_ai`, or `done`. Mission-scoped handoffs include `missionId`, optional `findingId`, and compact `missionContext`; read the named Mission before acting. A fifth state, `draft`, holds items the person accepted from a suggestion and has not released. Drafts are excluded from this listing by default and are only returned when you pass `state: "draft"` together with `includeDrafts: true`. A draft cannot be claimed. Treat one as a person's unfinished note to themselves, not as work waiting. PHOTOS STILL ARRIVING (2026-09-22): a set added on the person's phone is listed the moment it is added, as an ordinary `waiting_for_ai` ask, and its photos attach one by one as they upload. Every row carries `photos: {expected, attached, stillArriving, lastArrivedAt}`; when `stillArriving` is above 0, `photos.instruction` says so. The ask is real — tell the person, start on the photos that are there or list again in a minute or two, and do not go looking for it elsewhere. An ask holds up to 24 files. - `buying_get_queue_activity` — bounded attributable history for one Queue item. - `buying_get_capture_image` — one private queued image as inline MCP image content. Takes an optional `purpose`: what you say you are looking for is shown to the person beside the record that you looked. Large photos are returned downsized automatically; ask for a region to read fine print: a photo over 2 MB comes back as its 1600px copy (`variant: "agent"`, `originalBytes`, `servedBytes`), and optional `region` (`{quadrant}` or `{x, y, w, h}` fractions) returns that part at full resolution. The reply carries `codes[]` and `codesRead` (see September 24 above), and — only on a photo read automatically before 2026-09-27 — that older `reading` and `readingStatus` (see September 25 above; superseded September 27). - `buying_get_capture_images` — several private queued images in one call (up to 12); prefer the batch. Also takes an optional `purpose` and `region`. Returns as many as fit one reply (about 3 MB; photos over 1 MB as their copy) and pages the rest: `nextCursor` and `remainingFileIds`, then call again with the same `fileIds` and `cursor`. - `buying_get_photo_sheets` — scan a whole set of photos (added 2026-09-27): exactly one of `askId`, `itemId`, `tripId` or `fileIds` (up to 600), plus optional `page` (from 1), `sheets` (1–3, default 1) and `purpose`. Each sheet is up to 30 of the stored 320px thumbnails in a 6-across grid, each numbered in its top-left corner, returned inline as a JPEG with a legend (`#n fileId (ask …; Item …; close-up of …; taken …; codes …)`), and `structuredContent` `{total, pages, page, sheets[{page, from, to, cells[{n, fileId, askId?, itemId?, closeUpOf?, takenAtLocal?, codes?, caption?, drawnFrom}]}], nextPage, totalBytes, leftOut, capped}`. Numbers are stable for a set, so any number can be opened later with `buying_get_capture_image(s)` and zoomed with `region`. A photo with no thumbnail yet is drawn from its original only when that is small; otherwise it is a grey cell that keeps its number, its legend line says so, and its thumbnail is queued for a later call. A set of more than 1,000 photos numbers the first 1,000 and says so (`capped: true`). Only this workspace's stored photos; erased photos, voice notes and research parts are never on a sheet. Read only, recorded like every photo read. - `buying_get_voice_note` — ONE voice note the person recorded on an ask, returned inline as MCP audio content. Same `media.read` permission as the image tools, same optional `purpose`, and the same record shown to the person afterwards. `buying_list_capture_queue` marks every attachment with `media`: "audio" is a voice note and belongs to this tool, "image" is a photograph and belongs to the two above. One per call; the inline ceiling is 2,097,152 bytes. TRANSCRIPTS ARE PRO-ONLY (2026-09-25). Some accounts have Pro, an internal plan an administrator sets. A Pro account's voice note is transcribed by a speech-to-text model the moment it is stored (not a silent clip, nothing over five minutes), and the result — and each voice note in `buying_list_capture_queue`'s `files[]` — carries `transcript: {text, automatic: true, model, language, confidence, durationMs, at}`, `transcriptStatus` (`ready` | `pending` | `failed` | `none`), `transcriptError` when it failed, and `transcriptAvailable`. The text is a model's hearing of the recording, not the person's typing; the recording stays the source of truth. An ask that was only a recording may arrive titled automatically (`titleBy: "ai"`, `titleWrittenAutomatically: true`, `automaticSummary`); a better title from you replaces it, a person's own never is. On any other account there is no stored text of the recording, no transcription tool exists here, and none is being withheld (`transcriptAvailable: false`): transcribe it yourself from the audio, or ask. If your client cannot process audio, tell the person plainly and ask them what they said. Never present your own guess at the words as something the site supplied. Since 2026-09-24 the person's phone measures each clip before it is kept, and the result (and each voice note in `buying_list_capture_queue`'s `files[]`) carries `audio: {peakDb, meanDb, speechRatio, silent}`. `silent: true` — peak under -50 dBFS or under 5% of frames with speech-band energy — means they kept a clip with no sound: do not transcribe or guess; tell them it has no sound and ask what they meant. Older recordings have `audio: null`. - `buying_attach_photo` — attach photographs to a record. Needs `media.write`, which signing in grants as of 2026-08-29. SEPTEMBER 26 — A CLOSE-UP STAYS WITH ITS ITEM. `mode: "save_region"` with `fileId` (a photo already in the workspace), `target`, `targetId`, and `region` or `readingItem` (a position in that photo's Pro `reading.items[]`; its box is used and its label becomes the caption) cuts the box from the stored original at full resolution (up to 3000px) and keeps it as a new photo on the record, replying `closeUpOf: {fileId, box, readingItem?, rotate?}`, `width`, `height`. One call, no bytes, `makeMain` works, the original is never changed, and a resent `operationId` replays. OCTOBER 24 — `rotate` (0, 90, 180 or 270) turns the saved close-up clockwise, for a label or a photo taken on its side; to turn a whole photo, send `region: {x: 0, y: 0, w: 1, h: 1}` with `rotate`. Refusals: `NOT_FOUND` (no such photo here), `INVALID_REQUEST` (no box, or no such reading item), `UNSUPPORTED_MEDIA` (not a JPEG or PNG). SEPTEMBER 22 — BATCHES, PICTURES ALREADY HERE, TYPES, AND WHICH PICTURE LEADS. Several photos: `mode: "begin_batch"` with up to 12 `slots` (each `target`, `targetId`, `contentType`, `sizeBytes`, `sha256`, optional `caption`, `capturedAt`, `role`, `makeMain`) returns one first-party PUT `uploadUrl` and `uploadRef` per slot; PUT each file, then ONE `mode: "finish_batch"` with every `uploadRef` confirms and attaches them all, one result per slot, and a slot whose PUT failed is reported alone (`BYTES_NOT_STORED`, retryable — PUT again to the same URL and finish that one ref). Resending `begin_batch` with the same `operationId` and slots resumes them. A picture already in the workspace (a Queue ask's `fileIds`): `mode: "link_existing"` with `fileId`, `target`, `targetId` — one call, no bytes, never attached twice (`alreadyLinked`), refused as `FILED_ELSEWHERE` if it sits on a different Item, Type or Place, and it keeps its own provenance. `target: "type"` joins. Every reply says `isMainImage`, `mainImageFileId`, `role` and `howToChangeMainImage`: the first photograph on an Item or Type with none leads; `role: "evidence"` (screenshots, listings, receipts, price pages — and any file stored as a listing screenshot or receipt) never leads; `makeMain: true` changes the lead and is declined for evidence (`makeMainDeclined`). Crop evidence to the product with your own tools before offering it as the photo. Cost: batch 2 calls for up to 12; `link_existing` 1; `fetch_url` 1; `begin_upload`/`finish_upload` 3 per photo; inline 1, up to 1572864 bytes (1.5 MB), and a photo already in the workspace is linked (`linkedExisting: true`) rather than stored twice. SEPTEMBER 18 — WHAT IT REACHES, AND A FOURTH MODE. `target` is now one of `item` (the thing itself, by its `findingId` — the ordinary one at a sale, and an Item with no picture yet takes this as its main image), `place` (one of the person's OWN saved Places, by its `venueId`; a Place from the shared directory is not one of theirs, save it with `buying_use_public_place` first, and nothing you attach ever reaches the shared directory), `appraisal`, `queue_item` or `trip_stop`. Until this you could photograph the QUESTION about a thing and not the thing. AND THE SERVER CAN FETCH THE PICTURE: `mode: "fetch_url"` with `sourceUrl`, the direct https address of the IMAGE rather than the listing page around it. The server gets the bytes, checks they really are a JPEG, PNG or WebP no larger than 8388608 bytes — the SAME ceiling as an upload, never the smaller inline one, so a three-megabyte listing photo is an ordinary fetch — and stores them exactly as fetched through exactly the same verified path an upload takes. This is the one to reach for with a Marketplace or eBay listing: you do not have to download megabytes and reproduce them as base64, and most clients cannot. THE RULES IT FETCHES UNDER, so you can predict a refusal: https only; never follows a redirect (a URL that redirects is refused by name — copy the image's own address); never reaches a private, local or link-local address; a byte ceiling enforced while reading; and a timeout. Many listing sites block a server fetch outright, and when one does the refusal says so — it will say so every time, so do not retry more than once; download it yourself and use `begin_upload` instead. `photoUrls` on `buying_save_item`, `buying_save_items_batch` and `buying_capture` is the same fetch, folded into the save so the thing and its picture land in one call. SAVING A MARKETPLACE OR ONLINE FIND? Always attach its listing evidence in the same `buying_save_item` call: the product image in `photoUrls`, a screenshot of the listing as you saw it in `listingScreenshotBase64` (base64 JPEG, PNG or WebP within the inline limit, kept as dated evidence captioned "Listing screenshot" and never the main image; the reply's `listingScreenshot` is `{ok, fileId}` or the failed step with how to resend, and a failure never costs the Item), and the listing link in `sourceUrl`. It needs the same photo permission as `photoUrls`. Never record seller contact for this. Three modes for bytes you already hold: `begin_upload` returns a short-lived single-file `https://assistwithbuying.com` PUT URL and an `uploadRef`; you PUT the bytes yourself; then `finish_upload` makes the server re-read the stored object and verify its exact length, SHA-256, first-bytes format, and storage version before anything is attached. A declared MIME type is never trusted. The URL never redirects to or exposes Backblaze. Never echo, save, log, or narrate it: it is a write capability, not information. `inline` mode takes small `imageBase64` for compatibility clients only, capped well below the 256 KiB request limit. One `operationId` per photograph or batch; resend a failure with its ORIGINAL `operationId` and a retry of one that landed replays instead of storing it twice. (This field was named `opId` until 2026-08-29 and now matches every other write.) Nothing is stripped: the photo is stored exactly as sent, location and capture time included (see "Photos keep their location and capture time"), and the file is recorded as attached BY YOU, named and dated — never as one the person took. It lands private: you cannot make it public, share it, or take it back. When you ask the person to take the picture, ask them to put a hand or a shoe in the frame for scale: a photograph with nothing to size the object against is the commonest reason an identification comes back wrong, and it cannot be fixed once the thing is gone. For a set, preflight every file's exact length and lowercase SHA-256, open every slot with one `begin_batch`, PUT no more than three at once, and attach them all with one `finish_batch`; retry only a failed slot, by its own `uploadRef`. - `buying_update_capture_queue_item` — guarded claim, heartbeat, request-user, legacy-result complete, fail, or release transition. Every post-claim transition requires the active token and current version. For a Mission-era completed result, prefer `buying_save_complete_result` so the result and Queue transition commit together. Completing a Mission-scoped handoff into any other Mission is rejected. Since 2026-09-17, `complete` links EVERY record the work produced or changed in `results`: `[{kind, id}]`, kind `item`, `type`, `place`, `list`, `trip`, `mission` or `library` (a Library record's id; its dossier id is also accepted), one to 24, each resolved against this workspace — a record from another workspace, or an id of the wrong kind, is refused and nothing changes. `resultInterestId` still works for one Library record. Work with no single record to link (a workspace-wide cleanup, or a plain answer) completes with a non-empty `note` saying what you did and `recordsTouched`, the count of records changed (0 for an answer), stored as `completionKind: "answered"`; `completionKind: "result"` still means a linked record. Every ask can be completed this way, with or without a Mission or record attached; if `complete` is refused, the error says exactly what to resend, and an AI never tells the person an ask cannot be closed. On `request_user`, `requiredAction` is the exact question the person sees, and the ask's history quotes it; use it only for a real question, never to park finished work. An ask in Needs you cannot be claimed: it resumes when the person answers, and the person can also mark it done on its Queue page, stored as `completionKind: "closed_by_person"`. Since 2026-09-24, `action: "resolve_answered_elsewhere"` closes a Needs you ask you already answered somewhere else (usually in chat): no claim, `expectedVersion`, a required `note` saying where and what the answer was, optional `results` and `recordsTouched`, and `answeredIn` when it was not "chat". Stored as `completionKind: "answered_elsewhere"` with `answeredIn`; the Answered card reads "Answered in chat". Refused on any other state. Also since 2026-09-24 the person can edit a Waiting, Working or Needs you ask — words, title, photos, voice notes — and every edit bumps `version` and appends to `personEdits` (shown by `buying_list_capture_queue`, with `editedSinceClaim` on a Working ask). The AI holding the claim is not locked out: a `heartbeat`, `complete`, `request_user`, `fail` or `release` sent with any version it held since its claim is accepted when the claim token matches, and the reply carries `editedSinceClaim: true`, `changes` (what changed, with new file ids) and `editedInstruction`. Every AI transition clears `personEdits`. A `claim` with a version an edit moved past is refused with the changes spelled out. Every event in that history names the acting AI by its connection's readable name — the name the person gave the connection, or the assistant's reported name and where it ran — never by its connection id. - `buying_queue_for_later` — write down work the person ASKED to have done later, and keep talking. ONLY for their request: an idea you had on your own is a suggestion (`buying_propose_suggestion`), never a Queue row. `asks` is one to ten items, each with its own `operationId`, `ask` (their request, in their words as closely as you can) and `note` (what you were discussing), and optionally one `missionId`, `itemId` and/or `placeId`, `fileIds` of photos already in the workspace (for example ones attached with `buying_attach_photo`), `requestedOutcome` and `effort`. Each lands as an ordinary `waiting_for_ai` item through the same code path as the person's own composer — counted, listed, editable, archivable and deletable like one they typed — and every surface says "Left by your AI, at your request". Nothing starts, including for you; there is no new state and no argument that reaches an existing ask. Per-ask results: `filed`, `replayed` (the same `operationId` and content returns the first row rather than a second) or `refused` with a reason (a reused `operationId` with different content, a record or photo outside this workspace); a refusal leaves nothing behind and does not stop the others. Needs `queue.write`. A grant narrowed to chosen Missions cannot call it. TWO IDS, TWO NAMES (2026-09-18). The INPUT `itemId` is an ITEM's `findingId` — an Item you already have and want to ask about. The REPLY's `queueItemId` is the QUEUE ROW this ask became; pass `queueItemId` back to `buying_log_appraisal`. The reply also repeats it as `itemId`, which is DEPRECATED for one release and will be removed: it named the Queue row while the input of the same name names an Item, which is the confusion `queueItemId` ends. `fieldNames` in the reply says exactly this. To add a NEW photograph to an ask you just filed, call `buying_attach_photo` with `target: "queue_item"` and the returned `queueItemId`. `buying_list_capture_queue` carries `writtenDownBy` (connection name, date, note) on every ask filed this way: the words are that AI's record of the request, not the person's typing. - `buying_store_check` — IN-STORE MODE, call 1 of 2 (2026-10-16). When the person says "check", claim every waiting store batch (photos they snapped on Out) in one call: per batch `batchId`, `claimToken`, `place` with `today` (price, tag colours, notes), `tripStop`, `photos[]` (fileId, when and where taken, the person's `note`, `candidates` from purchases and Lists) and `imageReads`; plus `standing` (wants, collections, habits, sizes, `replyStyle`). `maxPhotos` (default 30) caps one check. Needs `queue.write`; photo addresses follow the same `media.read` gate as the Queue listing. - `buying_save_store_results` — IN-STORE MODE, call 2 of 2. `batchId`, `claimToken`, one row per thing (`fileIds`, `title`, `headline`, `verdict`, `worthLowCents`, `worthHighCents`, `tagCents`, `typeName`, `itemId?`, `owned?`, `wantedListId?`, `note?`) and the batch's `answer` card. Each row becomes an Item and Appraisal at the Place (photos filed, tag as asking price, worth as an estimate row) and the batch completes Done. Idempotent by (`batchId`, `claimToken`). Needs `mission.write` and `queue.write`. ### Privacy-scrubbed public venue context - `buying_list_public_locations` — search approved public stores, marketplaces, estate sales, and auctions without exposing contributors or private buying records. - `buying_find_or_create_public_location` — find a venue or propose a pending public shell using non-personal venue facts only. A match is an existing directory entry and is not changed: facts you sent that differ from it come back in `notApplied`; add them with `buying_add_location_contribution`. - `buying_add_location_contribution` — explicitly opt in to one anonymized, scrubbed venue note. Suspicious content stays pending. - `buying_withdraw_location_contribution` — withdraw only the authenticated person's own contribution; ownership is verified server-side and never exposed. ### The design rebuild's blocks — Preferences, Types, Purchases, Notes, Decisions Added 2026-09-16. Reads are under `mission.read` and writes under `mission.write`, the same two private lanes as everything above, for the same reason: each reads or writes the person's own workspace, carries your provenance, and is correctable by whoever wrote it. Two of these decide what you cost the person, and they are worth learning first. - `buying_get_preferences` — THE DETAIL READ FOR PREFERENCES (since 2026-10-14 not a first call: `buying_get_brief` is, and carries `lookingFor` and the rules; this reply's `contract` is a pointer, `{ see: "buying_get_brief", note }`). The person's AI Interaction Profile, which is how they want an assistant to work, in their own sentences, with its version number — plus every Preference they opened to an AI, each with its own version number and the `scope` it applies to. READ IT ONCE AT THE START; do not re-read it between saves. Buying records "this connection last read version N" as a single field on the connection and writes it ONLY when the version changed, so forty reads in a session where nothing changed write nothing at all. What is missing is deliberate: a Preference reaches this list only when the person marked that row readable, and everything else comes back as a count in `withheld` and never as content. Guidance is not permission — nothing here grants anything, and every record you touch still needs its own authority. SINCE 2026-09-19 it ALWAYS returns `styles`: `{conversationStyle (quick · balanced · detailed), workStyle (light · standard · thorough), isDefault, explanation}`, even when there is no profile at all — absent means balanced and thorough, and `isDefault` says whether the person chose it. The two are deliberately separate: conversation style is how long and detailed your replies are; work style is how deeply you research and how much you save. Research and save thoroughly in the background however short your replies are. The same block rides in `buying_get_brief`'s `contract`. SINCE 2026-09-19 it also ALWAYS returns `lookingFor` — WHAT I LOOK FOR, the person's own profile: `{wants, collections, habits, limit, truncated, path, note}`, each of the three an array of `{label, content, bar, forPerson}`. `bar` is the quality or price bar in their words ("Under $40, no chips") or null; `forPerson` is a plain name when it is for somebody else — their mother, a friend — and NULL WHEN IT IS FOR THEMSELVES. There is no Person record behind that name and there is not going to be one; a name is what somebody says out loud in a shop. Habits are how they shop: where they look first, how long they stay, what they walk past. `buying_get_brief` returns the IDENTICAL block beside `standingBrief`, from the same helper, so whichever of the two you already called is enough — do not call both for it. At most 60 rows travel and `truncated` says when more exist. Like every preference since 2026-10-19, a want, a collection or a habit is open to the person's own AI from the moment it is created, by them or by you at their request. The person can close any row of any kind at https://assistwithbuying.com/preferences by switching it to Only me — a row they closed is simply absent here, never flagged. - `buying_save_items_batch` — UP TO 200 ITEMS, ONE CALL, ONE TRANSACTION. This is the single biggest lever you have on what this product costs: 200 rows saved one at a time is 200 transactions and 200 re-renders of the person's Items page. BATCH EVERY SWEEP. Each row takes the same fields as `buying_save_item` plus its own stable `key`, and a key is an IDENTITY: send the same key again and the Item it already made is handed back unchanged with `replayed: true`, so a second pass over the same listings is safe. Correct an Item by sending its `findingId` instead. Rows may carry up to 20 `observations` — dated price readings with their own `observationKey` — on ANY Item, Mission or not; every field is kept (shipping, fees and `sourceRef` included), and the row's reply counts prices created, corrected and unchanged. PARTIAL FAILURE IS REPORTED, NOT FATAL: a refused row comes back under its key with the reason while every other row is saved (each row's `recap` and `venueName` work as on `buying_save_item`; `notApplied` and `warnings` rows are named `items[n].field`, and replayed keys are reported once as `items[replayed]`), and `saved` and `failed` are listed separately. A malformed batch — over 200 rows, or two rows sharing a key — is refused whole, because writing most of it would be worse than writing none. SEPTEMBER 18: a row carrying `findingId` is applied as a CORRECTION even when its `key` has already saved something — `notApplied` says the key was overridden — and a row may carry `photoUrls`, direct https addresses of the listing's images which the server fetches itself. The Items are all saved first in the one transaction; the pictures follow, and one that cannot be fetched is reported in `photos[]` under its row's key without costing that row its Item. - `buying_capture` — A WHOLE SALE IN ONE CALL, AND THE ONE TO REACH FOR WHEN THE PERSON IS STANDING SOMEWHERE. Up to 50 things, each with its prices, its photographs and what the person actually did about it, plus one purchase covering several of them — one call, one approval, one transaction. - EACH ROW takes the same fields as `buying_save_item` plus its own stable `key`, which is both its replay handle and how the purchase says which things it covered. A row carrying `findingId` is a correction and wins over the key. - `prices` are the SAME rows `buying_save_prices` takes. A price with no key of its own is keyed from its row, so re-sending the capture corrects the same prices rather than stacking duplicates. - `outcome` is what the PERSON did, never your advice: `bought` (also moves the Item to Purchased), `passed`, `come_back`. `outcomeNote` carries why, in their words. Leaving it out is fine and is not the same as `passed`. - `venueId`/`venueName`, `missionId`, `tripId` and `listId` at the top level apply to every row that does not name its own. At one sale they are the same for everything, and repeating them fifty times is fifty chances to get one wrong. - `purchase` carries `purchaseKey`, `totalCents`, `currency` and `covers` — the keys of the rows it paid for. ONE PRICE FOR A BOX OF THINGS is SPLIT across the rows it covered, in proportion to per-unit tags times quantity, using whole-lot tags once. Quantity comes from the line or its Item and is recorded separately. When nothing carried a tag the split is even PER THING rather than per line. The shares always add up to exactly what was paid. The reply carries `bundleAllocated: true`, the `allocationBasis` it used (`tag`, `estimate` or `even`), every line's `allocatedCents` beside the `weightCents` and `quantity` the split used, and a `bundleNote` saying a share is not a price anybody quoted and the person can change the split. When you already know what each thing cost, send `purchase.lines` with a line per Item (each may name its row with `itemKey`) and they are recorded exactly rather than allocated. - PARTIAL FAILURE IS PER ROW, and one stable `operationId` replays the whole call. - `buying_save_types_batch` — up to 100 Types in one call, each row exactly a `buying_save_type`. NO PER-ROW KEY, deliberately: a Type's NAME is its identity, so re-sending a batch updates rather than duplicates and a key would be a second, weaker handle beside a strong one. `created: true` in the reply means a new record; `false` means the Type of that name was updated. Two rows naming the same Type refuses the whole call, because the second would silently overwrite the first. - `buying_save_prices` — EVERY ITEM HAS A PRICE HISTORY, Mission or not. Up to 100 dated prices for one or more Items in ONE call and one transaction. Each row: - `key` — the price's identity; derive it from the Item and the source (`:ebay-sold-2026-09-03`). The same content again reports `unchanged`; different content CORRECTS the row in place and keeps what it said before in its history. Nothing is silently skipped. A key that belongs to another Item is refused. THE KEYS YOU ALREADY USED ARE READ BACK from `buying_get_item` (its `prices` block) and `buying_get_type`. Read one of those before correcting a price from a fresh session: without the key you invent a new one and write a duplicate. A CORRECTION MERGES (2026-09-18). A field you do not mention is left exactly as it was — resending `{key, kind, amountCents, currency, state}` to fix a typo no longer erases the `scenario`, `learnedBy` and `channel` you recorded the first time. To CLEAR a field, send it as `null` explicitly: `scenario`, `note`, `sourceLabel`, `sourceUrl`, `venueId`, `learnedBy`, `channel`, `state`, `currency` and the amount fields all accept `null`. `changed` lists what actually moved. - `basis` — WHERE A NUMBER WITH NO LINK CAME FROM, in one sentence (2026-09-19). A `comparable` or an `estimate` needs either a `sourceUrl` or this; a `sold` needs a `sourceUrl` or `learnedBy: "receipt"`, because `sold` means a completed sale and an active listing is a `comparable`. A row that carries neither is refused under its own key with the fix named, and the rest of the call still saves. An existing `note` counts as the basis. - `findingId` — the Item. - `kind` — WHAT THE PRICE IS. Eight words, one claim each (2026-09-18). `listed` — the price THIS thing is offered at where it was seen: the sticker on the lamp, the number on the booth card, the price on its own listing. *A $18 sticker on a marimba at an estate sale.* `prediction` — what you think THIS seller will have tagged this thing, written down BEFORE the tag is seen, with `observedAt` set to when you predicted. When a `listed` price later lands on the same Item the pair is scored; read the record with `buying_get_prediction_accuracy`. *$25 predicted on a Kirby; the tag said $40.* `comparable` — the same kind of thing offered by ANOTHER seller, still for sale. A comparable, never a sale. *The same marimba listed at $75 on Reverb.* `sold` — what one actually sold for. *One of these closed on eBay at $62.* `paid` — what the person paid for this thing. *You paid $20 after asking.* `offer` — a price proposed in a negotiation, by either side, that nobody has taken. *They asked $40, you offered $25 — the $25 is an offer.* `estimate` — a figure somebody worked out rather than read: an appraisal's number, your valuation, arithmetic over the other rows. *Your appraisal put it at $40–$75.* `list_price` — the maker's or catalogue price for the thing new. *The catalogue listed it new at $240.* THE OLD WORDS ARE ACCEPTED FOR ONE MORE RELEASE ONLY and mapped, never silently: `asking` → `listed`, `asking_elsewhere` → `comparable`, `estimated` → `estimate`, `msrp` → `list_price`, `observed` → `listed`, `seen` → `listed`, `calculated` → `estimate`. The row SAVES under the replacement and the reply carries `deprecated: [{field, sent, storedAs, note}]`. Send the replacement from now on; after the next release these refuse. Never send what somebody wants as what somebody got. - `learnedBy` — HOW YOU LEARNED IT, which is not what it claims. `tag` (it was physically on the thing: a sticker, a grease-pencil number, the shelf label under it), `listing` (you read it in a listing or an ad, online or printed), `told` (a person said it, in person or by message — across the booth, in a text; no tag, no listing), `receipt` ( it came from a sale record: a sold listing, an auction result, a receipt), `worked_out` (nobody quoted it; you reasoned to it). A $40 sticker on a lamp is `listed` + `tag`; a dealer saying "I'd take $300" is `listed` + `told`; an eBay sold page at $720 is `sold` + `receipt`. `list_price` LEFT THIS AXIS on 2026-09-18: it is a kind of price, not a form of evidence, and a row that carried it is migrated to `kind: "list_price"` with `learnedBy` cleared. Sending `learnedBy: "list_price"` is ACCEPTED FOR ONE MORE RELEASE, exactly as the seven retired kinds are — the row is stored with NO `learnedBy`, and the `deprecated` note says a maker's list price is `kind: "list_price"` while `learnedBy` says how you learned the number. - `channel` — WHERE IT WAS FOUND, as a kind of market rather than a shop's name. `retail_store` (a shop selling new goods over a counter: a department store, a warehouse club, a music shop), `online_retailer` (a shop's own website selling new: Amazon, Wayfair, the maker's store), `online_marketplace` (a platform of individual sellers: eBay, Facebook Marketplace, Craigslist, Reverb), `auction`, `thrift_store`, `yard_sale`, `estate_sale`, `antique_store`, `flea_market`, `pawn_shop`, `classifieds`, `other`. An eBay comparable is `online_marketplace`; a Goodwill sticker is `thrift_store`; a booth at an antique mall is `antique_store`. **Sending `venueId` derives `channel` from that Place and replaces what you sent** — the saved row comes back with `channel` and `channelFrom` (`place` or `sent`), so reuse the word you are handed. Leave `learnedBy` or `channel` off rather than guessing; `other` means you looked and none of the markets fits. - `amountCents`, or `minCents` + `maxCents` for a range you genuinely hold — sold comps from $110 to $140 are a range; do not invent a midpoint. `currency` is a 3-letter code (USD when omitted). `observedAt` is when the price was true, in epoch ms. - `scenario` — the condition or bundle this price is for, for THIS Item, in a few words: "used, as-is", "used, with mallets and bag", "new in box". Without it two prices for two different offers read as a contradiction. - `state` — HOW YOU KNOW IT, in the comparison board's own words: `verified` (you read the sold price or the sticker yourself), `measured`, `inferred` (worked out from something close), `estimated` (your judgement), `calculated` (arithmetic on other rows), `conflicting`, `unknown`, `asked`. The Prices tab draws verified prices solid and estimates lighter, and sets what was paid (or the Listed price) against the best-evidence resale range — verified sales first — so pick honestly. - `quantityBasis` — IS THIS PRICE FOR ONE THING OR FOR SEVERAL, as a field rather than a sentence. `item` is the default and the ordinary case. `lot` means the amount is for the WHOLE set and needs `unitQuantity` (2 or more): a canteen of twelve spoons for $180 is `amountCents: 18000`, `quantityBasis: "lot"`, `unitQuantity: 12` — and it is NOT a $180 spoon. `per_unit` means the amount is what ONE costs and needs `unitQuantity` — how many the offer covered: fifty cents a golf ball out of a case of three hundred is `amountCents: 50`, `per_unit`, `unitQuantity: 300`. Send 1 when the seller simply prices by the unit. `buying_find_comparables` groups on this word and NEVER mixes the three, so a lot price saved as an item price quietly wrecks the comparison it feeds. If you do not know how many are in the lot, say so in `scenario` and leave `quantityBasis` off rather than inventing a count. Every price reads back `quantityBasis`, `unitQuantity` and `perUnit`. - `valueBasis` — WHAT AN ESTIMATE IS AN ESTIMATE OF, only on kind `estimate` (2026-10-15). `melt` is what the metal would fetch at today's spot price (a 40 g sterling teaspoon is about $22 melt: `kind: "estimate"`, `valueBasis: "melt"`, with a `basis` saying the weight and spot); `replacement` is what a new equivalent costs; `resale` what it would sell for second-hand; `insurance` what a policy would cover. The Item's page draws melt as its own line beside what was paid, so a melt figure must never be a plain estimate. Sent on any other kind, the row is refused (`PRICE_VALUE_BASIS_KIND`) while the rest of the call saves. Correcting an estimate to another kind clears the `valueBasis` it had (the receipt's `changed` names it). Every price reads back `valueBasis`, null when not said. - `condition` — WHAT CONDITION THIS PRICE IS FOR, as one word (2026-10-15): `new`, `sealed` (factory-sealed, never opened), `used` (second-hand, opened, worn), `as_found` (untested, as-is, as it came out of the bin) or `for_parts` (not working). Keep the wording in `scenario`; `condition` is what `buying_get_type`'s `priceSummary` groups on, so new and used prices are never one range, and the Type's price picture filters by it. Leave it off when the source does not say: a row with no condition is summarised by what its scenario's words state. Every price reads back `condition`, null when not said; sending null clears it. - `venueId` (a Place) or `sourceUrl` (a listing), plus a short `sourceLabel` ("eBay sold, 90 days"); `note` is one short remark; `shippingCents`, `feesCents`. - `researchNoteKey` links a note declared once in `researchNotes` (`{noteKey, findingId, text, detail}`) — the longer write-up several prices rest on, kept as one Context note on the Item. `researchNoteId` links one already there; `appraisalId` links the appraisal that produced the price. PARTIAL FAILURE IS REPORTED, NOT FATAL: a refused row comes back under its key with the reason and the rest are saved. Two rows sharing a key, or more than 100, refuses the call. The rest, in the order you are likely to need them. - `buying_list_types` — a page plus a cursor over Types. A TYPE IS A KIND OF THING and an ITEM IS ONE OF THEM: "bolo ties" is a Type and carries what to check before paying and roughly what they cost; a $45 bolo tie in a case in Tucson is an Item. Each row carries exact Item, Purchase and Note counts taken from the Type's own tallies rather than by counting anything. `query` searches names — and tags, over a bounded window it states — do that before `buying_save_type`. TYPES ARE A TREE: every row says its `parentTypeId`, its `ancestorTypeIds` (root first) and its `depth`. `parentTypeId` or `parentTypeName` returns the Types filed DIRECTLY under that one, as one indexed read of one level; a parent nothing matches comes back empty and says so rather than as every Type. `tag` narrows to one tag. - `buying_get_type` — one Type with a BOUNDED page of its Items, its recent Purchases and its Notes. The counts are exact and the slices are bounded, and both are stated, so "20 of 412" is honest and costs one indexed read. Follow `itemsPageInfo.continueCursor` rather than raising the limit. AND WHAT THESE GO FOR (2026-09-18). `prices` is every dated price across EVERY Item filed under this Type — the newest 100, flat and newest first, each row naming its `findingId`, with `window` and `capped` — in one indexed range read. The rows are the same shape `buying_get_item` returns, `key` included, so a correction from a fresh session has the key it needs. WHERE IT SITS IN THE TREE (2026-09-18). `parent` is the group Type it is filed under; `ancestors` is the breadcrumb, root first, and its length is the depth; `children` lists the Types directly under it, bounded by `children.window`. When there ARE children, `rollup` answers for the WHOLE BRANCH: `typeCount`, `levels`, the Item, Purchase and Note counts summed from the records' own tallies, and the widest price range any Type in it states with `fromTypes` saying how many that stood on — DERIVED, and never this Type's own range. `rollup.capped` says whether the walk hit its bound. `branchPrices` is every dated price across the branch, newest first, each naming its Type, with `typesRead` of `typesInBranch`; it is absent on a Type with no children, because there is no branch to roll up. - `buying_list_purchases` — the dated record of what was actually paid, which is the half of "assist with buying" that a listed price and an appraisal cannot answer. A DATE RANGE IS ALWAYS APPLIED: sending none means the last ninety days rather than the whole record, and the reply states the window. Narrow with `typeId`, `listId`, `tripId`, `findingId` or `venueId`; each has its own index, so narrowing never becomes a scan. The totals line describes THAT PAGE, says so, and is null when the page mixes currencies, because a total that adds dollars to euros is not a total. - `buying_list_notes` — every Note on one record (`targetKind` item, type, list, place, mission or trip, plus its id), newest first, as a page with a cursor. TWO KINDS OF TEXT COME BACK AND THEY ARE NOT THE SAME: `text` is the person's own wording and `earlier[]` holds every wording it replaced, while `aiReading` is an assistant's paraphrase stored beside the words with its name and date. Quote theirs as theirs. - `buying_save_item` — one Item: one thing, at one price, at one Place. It needs no Mission. Name the kind of thing with `typeId`, or with `typeName` to find or create that Type in the same call. `askingPrice` is what the thing is LISTED at, in integer cents with a currency, and is never a valuation. `title` is what the thing is, never where: link the Place with `venueId`, or `venueName` to find or create a private Place by name in the same call (`placeCreated` says which). `photoFileIds` may name pictures already attached with `buying_attach_photo`; referencing an existing file stores no new bytes and needs no media permission — since 2026-09-22 EVERY id is filed on the Item's Pictures, the first non-evidence one leads only when the Item has no main image, and one filed on a different record stays there and comes back in `notApplied`. The reply's `photos` says what was filed and which picture leads. Evidence you save is attributed to you; a line sent as `by: "you"` is reported. Pass `findingId` to correct one. Fields you do not send are left as they were. USE THE BATCH WHEN YOU HAVE MORE THAN ONE. MEASUREMENTS ARE STRUCTURED, NOT PROSE (2026-09-18). `dimensions` is a list of measurements somebody took: each names a `kind` (`diameter`, `height`, `width`, `depth`, `length`), a number in `inches` OR `cm` (never both), and `where` the tape touched — `inside` or `outside`. That last one is load-bearing: a planter is SOLD by its outside rim and PLANTED to its inside wall, and a Mission criterion asking for "15–20 gal" is checked against the inside. Sending `dimensions` REPLACES the list; a second reading of the same pot is a correction, not a second pot. `capacity` is what it holds — `{ value, valueMax?, unit: "gal" | "l", basis }` — where `basis` says `measured` (it was filled), `stated` (a label or a seller said so) or `computed` (you worked it out), and `valueMax` carries a range, because "twelve to fourteen gallons" is what a person actually knows about a pot. LEAVE `capacity` OUT and the site computes an approximate figure itself, from the inside diameter and height for a round container or the inside width, depth and height for a square one, and every screen that shows it says so and offers the arithmetic. Do not send a computed number as `measured`. HOW MANY, AND WHAT A UNIT COSTS (2026-09-18). Three identical pans on one shelf are ONE Item with `quantity: 3`, not three Items. `unit` says what one of them is (`each`, `pair`, `set`, `case`, `box`, `pack`, `dozen`, `roll`, `bundle`), and `pricePerUnit: true` says `askingPrice` is the price of ONE — golf balls at 50¢ each, about three hundred to the case — so the Item shows unit price × quantity = total rather than implying the tag said $150. An observation row carries the same idea as `perUnit` and `unitQuantity`. THE STOP IT CAME FROM (2026-09-18, widened 2026-09-19). An Item saved with a `venueId` is added automatically to a stop at that same Place on an OPEN Trip in this workspace — one not closed and not archived — whose date is within ONE DAY of today in that Trip's own timezone. The ±1 day is deliberate: a Saturday's sales get written up at eleven that night or the next morning, and every one of those saves belongs to that Trip. The closest date wins and the most recently changed one breaks the tie; at most one Trip and one stop, nothing created, and a stop that already lists the Item is left alone. The reply says `tripStop` when it happened, carrying the Trip's own `tripDate` and a `url`. Name `tripStopId` to link a stop outright; an explicit stop always wins. - `buying_save_purchases_batch` — A RECEIPT IS ONE CALL: up to 100 lines, one Place, one day, optionally one Trip and one List, in a single transaction. Every line carries its own stable `purchaseKey`, so an identical resend creates no second purchase — which makes reading a photograph of a receipt safe to retry. A line may name an Item, a Type, a List entry, or nothing but its own title text. A SAME-KEY RESEND CAN CORRECT OR ATTACH: use a new `operationId`, the saved `purchaseKey`, title and totalCents (or `pricePending: true`), plus `tripId` to attach the existing line to a Trip or move it between Trips. The existing purchase keeps its ID and its money and count move with it. Omitted Item, Place, List, receipt, bundle and annotation fields keep their saved values; resend the original quantity, currency and purchasedOn to keep those too. First create the day's Trip with `buying_save_trip` when none exists. For example: `{operationId: "attach-receipt-9", tripId, purchasedOn: "2026-09-24", lines: [{purchaseKey: "receipt-9:1", title: "Coffee", totalCents: 1299, quantity: 2, currency: "USD"}]}` attaches that saved line without duplicating it. Correct a price with another new operationId and the same line, sending `totalCents: 1199`; leave `tripId` out to retain its Trip. Buying coffee again next month is a genuinely new purchase and needs a new `purchaseKey`. An identical resend and a new purchase are different events. You are writing down what the person already paid; this records a purchase and never makes one. Purchase resend guidance updated 2026-09-29. - `buying_update_list` — UP TO 500 ENTRIES IN ONE CALL (100 before 2026-10-12), because walking a shop ticking twelve things must not be twelve calls. Each change names an `op`: `add`, `tick` (bought), `skip` (deliberately passed over), `annotate`, `reset`. SKIP IS NOT UN-TICK: "still have half a bag" is a decision worth keeping. An entry can point at a record (`refKind` item, type, place, mission or library with its `refId`) or be plain text, which is the ordinary case for a shopping list. A RUN IS A TRIP: `startRun` opens the dated event this shopping is, snapshots whatever the previous run came to and resets the entries; `startRun.venueId` is where the run is shopped and becomes that Trip's first Stop (`startedRun.stopId`) — the List itself is not changed; `closeRun` snapshots this one onto its Trip and closes it. Both happen in this same call, and entries ticked here belong to the run that was open when they were ticked. - `buying_save_type` — reusable knowledge about a KIND of thing. THE NAME IS THE IDENTITY: saving "Bolo ties" twice updates one record, so search first; pass `typeId` to rename one without splitting it. Fields you do not send are left alone. `priceRange` carries its `basis` — `listed`, `comparable`, `sold` or `estimate`, the price vocabulary's own words — because a range with no basis is a number nobody can argue with. `priceSources` is where to look up prices for this kind of thing, `[{name, url?, note?}]`, at most twelve, sent whole (`[]` clears it). FILE IT UNDER A GROUP TYPE with `parentTypeId`, or `parentTypeName` which finds or creates the group. Types go at most 5 levels deep and a Type can never be moved beneath its own descendant; either is REFUSED rather than quietly dropped. Sending neither leaves it where it is; `clearParent` makes it a root again. Moving a group moves everything under it, and `movedDescendants` says how many. - `buying_save_price_sheet` — ONE COMPANY'S DATED PRICE SHEET, with its photographs as evidence. A Type holds ONE price range and one note, so a second seller's sheet would overwrite the first — and "what do sellers charge for this" is a question about several sellers or it is not a question at all. A sheet belongs to a Place (`venueId`; a company is a Place). EVERY LINE BECOMES A DATED, SELLER-ATTRIBUTED PRICE ROW ON ITS TYPE — kind `listed`, this Place as the seller, labelled "price sheet", dated the day the sheet was true — and NEVER an overwrite: nothing here touches the Type's own range or note, and next month's sheet is a second set of rows. A line's `typeName` finds or creates the Type by name and sends nothing else, so an existing Type keeps every check and range it holds. `unit` is the seller's own words for what the price is per — "per yard", "each" — kept verbatim, because $6 means nothing without it, and `quantityBasis` with `unitQuantity` is the same fact as a field the comparison read acts on: "$180 a dozen" is `lot` with 12, "$6 a yard" is `per_unit` with 1. Up to 100 lines, and ONE LINE'S FAILURE IS ONE LINE'S FAILURE: the rest save and the failed line comes back with its reason. `operationId` replays a retried CALL; `sheetKey` is your own name for THE SHEET, so re-reading the same photographs later corrects that sheet's rows instead of writing a second copy of them. - `buying_save_note` — `text` IS THE PERSON'S OWN WORDING, VERBATIM. A revision keeps the wording it replaced. YOUR READING GOES IN `aiReading`, separately, stored beside their words with your name and the date. There is deliberately no path by which an assistant rewrites `text`: one that could would be able to turn "the shipping stopped me" into "disliked the chair", and every later inference would stand on that. A revision (`noteId`) can change `kind` (note or context) and `origin`, never the record the note is on. - `buying_save_preference` — ONLY FOR AN EXPLICIT ASK. `theirWords` is what the person actually said when they asked, and it is required, because it is the difference between this tool and the next one. A new wording is a NEW VERSION, never an overwrite, attributed to you and dated. A changed `scope` is applied even when the wording is the same (`scopeUpdated: true`), and `theirWords` then comes back in `notApplied` because no new version carried it. Since 2026-10-19 an ordinary preference you create is open to you from the start and comes back in `buying_get_preferences`, unless the person switches it to Only me at /preferences. SINCE 2026-09-19 it also takes `kind` — `want`, `collection` or `habit` — with `forPerson` (a plain name; leave it out when it is for them) and `bar` (the quality or price bar in one sentence). Those three are WHAT I LOOK FOR, the profile the person keeps for their own AI; like every kind they are open to you from the start, and they come back in `lookingFor` on the next read. The reply says where the row comes back. Everything else about the tool is unchanged: still only for an explicit ask, still `theirWords`. - `buying_propose_preference` — what you INFERRED, offered for the person to accept or decline. It waits and holds no authority; nothing reads a pending proposal as a standing fact, and THERE IS NO TOOL THAT ADOPTS ONE. Do not go looking for the accept call — its absence is the design, and it is the same omission as the missing accept for a suggestion. Cite `evidence` (records you read) and `reasoning`, or the person cannot judge it. It takes the same `kind`, `forPerson` and `bar` as the save, so a proposal about what they look for is adopted as that profile row rather than as a plain preference — it still waits for their click either way. - `buying_save_item_decision` — what was decided about one Item, AND IN WHAT CONTEXT: `pass`, `shortlist`, `choose` or `defer`, with the reason and the List or Mission it was decided inside. The context is the feature: a chair passed because shipping cost $79 and later chosen during a free-shipping week for a different room is two rows, not one word that changed. Set `aboutProduct: false` when the reason was the circumstances rather than the thing. Append-only. You are recording what was decided, not deciding. ### Titles, Recaps, Lists and the Library shelf — September 17, 2026 - `buying_list_records_needing_titles` (`mission.read`) — THE AUDIT READ. Input `{ kinds?: ("mission"|"queue"|"item"|"type"|"place"|"list"|"trip"|"library")[], missing: "title"|"recap"|"either", cursor?, limit ≤ 50 }`. One kind per page; at most `limit` rows of it are READ over a workspace-first index (Missions and Library records alphabetically, the Queue by creation, the rest by last change) and filtered, so a page can be short or empty and still not be the end — follow `pageInfo.continueCursor` until `isDone`, and `scanned` says how many were read. Each row: `kind`, `id`, `currentTitle`, `titleBy` (null = nobody attributed it: a person's raw first line or a record from before titles), `recap`, `recapSource` (`you`, `ai`, `objective` or null), `needs`, `path`, and `context` — a Mission's objective, status, type and capped item/criteria counts; a Queue ask's first 400 characters, state and the names of what it points at; a Trip's dates, area, status, stop count and List names; an Item's status, Type, Place, listed price and summary; a List's behaviour, entry count and first five entries; a Type's overview and item count; a Place's kind and where; a Library record's description and `inLibrary`. Queue asks need `queue.read`; without it they are listed under `withheld`. - `buying_save_titles_and_recaps` (`mission.write`) — up to 100 rows `{ kind, id, title?, recap?, overwritePersonText?, reason? }` in ONE mutation with one `operationId`. Record kinds take both; `{ kind: "queue", id, title }` sets the ask's `aiTitle` (needs `queue.write`; an ask has no Recap and its own words are never touched). Everything written is credited `titleBy: "ai"` or `recap.source: "ai"` and dated. Per row, per part, the reply says `written`, `confirmed` (the title sent equals the one already there on a record nobody attributed — attributed to whoever wrote the row, not to you), `unchanged`, `skipped` (with `reasons`) or `not_sent`. PERSON-WRITTEN TEXT — `titleBy: "you"`, `recap.source: "you"` — is skipped unless the row sends `overwritePersonText: true` AND a `reason`; the row then says `overrodePersonText` with that reason. A rename into a name another record of that kind already holds is skipped, as is a shared directory Place. - `buying_save_list` (`mission.write`) — create (`name`) or edit (`listId`, only what changes) a List: `behavior` (`checklist`, `reusable`, `rule`), `classifications` (`favourites`, `household`, `gifts`, `research`, `places`, `projects`, `avoid`, `seasonal`), `description`, `favorite`, `dueOn` (YYYY-MM-DD), `cadence`, `rule` (`{ kind: "purchasesInWindow", window: "lastMonth" | "last30Days" | "thisYear" }`), `placeLinks` (`[{ venueId, role: "usual" | "backup" }]`), `note`, `recap`, `parentListId`. The name is the identity at its level. A name the person gave is kept (`titleDeclined`, and `name` in `notApplied`). `parentListId` with `listId` MOVES that List under another — no cycles, at most four deep, no second List of that name there. `copyFromShareToken` saves a private checklist copy of a List shared by an unlisted or public link, built only from what the link shows, and every other field in the same call is applied to the copy; a List shared with named people refuses, because an assistant is not one of them. SHARING IS NOT WRITABLE: no scope covers opening a List to other people. - `buying_save_prompt` (`mission.write`) — `{ operationId, slug?, forkFrom?, title?, whenToUse?, text, kind?, researchDepthHint? }`. The person's own copy of a prompt, private to their workspace. `kind: "work_style"` files a named work style; it does not and cannot become their default, which only they set at /preferences. `forkFrom` copies a library prompt and fills in whatever you left out; `slug` alone files a new version of one they already have, keeping the earlier wording. From then on `buying_get_prompt` answers that slug with their words and `source: "yours"`. - `buying_set_library_membership` (`mission.write`) — `{ interestIds (≤ 100), inLibrary }`. `false` takes records off the Library shelf: they leave the Library browse and its count. NOTHING ELSE CHANGES — the record, dossier, sections, claims and every Queue and Trip link stay, and the research is still returned under `attachedResearch` by `buying_get_item`, `buying_get_type`, `buying_get_place`, `buying_get_mission_context` and `buying_get_trip` (each row: `interestId`, `title`, `inLibrary`, `status`, `recap`, `description`, `path`; `window` states how far the asks were read). `buying_search_workspace` returns `library` hits on the shelf or not. Idempotent and reversible. OR, PER RECORD (2026-09-23): `{ operationId, records: [{ interestId, links?, inLibrary? }] (≤ 100) }` — never both forms in one call. Each row links an EXISTING Library record, dossier-era research included, to up to 25 Items, Types, Places and Missions (`{ kind: item|type|place|mission, id }`, ≤ 250 links a call) and sets its shelf in the same call. Every target is resolved before anything is written: an id outside this workspace (a Place may also be a public directory entry) refuses the WHOLE call with `RECORD_NOT_FOUND` naming `records[i].links`, and nothing moves. A pair already linked stays one link (`created: false`). A record id not in this workspace is `not_found` on its row. Reply: `changed`, `unchanged`, `notFound`, `linksCreated`, `linksAlreadyThere`, `offShelf`, and per row `outcome`, `shelf` (changed|unchanged|not_asked), `inLibrary`, `links`, `path`. Every linked record then lists it under `attachedResearch` and shows it under Research on its page; its own page still opens off the shelf. THE SHELF RULE: the shelf is for research that helps in bigger cases — a kind of thing, a kind of store, how a market works — and for Sources; a write-up about one Item, or only useful for one Type or Place, is linked to it and sent `inLibrary: false`. If the Item is not saved yet, save it first, then link. - `buying_read_library_document` (`mission.read`) — `{ interestId? | title?, offset?, maxChars? }`. A Library record's Markdown body, paged by character offset (default 20,000, up to 50,000 a page): `text`, `offset`, `nextOffset` (null when `done`), `totalChars`, `sections` (title and length each). Until `done` you have read only part of it — say so. Workspace-only. On a connection that withholds media every picture (inline, by reference with its address, or as HTML) is replaced by its description, the reply says `mediaWithheld: true`, and a kept file's id is withheld; otherwise a Source that keeps a file names `sourceFile.artifactId` for `buying_get_research_file`. - `buying_save_library_document` (`mission.write`) — `{ operationId, title, bodyMarkdown? (Markdown, ≤ 10,000 characters), kind? (research|source, default research), artifactId? (kind source only), summary?, sourceUrl?, documentKey?, links? (≤ 25 `{ kind: item|type|place|mission, id }`), inLibrary? }`. `inLibrary: false` keeps a write-up about one thing on the records in `links` and off the Library shelf (refused when nothing is linked); left out, a new document goes on the shelf and a correction keeps its shelf. It is your call about the words, never derived from the links. KEEPS A FINISHED WRITE-UP OR A SOURCE. The Library holds three kinds: Research (your write-up of what you read — the default), Sources (somebody else's document kept whole: a manual, a book chapter, a page word for word, a PDF or Word file) and Instructions (the person's own prompts, loops and work styles — saved with `buying_save_prompt`, never here). Either kind becomes a Library record whose body is rendered as Markdown on its own page, whose words `buying_search_workspace` reaches, which shows on the Library page under its kind, and which is listed under `attachedResearch` with `isDocument: true` and `kind` on every record named in `links`. `bodyMarkdown` is required except for a Source that keeps a FILE: keep it first with `buying_attach_research_file` and send its `artifactId` (a finished `source_capture` file in this workspace; anything else is refused by field name), and the Library shows the file's size. On a correction, leaving `kind` out keeps the kind already saved. The reply carries `url` and `kind`; give the link to the person. SAVING IT AGAIN UPDATES IT: the same `documentKey` corrects the document in place, and with no key the key is derived from the title, so the same title updates rather than duplicating. A title the PERSON edited is never overwritten by yours. Linking is the last field of this save, never a second call, and a repeated pair is one link. NOT FOR PRICES OR APPRAISALS: a price is `buying_save_prices`, a judgement of worth `buying_log_appraisal`, a dated thing seen on one visit `buying_add_place_observation`, and a standing fact about a Place `buying_save_note` with `kind: "context"`. ## What a thing IS, and what is the same thing as what WRITE MEASUREMENTS WITH THE THING, NOT IN A SECOND CALL. Every Item-writing tool -- `buying_save_item`, each row of `buying_save_items_batch`, each row of `buying_capture` -- takes a `properties[]` section, and `buying_save_research` checkpoints take one too. "38 g, sterling, tag $45" is ONE call: the grams and the material are properties, the $45 is a price. A row is `{ rowKey, key, value, unit?, text?, basis?, observedOn?, context?, factId?, expectedRevision?, retract? }`. `value` is a number, a `[low, high]` span, some words, or true/false. One shape covers every kind of thing: a citrus tree's height in feet and container in gallons, a trunk's dimensions in inches and its material, a camera's sensor and shutter count, a sterling spoon's mass in grams and its purity as a fraction. There is no per-Type schema and no migration when you write down the ten-thousandth kind of thing. UNITS. Write the unit however you have it -- `grams`, `g`, `lb`, `in`, `cm`, `gal` -- and the server converts it and keeps your words beside the number for all time. Two spellings are refused on purpose, because each is two different measurements: write **`ozt` for troy ounces** (what silver and gold are weighed in) and **`fl oz` for fluid ounces**. A bare `oz` is a question, not an answer, and is refused with that sentence rather than guessed at. A unit from the wrong dimension refuses THAT ROW and saves every other one. AN UNKNOWN KEY IS SAVED, NOT REFUSED. Write `shank_length` in a workspace that has never heard of it and the measurement is kept in your own key and your own words, marked as not yet matched to a property, and the reply's `gaps` names the nearest keys this workspace already uses. Write `wt` and it offers `weight` and `mass`. Use one of those instead of coining a third spelling -- that is the whole point of the sentence. MONEY IS NEVER A PROPERTY and a property is never a price. They are different records with different rules, and a workspace that mixed them could never separate them again. A property key that names money is refused by name with the price tool to use instead. `context` says which encounter a measurement belongs to. Leave it off (or send `"this listing"`) and it belongs to the thing being saved -- one example, seen once. Send `"standing"` ONLY for a specification of the model itself, true of every one of them. Getting this wrong joins a mass measured on one object to a price from a different listing of the model. MEASURING IT AGAIN IS A NEW OBSERVATION, NOT A CORRECTION. Send the same key for the same thing on a DIFFERENT `observedOn` day and a SECOND dated fact is saved: both stay active, both come back from `buying_get_item` newest first, and a comparison uses the newest while saying the earlier differing one is still on the record. The saved row's `outcome` says which of `created`, `corrected` or `new_observation` happened. The same day again is a correction. A supplied `expectedRevision` guards the observation selected by `observedOn`; it cannot create an observation that is missing. To correct an exact earlier figure, send its `factId` and `expectedRevision`. Send `factId` when changing its date. History is kept either way. A REFUSED ROW IS IN `gaps` TOO. A measurement that could not be saved honestly costs that row and nothing else, and the reason arrives both in `properties.refused` and in the reply's `gaps`, where the rest of the work to do in this pass already is. WHAT MATTERS FOR A KIND OF THING. `buying_save_type` and `buying_save_types_batch` take `properties: [{ key, label, meaning, valueKind, dimension?, importance, help? }]` -- which properties decide a purchase of this kind of thing and what each one means. `meaning` is REQUIRED for a key nobody has defined and is FIXED once anything is saved under it: "weight" meaning the object and "weight" meaning the shipped box are two properties, and a different meaning needs a different key. `buying_get_type` returns the whole vocabulary, a Type's own and every ancestor's, marked which is which, with the unit each key is compared in. `buying_get_item` returns what one Item actually has, with the unresolved ones listed separately. IS THIS THE SAME THING? `buying_save_identity` answers every identity question, because they are one question. Actions: `person` (create, rename, add the aliases that let a name resolve), `resolve_person` (words in, match or candidates out -- it writes NOTHING and is the call to make before you guess), `merge_people` / `unmerge_person`, `link_items` (two Items and what they share: `same_model`, `same_listing`, `same_physical_object`), `type_alias`, `merge_types` / `unmerge_type`, and `split_type`. PROPOSE; DO NOT MERGE. A similar title or a shared photograph is a good reason to PROPOSE a match and never enough to settle one -- send `state: "proposed"` and the person confirms it on the Item's page. A `confirmed` link whose basis is an inference or a comparison estimate is refused outright, because every deduplicated count downstream believes a confirmation. Confirming one relation never implies another: `same_model` is two of the thing, `same_physical_object` is ONE thing seen twice, and collapsing them is how "I saw three of these" becomes "I saw one of these three times". TWO PEOPLE WITH ONE NAME ARE TWO PEOPLE. "Mom" written twice is two mentions, never proof. `forPerson` and `boughtBy` keep their words always; `forPersonId` and `paidByPersonId` sit beside them and are set only when you send them. Send words alone and the reply's `gaps` offers the Person it matched and the id to pass -- it will not link for you. A BIG MERGE FINISHES ACROSS CALLS. `merge_types` repoints what is filed under the retired Type in bounded batches and reports `repointed` and `repointDone`. If `repointDone` is false, send the SAME call again with a NEW `operationId`: it continues where the last one stopped and reports `alreadyMerged` true, which is not an error. Reads resolve through the redirect the whole time, so nothing is broken in between. `split_type` reports the same pair, and it takes each moved Item's own prices, measurements and purchases with it. NOTHING IS EVER DELETED. A merge is a redirect: the merged record keeps its id and its whole history, everything filed under it still resolves, and the unmerge actions take it back. A Person here is a private note about somebody this person buys for; it is not an account, a contact or a collaborator, and nobody is ever contacted, invited or notified. - `buying_save_identity` (`mission.write`) -- `{ operationId, action, name?, personId?, fromPersonId?, intoPersonId?, aliases?, itemA?, itemB?, relation?, state?, typeId?, fromTypeId?, intoTypeId?, plan?, basis?, expectedRevision? }`. One tool for every identity question. `resolve_person` writes nothing. Every merge, unmerge and link needs a `basis`, and the `method` on it decides what is allowed: `person_reported`, `direct_observation`, `measurement` and `source_reported` can confirm or merge; `inference`, `comparison_estimate`, `general_knowledge` and `unknown` can only propose. `split_type` takes an explicit plan and leaves every Item it does not name exactly where it is. ### Purchases that add up, Types that can be tidied, what they already own (2026-09-24) A cloud AI worked the Queue all day while Scott stood in three bin stores. These are the calls it did not have. - `buying_void_purchase` (`mission.write`) -- `{ operationId, purchaseId?, purchaseKey?, reason?, restore? }`. Takes a purchase line recorded by mistake out of EVERY total: the Item's and the Trip's spend and count, the day's total, spend summaries, the Type's purchase count, the seller's pricing profile (the line's sale-day markdown, and the bundle's discount once no line of that bundle is left) and `alreadyOwned`. Nothing is deleted: the line stays readable, struck through on the person's pages with your `reason`, and `buying_list_purchases` returns it with `voided: {at, by, reason}`. `reason` is required to void. Idempotent: voiding a voided line replies `alreadyVoided` and changes nothing; `restore: true` puts it back everywhere it counted, and restoring a line that is not voided replies `alreadyRestored`. Never re-send a mistaken line at $0 -- a $0 line is a claim that something was free. - PRICE PENDING, on `buying_save_purchases_batch`: a line may send `pricePending: true` with no `totalCents`. It is recorded as bought; every total counts it as unpriced (`totals.unpricedLines`, `totals.unpriced: "1 unpriced line"`) and never adds it as $0, and reads return its `totalCents` as null with `pricePending: true`. To fill the price, send the SAME `purchaseKey` again with `totalCents` (a new `operationId`): the line replies `priceFilled: true`, the Item's and the Trip's spend move by the price, and the row records who filled it and when. A line that already has a price is never un-priced by a later pending re-send (`priceKept`). A pending line cannot be part of a bundle. A line with neither `totalCents` nor `pricePending` is refused. - A LINE WITH `findingId` MOVES ITS ITEM TO PURCHASED, in the same transaction: the status is appended once (never doubled on a retry) with "Bought (day) at (Place)" in its history, the Item takes the purchase's Place only when it had none, and the line and the reply carry `itemStatusUpdated`. A separate `buying_set_item_status` call is unnecessary. - `buying_archive_type` (`mission.write`) -- `{ operationId, typeId, reason }`. Archives a Type that holds nothing; it leaves the Types page and keeps its history, and the person can restore it from its page. REFUSED with `TYPE_NOT_EMPTY` while it still has Items or live child Types, and the refusal names up to five of each. Archiving one already archived replies `alreadyArchived`. - `buying_merge_type` (`mission.write`) -- `{ operationId, fromTypeId, intoTypeId, reason }`. Folds `fromTypeId` into `intoTypeId`: Items, their purchases, price rows and measurements (through the bounded re-point job), Notes and child Types move to the target; the target's own words win and only what it lacks is filled from the source (`priceRange`, `priceNote`, `overview`, `checks` -- listed in `filledOnTarget`); the source's property vocabulary is copied where the target does not already declare it (`propertiesCopied`). NO CYCLES: when the target sat under the source it first takes the source's place in the tree (`targetMovedUp`). The source is archived behind a REDIRECT (`mergedIntoId`): its old /types link, its old `typeId` in any later save and `buying_get_type` (which replies `redirectedFrom`) all resolve to the target. Bounded: when `finished` is false, send the SAME call again with a NEW `operationId` until it is true; it picks up where it stopped and reports `alreadyMerged`. For a split, an alias or taking a merge back, use `buying_save_identity`. - NEAR NAMES on `buying_save_type`: a NEW Type whose name is very close to an existing live one (normalized words, Dice similarity 0.75 or more: "Board games" beside "Board & card games") still saves, with a `TYPE_NAME_CLOSE` warning naming the close Type and its id. Merge them with `buying_merge_type` if they are one kind of thing. - `buying_what_do_i_own` (`mission.read`) -- `{ query?, typeId?, findingId?, limit?, priceCents?, currency? }`. Since 2026-10-12 it also reads List entries kept as owned or wanted (`onList`, `ownedOnLists`, `fillsGap`, `vsBudget`; see "At the shelf" above), and since 2026-10-24 the lines of Library documents titled as a collection (`inCollections`, `ownedInCollections`, `sources`). What the person already owns: `count` (quantities summed over purchases of the same Item and close titles, plus the Item's `ownedQuantity`), `sameTypeCount` (other things of the same Type, kept separate), a one-line `summary`, and each purchase's `purchasedOn`, `totalCents` (null while unpriced), `place`, `match` (`same_item`, `similar_title`, `same_type`) and `path`. A query that is a Type's exact name reads that Type too. Bounded: at most `limit` (default 20, up to 50) recent purchases from each source; `bounds` says so. Voided lines and a companion's buys never count. - `alreadyOwned`, the same block, rides `buying_search_workspace` (for the query, and on each of its first five `findings`), `buying_save_item`, every saved row of `buying_save_items_batch` and `buying_capture` (leaving out the purchase that same call records), and `buying_log_appraisal`. It is present only when something matched. A close title means most of the shorter title's words appear in the longer one, with at least two shared (one-word titles must be the same word). At most 20 recent purchases per source are read. `ownedQuantity` (0 to 100000) is a new optional Item field for things the person had before the app. Before recommending, check `alreadyOwned` and mention it in one line. Duplicates are often fine -- inform, don't discourage. ### Research memory (2026-09-21) Scott's central complaint, in his own words: an AI inspects many websites and hundreds of examples and then saves one paragraph. These two tools are where the material actually obtained goes, so that in a year another AI can reread the EVIDENCE rather than our summary of it. The screens call it Research, Sources, Basis and History. FOUR THINGS THAT ARE NOT THE SAME THING. A PASS is one piece of research, with the exact question and work style frozen at the moment it started. A SOURCE is a continuing identity — this listing, this page, this person who told you. A CAPTURE is what was retained from one source at one moment, and it is immutable. An ENCOUNTER is one visit to one source inside one pass, including the visits that failed. A source checked twice is two encounters even when they share one capture, and a page that would not load is a real encounter with `status: 'blocked'` or `'requires_user'` and NO capture — that is the honest record, and it is more useful than a fabricated one. - `buying_save_research` (`mission.write`) — `{ operationId, action?, passId?, expectedPassRevision?, question?, passKey?, request?, queueId?, subjects?, sources?, captures?, entries?, claimEvidence?, checkpoint?, limitations?, coverage?, reportedActivity? }`. CHECKPOINT AS YOU GO, not once at the end. The first call needs only `question`; a pass is created for it and the reply carries its `passId`, which every later call sends. Each call is one bounded atomic mutation and is durable on its own, so an interrupted session keeps everything already sent. `expectedPassRevision` is required only when you are REVISING the checkpoint note — appending encounters never needs it, because two clients appending independent encounters can both succeed. PAGE TEXT GOES IN `captures[].text`. Up to 192 KiB of the page's own words ride in the ordinary tool call — no upload, no byte transfer, nothing most clients cannot do. A longer page continues as further captures of the same source, each with `partOfCaptureRowKey` naming the first and its own `partIndex`. A capture of BYTES (a PDF, a photograph, an original) names a `researchArtifacts` id that is already `ready`; one still uploading is refused as `artifact_not_ready`, and the encounter can name it as `pendingArtifactId` meanwhile. The SHA-256 of what was retained is computed on the server; a hash you send would prove nothing about the bytes the row holds. A SOURCE IS NEVER IDENTIFIED BY ITS TITLE. Send a `url`, or — for a platform listing — a `platform` with its stable `externalListingId`, or an `identityKey` of your own. Two listings that share a headline are two listings; merging them would manufacture a second independent sale out of one. Tracking parameters are stripped from a URL; anything that could name a different listing or variant is kept. EVERY ROW CARRIES YOUR OWN `rowKey`, unique across the whole call. A row that cannot be saved comes back under that key in `refused` with a `code` and the `fix`, and the valid rows in the same call still save. A row whose referenced new row was refused gets `dependency_not_saved` rather than being saved as a fact with nothing behind it. A reference to something this connection may not read answers `unavailable_reference` and never says whether it exists. One call carries at most 100 rows and 256 KiB, both counted before anything is written. THE RECEIPT: `accepted`, `refused`, `replayed`, `savedCoverage` (what the server counted: encounters, ready captures, pending artifacts), `reportedCoverage` (what YOU said you did — the site cannot see your browser and stores the two separately and never merges them), `gaps`, `derived`, `next`, `path` and `effectiveRequest`, the exact work style in force. `effectiveRequest` carries `styleName`, the frozen `instructions`, `styleVersion`, and — when a named style governed — `styleRefId` and `promptRevisionId`, the exact wording it was. `requestNote` beside it says how honestly this site knows what was asked for: what you reported, what the person chose here, or that nothing was conveyed and the interpretation is unknown. A `request.styleRef` naming a style this workspace does not have is not an error — the request resolves to their default and the receipt tells you which style really ran. `gaps` are mode-aware: a Ballpark pass is never told to add more sources, because Ballpark is an instruction not to go researching. FINISH with `action: 'finish'` and `coverage`. `complete: false` with `reasons` records `completed_with_limits`, which is a first-class honest outcome rather than a failure. Finishing NEVER upgrades pages you reported examining into captures that were retained. A QUICK ESTIMATE NEEDS NONE OF THIS. If the person asked for a ballpark and you answered from what you already knew, answer them and log the price with `buying_save_prices` or `buying_log_appraisal`. Do not manufacture a research pass to have something to save. `action: 'submit_assessment'` SAVES A CONCLUSION TOGETHER WITH THE EXACT THINGS IT RESTED ON, in ONE atomic mutation. Nothing is staged, nothing is uploaded, and there is no "assessment pending" state: `{ question, conclusion, basis, method?, methodVersion?, limitations?, outputs?, subjects?, passId?, selection?, input?, supersedesAnalysisId?, reviewOfAnalysisId?, sawPriorElsewhere? }`. `selection` names what you actually used, by id: `{kind:'price', observationId, expectedRevision?}` for a saved price (its exact immutable version is resolved on the server, baselining an older row if it has never had one), `{kind:'revision', revisionId}` when you already hold that version, `{kind:'capture', captureId}` for a page you retained, `{kind:'analysis', analysisId}` for an earlier assessment, and `{kind:'legacy_value', record, expectedValueHash}` for a living record — Item, Type, Place, Mission, Appraisal, purchase, preference or prompt. Get that hash from the record's own read (`frozenInput.valueHash` on `buying_get_item`, `buying_get_type`, `buying_get_place`, `buying_get_mission_context`, `buying_get_prompt` and each preference row, or `buying_get_research` with `inputRecord` and `section: 'value'`, which covers every supported family). A record edited since you read it is refused as `STALE_INPUT` with the instruction to read it again; a family with no resolver is refused BY NAME rather than quietly left out of the dependencies. Up to 250 inputs. THE SERVER WORKS OUT WHAT ELSE THOSE INPUTS DEPEND ON, and you never send it. A capture brings its source, its artifact and any original photograph the bytes came from; an earlier assessment brings its whole closure; the subjects you attach, the pass and the style in force arrive as access-only dependencies. That is what makes a restriction applied LATER reach this conclusion. Dependencies are capped at 250 and copied record values at 512 KiB in total; over either, the save is refused by name with the remedy, no partial assessment is written, and the research you already saved is untouched. `inputsHash` is the SHA-256 of the canonical ordered dependency list. A QUICK ESTIMATE IS ONE CALL: question, conclusion, `basis`, and `input: {kind:'no_external_inputs', reason}` naming the assumptions that matter. No pass, no sources, and the reply carries a url. It is stored as YOUR DECLARATION — the site cannot see an external browser and never presents it as proof of what you read — and it is refused if you also send a `selection`. A `basis.method` of `source_reported`, `calculation` or `comparison_estimate` with nothing selected is refused as `BASIS_WITHOUT_INPUTS`: select the saved prices and captures you used, or save it as a general-knowledge estimate. A good guess is allowed when it is labelled; it may never read as a recorded sale, a confirmed preference or a decision the person made. ASSESSMENTS ARE IMMUTABLE. A new version is a NEW ROW: `supersedesAnalysisId` is the same author replacing their own earlier view, `reviewOfAnalysisId` is somebody else's assessment OF it. Both branches may coexist, and neither is a vote — a second opinion does not outrank a first by arriving later. TO REVIEW SOMEBODY ELSE'S WORK: read the evidence with `reviewView: 'evidence_first'` first, form your own view, save it with `reviewOfAnalysisId`, and only THEN read the earlier assessment to compare. The reply's `priorConclusionExposure` says `not_served_before_save` when this site had served you the evidence-first view and never the full assessment for that target, `seen` when it had, and `unknown` otherwise. Silence is `unknown`, never independence. `sawPriorElsewhere: true` moves it to `seen` and nothing moves it back: the site records what it served, and cannot certify anything about a conversation it never saw. THE RECEIPT carries `assessment` — `analysisId`, `inputKind`, `inputsHash`, `dependencyCount`, `priorConclusionExposure`, the subjects it was attached to — plus `url`, `gaps`, `derived` and `next`. Give the person that url. - `buying_get_research` (`mission.read`) — EXACTLY ONE selector (`passId`, `subject`, `sourceId`, `captureId`, `analysisId`, `inputRecord`, or `workspace: true`) and EXACTLY ONE `section`. Each pair is one indexed range that fixes the workspace first; a pair with no index is refused by name and told which pairs exist, rather than served by a scan that stops working later. Sections: `overview` (the default — the pass, its frozen work style, saved versus reported coverage), `encounters`, `captures`, `assertions` (which retained bytes a particular VERSION of a claim rested on), `history` (every version of every corrected record), `continuation` (the checkpoint, so another AI can pick up where one stopped, and disagree with its plan), `assessments`, `text`, and — with `analysisId` — `packet` and `freshness`. RESUMING SOMEBODY ELSE'S WORK. `buying_get_brief` and the record reads carry `openResearch`: open passes with a checkpoint, newest first, each with its question, the work style that governed it, a checkpoint excerpt, its subjects, its age and its `passRevision`. Read one with `section: 'continuation'`, read its evidence, and then continue it with `buying_save_research` sending the SAME `passId` AND `expectedPassRevision`. IF YOU LOSE THE RACE, YOUR EVIDENCE IS STILL SAVED. A stale `expectedPassRevision` does not discard the call: the sources, encounters, captures and property rows it carried are independent append-only rows, and they are in `accepted`. What is refused is the CHECKPOINT — the one field two clients can disagree about — and the reply carries `checkpointConflict` with `expected`, `current` and `currentCheckpoint`, so you merge what the other client wrote with yours and send it again at the revision this reply returns. A lost race also cannot `finish` a pass it has not caught up with. A PASS THE PERSON STOPPED IS NOT OFFERED BACK. Abandoning is their act, on their own screen, and it is a different thing from a client that stopped sending: an interrupted pass stays open and is still yours to pick up. Everything a stopped pass found stays readable — stopping says the work ended, not that it was worthless — but nothing further attaches to it, including an upload that began before they stopped it. `openResearch` OBEYS WHAT THE PERSON ALLOWED THIS CONNECTION. Research about somebody they keep private is absent from it entirely — not shortened, not described, not counted — and a connection narrowed to selected Missions sees only work whose subjects are all inside that selection. A LATE `buying_save_research` ON A STOPPED PASS IS REFUSED WHOLE, code `PASS_STOPPED`. Not partly: nothing in the call is written, because half of it landing in a pass the person ended is the thing they stopped. YOUR PAGES ARE NOT LOST — send the same call again WITHOUT `passId`, with the same `question` and the same rows, and it starts a follow-on pass naming the stopped one as `continuedFromPassId`. The material is then kept under a pass they can see, beside the one they ended. A pass the person ERASED answers `PASS_ERASED` the same way, and no follow-on ever names an erased pass. `assessments` (on a `passId`, `subject` or `workspace: true`) lists every saved conclusion with its chain (`supersedesAnalysisId`, `reviewOfAnalysisId`), the writer AS REPORTED — a client's own name for itself is a label, never a verified identity — its `inputKind`, `inputsHash` and `dependencyCount`. A pass with none answers EMPTY and `available: true`: "none yet" and "no such section" are different facts. `analysisId` alone returns one assessment: its conclusion, basis, outputs, frozen request snapshot, and whether anything it rested on has moved since. `section: 'packet'` renders the frozen inputs themselves as `awb-input-packet/1` JSONL — a header row, then one row per dependency carrying the frozen payload from the price revision, the capture's metadata and hash, or the copied record value. It is RENDERED ON READ, never stored, and it quotes what the assessment was COMPUTED FROM: a price corrected afterwards still renders its OLD value here, and `section: 'freshness'` is where "and it has moved since" is answered, per input, with no workspace-wide counts. A change is not a verdict — a correction may leave the conclusion perfectly sound. AN ASSESSMENT IS WITHHELD WHOLE. Every dependency root is re-checked live on every read, and if any one of them is out of this connection's reach the conclusion, the question, the outputs and the packet all go together, behind one short non-revealing denial. Showing a sensitive conclusion while hiding only its citations would be the worse answer, not the safer one. A withdrawn assessment answers with a non-content tombstone saying it was withdrawn and when, because that is a different fact from never having existed. `inputRecord: {kind, id}` with `section: 'value'` returns the exact fields an assessment would freeze for that record, plus the `valueHash`, `hashFormatVersion` and `readAt` to send back as `expectedValueHash`. Item, Type, Place, Mission, Appraisal, purchase, preference and prompt are supported; anything else is refused by name. CAPTURE TEXT is read one capture at a time: `captureId` with `section: 'text'`, plus `offset` and `maxBytes`. Positions are UTF-8 BYTES, so an evidence locator still means the same place after paging; a range that falls inside a multi-byte character is returned as asked with `startsMidCharacter`/`endsMidCharacter` set rather than quietly moved. `workspace: true` with `section: 'captures'` returns EVERY retained capture in immutable recorded order, with no Type, Item or classification required. That is the read to reach for when today's filing looks wrong. `reviewView: 'evidence_first'` withholds prior conclusions so you can form your own view of the same material. It is a statement about what this site SERVED you and nothing more — it is never a certificate that your context is free of the earlier answer. Pages default to 250 lean rows, at most 1,000, aiming at 128 KiB, with a scan budget that can return an EMPTY page and a cursor rather than scanning on. `coverage` is `mixed_live`: rows move between pages as corrections land, so deduplicate by id. A cursor is bound to the selector, section and format that produced it and is refused against any other. `format: 'columns'` is available for a machine read. Older paths that wrote "a source was checked" — `buying_log_sources`, `buying_save_option`, `buying_save_complete_result` and an Appraisal's `sourceChecks` — now ALSO append a durable encounter through the same writer, into one clearly labelled pass per Mission or Appraisal. Their own replies and shapes are unchanged. No capture is fabricated by those paths: they carry a URL and a sentence, and never held the page. Extensions that came with them: - `buying_save_mission` — `missionId` alone identifies the Mission; send only what changes (`title`, `objective` — the same field as `goal`, `recap`, `status`…). A create still needs `missionKey`, `title` and `type`. `status` no longer defaults on an edit. - `buying_get_mission_context` — `objective` and `recapSource` travel beside `recap` in both verbosities, with `attachedResearch`. - `buying_save_trip` — `recap`; the title is credited `titleBy: "ai"`, and a title the person wrote is kept (`titleDeclined`). `buying_get_trip` with a `tripId` returns `title`, `titleBy`, `recap`, `recapSource` and `attachedResearch`. It also returns `area` and `areaDerived`: when the stops span more than one place and nobody typed an area themselves, the `area` shown is read off the stops ("Surprise, Sun City, Glendale & Peoria", capped at four names then "+N") and `areaDerived` is true. Nothing stored is rewritten — save an `area` on the Trip to say it in the person's own words. - `buying_list_lists` — `cursor`, `behavior`, `includeArchived`; rows carry behaviour, counts, visibility, recap and `titleBy`, and the reply carries `pageInfo`. - `buying_get_list` — `list` gains behaviour, classifications, description, due, cadence, rule and Places; `entries` is one page in the person's order with every field (`state`, `why`, `use`, `priority`, `budgetCents`, `neededBy`, preferred Place, `refKind`/`refId`, `order`), paged by `entriesCursor`/`entriesLimit`; `currentRun` and `runs`; `ruleRows` for a rule List with `today` and the resolved `window` (send `today` to inspect another day); `sharing` read-only — visibility, share fields, a count of named people, whether a link exists, never the link or the addresses. The older `items` projection is unchanged. - `buying_update_list` — `order`, the listItemIds in the order they should read, applied after the changes. Taking an entry off a List and putting a List away are the person's, on the page; no tool here does either. FEWER CALLS, SAME RESULT. Read once at the start (`buying_get_brief`, the one first call; `buying_get_preferences` only for more of their Preferences); batch every sweep; ask for a page and follow its cursor rather than raising a limit until a table comes back; link a record in the same call that saves it; prefer the one tool that summarises a block over assembling it from five. ### Retained research files — September 21, 2026 - `buying_attach_research_file` (`research.files.write` + `mission.write`) — `{ action, artifactKey?, artifactId?, passId?, sourceId?, filename?, mimeType?, sizeBytes?, sha256?, parts?, contentBase64?, sourceUrl?, retention?, visibility? }`. Keeps the SOURCE ITSELF, not a note about it. `action: "begin"` is the normal route and the only one that can carry a file larger than a single first-party request: the file is split into deterministic contiguous parts of at most 2 MiB (zero-based, every part but the last exactly 2 MiB), up to the per-file ceiling the tool's own description states — it is set per deployment, so read it there rather than assuming a number. Send the whole-file `sha256` and one `{part, sha256}` per slice; the reply carries the plan and one short-lived first-party https://assistwithbuying.com PUT URL per part. PUT each slice with `Content-Type: application/octet-stream` and its exact `Content-Length`, then call `action: "finish"` with the `artifactId`. Never echo, save, log or narrate an upload URL: each is a one-object write capability. `finish` re-reads every part's length, digest and exact storage version, checks the reassembled file against your whole-file hash, checks what the bytes actually are, and re-checks the person's permission at that moment. Nothing half-arrived is ever evidence, a replayed finish counts once, and a finished manifest is IMMUTABLE — a correction keeps a new file beside the old one. `action: "resume"` reports the parts still missing and issues fresh URLs for exactly those; identity never changes across a resume. `action: "cancel"` removes an UNFINISHED upload only; a retained file is never removed by any tool here. `action: "inline"` takes a small file as `contentBase64` (at most 128 KiB, because the whole request is capped) and `action: "fetch_url"` has the server fetch ONE image over https from a direct image address — the existing narrow image fetch, not a crawler. Retainable: `text/plain`, `text/markdown`, `text/csv`, `text/tab-separated-values`, `application/json`, `application/jsonl`, `application/x-ndjson`, `application/pdf`, `text/html`, and JPEG/PNG/WebP/GIF. HTML is retained as INERT DATA — nothing in it is rendered, executed or fetched. Archives are never unpacked and a stored script is never run. WHAT IS KEPT IS SAID HONESTLY. Since 2026-09-24 a picture is kept byte for byte, camera and location metadata included, on every route, so it is an uploaded original. A picture retained before then through inline or fetch_url is a privacy-processed master (metadata removed). `retention` distinguishes an uploaded original, a privacy-processed master and the thumbnail a source site published, and the reply says which you have. - `buying_get_research_file` (`research.files.read` + `mission.read`) — `{ artifactId, mode?, startByte?, maxBytes?, size?, crop?, purpose? }`. `mode: "manifest"` (the default) returns what the file is, its size, its hash, its part plan and its retention. `mode: "text"` returns a bounded UTF-8 page with the exact byte offsets and line numbers a citation needs, plus the `startByte` to ask for next — say plainly when there is more rather than implying you read the whole file. `mode: "image"` returns the picture inline as MCP image content. A retained original too large to send inline comes back as a SMALLER COPY of it, labelled as one, with the original's own dimensions and `sha256` beside it in `retainedOriginal`: `size` picks which copy (`thumb` 320, `preview` and `ai` 1600, `large` 2560) and `crop: { x, y, width, height }` — fractions of the retained image between 0 and 1 — asks for one region of it, which is how you read a maker's mark or a price tag that a whole-frame copy blurs. Crops are snapped to a bounded grid, so ask for the region you need rather than sweeping. Say you are looking at a smaller copy when the detail matters, and never describe a picture you have not seen. `mode: "parts"` returns one short-lived first-party download URL per part with its offset, length and hash: GET them in order, concatenate at those offsets, and the result must hash to the file's `sha256`. Every download is authorized again at the moment it is served, so a permission the person changes now stops the next part. IF YOUR CLIENT CANNOT WRITE FILES OR FETCH URLS, say so to the person and use the bounded modes. Never claim a file is in your context when it is not. A file id alone is never a way in. A part of a retained file cannot be opened through any photograph route, and the read is written to the person's own media record exactly as looking at a photograph is. ### Connection self-management - `buying_manage_connection` — read this connection's own authority, or file one request for the person to approve. Not a required first step. `describe` returns disjoint held-now / available-to-request / waiting-for-the-person / legacy fields, a revision number, the starter-policy provenance, and the standing rules. `request_access` is additive only: it needs a stable `operationId` and the revision from the latest `describe` (the older spelling `opId` is still accepted on this one tool), it never narrows, removes, or revokes anything, it changes no live authority by itself, and it returns a link the person clicks. A retry with the same `operationId` returns the same request rather than filing a second one. A connection waiting for the person's approval is listed exactly this one tool, never an empty catalogue. These four Places tools are the narrow public exception to the otherwise private workspace contract. They cannot publish or delete Mission research: publication requires explicit `confirmAnonymizedShare: true`, is privacy-scrubbed, and withdrawal is limited to the same person's contribution. ## All of them, and how complete that is — `buying_prepare_dataset` Every other read here answers a conversation-sized question. This one answers "is this all you got?", and it exists because a page of fifty rows standing in for four thousand is exactly how a person comes to believe their AI read everything. THE THREE LABELS ARE THE FEATURE. The rows are ordinary; the sentence about how complete they are is what an ordinary read cannot honestly give you. - `complete_at_cutoff` — every row that matched this selection at one committed position in this workspace's own history, reconstructed from immutable revisions. Corrections above that position do not appear, retractions above it do not remove, and a receipt entered today about last year is outside it by construction. It is REFUSED unless every record family in the selection has passed its baseline barrier, and the reply says which check failed. You cannot ask for it. - `mixed_live` — read from the current indexes while the workspace kept changing. A true account of what was there as it was written, and NOT a snapshot: a correction can move a row between parts, so deduplicate on `recordKey` and `revisionId`. - `selected_inputs` — exactly the inputs somebody named. Reproducible in full, and never a claim that nothing else matched. - `buying_prepare_dataset` (`mission.read`; a part's BYTES additionally need `research.files.read` on the SAME connection) — `{ action, datasetKey?, datasetId?, select?, subjectKind?, subjectId?, typeId?, includeSubtypes?, venueId?, fromDay?, toDay?, propertyKey?, predicate?, low?, high?, unitCode?, passId?, analysisId?, families?, includeRetracted?, fields?, part?, startByte?, maxBytes? }`. `action: "plan"` is a dry run: the index it would use, the honest label and why it is not stronger, a SAMPLED row estimate (a sample, never a total), and anything this server will not do. It prepares nothing. `action: "start"` takes your own `datasetKey` and builds it in ordered JSONL parts. The same key is always a replay — never a second dataset, never a second walk. `action: "status"` reports phase, parts with their row counts and SHA-256s, bytes, the manifest hash, and for a complete dataset the exact number of rows IT RESTS ON that have changed since its cutoff — or "at least N" when the walk was bounded. It is never a workspace-wide count, because "something in your workspace changed" is true every day and means nothing. `action: "resume"` continues a stopped job from its own recorded position; a worker whose lease has moved on can read but can never publish, so nothing is written twice. `action: "cancel"` stops it and leaves the parts already written. They are PARTIAL. Do not total them, do not describe them as the answer, and do not say "complete". `action: "read_part"` returns a bounded text page of one part with byte offsets. SELECTORS ARE A CLOSED LIST and will stay one: `workspace_stream`, `subject`, `type` (with `includeSubtypes`), `type_subtree`, `place_days` (inclusive local days, YYYY-MM-DD), `property_predicate` (your own property key with `point_in`, `fully_inside`, `overlaps`, `equals` or `exists`), `research_pass`, `analysis_inputs`. This is not SQL and not a filter tree; an unsupported selector or shape is refused by name rather than approximated. A Type subtree's expansion is FROZEN into the dataset when it is planned, so a later Type split cannot silently change what the dataset was an answer to. A subtree exceeding 500 Types is refused: choose a smaller branch or select the Type without `includeSubtypes`; a truncated expansion cannot claim completeness. `families` accepts only `observations` and `propertyFacts`. `sources`, `captures`, `entries` and `analyses` are refused until their bounded scan paths exist, rather than publishing an empty dataset with omitted records. `families` names the record families to carry; `includeRetracted` makes a retraction a finding rather than a silent exclusion; `fields` narrows what each row carries and can never reach a field the stored payload does not hold. EVERY DELIVERY IS DECIDED AGAIN. The live grant, the dataset's own visibility, the workspace's access epoch and its dependency roots are re-checked on every part, including direct retained-file reads and reads or saves of dataset-backed assessments. A changed epoch requires current authorization checks; an unaffected dataset clears after those checks, inline for small datasets or through viewer-specific current-epoch revalidation for larger ones. Pending revalidation withholds delivery. A restricted or erased input means the dataset is WITHHELD — never silently stripped of the affected rows while keeping the same manifest hash, because then the hash would be a signature over something that had changed. WHAT A DATASET DOES NOT CONTAIN. The privacy rule is applied at SELECTION rather than at delivery. A dataset prepared for your connection never holds a property fact the person marked only for themselves; holds no Person row at all if they narrowed you with `withholdPreferences`; and holds nothing outside the Places, Lists or Missions they pointed you at, on EVERY selector including `workspace_stream`. A selected-Mission connection reaches no row that belongs to no Mission — the Build 1 rule, in bulk. Those rows are not in it, rather than in it and withheld, which is why its `rowCount` is true FOR YOU and is not the workspace's total. Do not report it as one. NAMING SOMETHING YOU WERE KEPT AWAY FROM is refused with the ordinary unavailable-reference sentence, word for word — an excluded Place, or a pass the person stopped or erased. You cannot tell "outside your selection" from "no such id", which is the distinction the narrowing and the stopping were for. A DATASET ANSWERS THE CONNECTION THAT PREPARED IT, and no other. A dataset is not a workspace record that happens to be readable: it is an answer assembled under one connection's selection, and another connection's would give different rows. A second connection on the same account gets `PREPARED_FOR_SOMEBODY_ELSE`; the remedy is cheap and the refusal says it. One prepared on the person's own screen, over their whole workspace, answers no connection at all. `REVALIDATING` IS A DIFFERENT ANSWER FROM `WITHHELD`, and it is retryable. A dataset resting on more records than a single read can clear is withheld while its permission recheck runs in steps; each call you make moves that recheck on, and `next` says retry shortly. Nothing is wrong, and there is nobody to go and ask. YOUR OLDER RECORDS ARE IN A LIVE DATASET. A row written before this history was kept and untouched since comes back marked `legacy_unbaselined` — a current value with nothing behind it, which is worth knowing and is not a reason to leave it out. `plan` says how many such records a selection holds. `complete_at_cutoff` cannot carry them, because a record with no history has no state at a past position, and it says so. A DATE WINDOW REPORTS WHAT IT COULD NOT DATE. `undated` counts the rows excluded because nobody wrote down when they happened — excluded and counted, never filed under today and never quietly dropped. NOT A SELECTABLE FAMILY: outcome events. They keep no revisions, so an outcomes dataset could only ever be `mixed_live` and would need its own scan path; asking for one is refused by name with the families that do exist. Linked Appraisal copies follow the current assessment's availability immediately, including pending erasure. Attached research windows report direct Library-link and deduplicated Library/Queue overflow. Comparable duplicate suppression is per price kind, normalized currency and quantity basis; undated continuation finishes without replaying rows. Dataset freshness includes records that left the selector, and reports non-exact if historical membership hits its bound. YOUR SUMMARY IS NOT PROOF YOU READ IT. This site records what it SERVED you: parts, bytes, and when, per connection. The person can see that number. Say how many parts you actually fetched, and never describe a dataset you asked for and did not read. AN ASSESSMENT OVER A DATASET is `buying_save_research action submit_assessment` with `input: {kind: "dataset", datasetId}`. The dataset IS the selection, so never send `selection[]` as well. The Analysis depends on the dataset's access root and its parts, not on a copy of its membership — forty thousand rows do not become forty thousand dependency links — and reproducibility lives in the manifest hash, which is checkable. A SECOND OPINION adds `reviewOfAnalysisId` and `review: {verdict, basis, findings?}`. `verdict` is `agrees` | `disagrees` | `revises` | `inconclusive`; `basis` is `same_inputs` | `new_data` | `different_method` | `new_sources`. The basis is the half that carries the information: agreeing after reading the same rows and agreeing after finding new ones say different things about a conclusion. A review with no target is refused by name. THE OWNER'S ARCHIVE, WITH THE BYTES, is theirs alone. The portable export you can reach is a complete copy of the RECORDS and says so; the byte-inclusive archive is built from a signed-in session on their own screen, is resumable, names everything it could not include and why, and has no tool, no scope and no permission that reaches it. That is not an oversight. ## Stable-key and correction rules Use lowercase stable keys with letters, numbers, colon, dot, underscore, or dash: - `missionKey`: durable goal, for example `replace-kitchen-table`. - `productKey`: reusable model identity, for example `vitra:eames-dsw`. - `optionKey`: one Mission-specific offer/listing/source, for example `marketplace-listing:12345`. - `criterionKey`: stable comparison row, for example `landed-cost`. - `observationKey` (a price row's `key` in `buying_save_prices`): source/event identity, usually Item or option + observation date + source. Sent again with different content it corrects that price in place and keeps the earlier value; it is never silently skipped. - `sourceKey`: Mission-scoped source check identity. - `decisionKey`: dated decision identity. - `operationId`: unique request attempt. Reusing one with different content fails closed. Omitted optional fields on update mean leave unchanged. Read before a correction. Preserve source/evidence for every asserted value. Source checks, decisions, and timeline events are append-only; an observation is corrected only by resending its key, and keeps what it said before. A newer source or correction adds history; it does not rewrite the earlier world state. ## Data meaning - Mission: the durable buying goal. - Product: reusable identity independent of seller or offer. - Finding/option: a Mission-specific listing, contender, source, observation, or warning. - Observation: dated reality such as a listed price, a sold price, availability, or condition. - Criterion: what matters to the person. AI suggestions are proposals, not adopted facts. - Cell: one criterion × one option, carrying value and knowledge state. - Source check: where the AI looked, what happened, and any access limitation. - Decision: dated outcome and rationale, distinct from the option itself. Never combine currencies into one range. Never treat a listed price as a sold one. Use integer cents and explicit ISO currency. `verified`, `measured`, `inferred`, `estimated`, `calculated`, `conflicting`, `unknown`, and `asked` describe knowledge—not whether an option is good. ## Efficient workflow examples New research: `buying_get_brief` → `buying_search_workspace` → create/read Mission → research outside Buying → `buying_save_complete_result` → open returned `/missions/` path. Person-shaped Mission continuity: The person may already have revised the Mission question, accepted stable criteria, saved a note or source-backed Finding, and recorded a dated decision. Read that context first. Preserve accepted criteria, add evidence with provenance, leave AI-inferred criteria as proposals unless explicitly confirmed, and append a newer decision rather than replacing the person's history. An appraisal that writes prices (one call, standing in front of it): ```json { "tool": "buying_log_appraisal", "arguments": { "operationId": "log-2026-09-17-marimba-01", "appraisalKey": "estate-sale-marimba-2026-09-17", "title": "Musser student marimba", "question": "Is $450 fair for this marimba, mallets and bag included?", "answer": "Yes — at $450 with mallets and a bag it is under what these sell for used.\n\nCheck the resonators for dents before paying.", "verdict": "Good price", "confidence": "medium", "askingPrice": { "amountCents": 45000, "currency": "USD" }, "venueHint": "estate sale, Tucson, 9/17", "reasoning": "Three sold listings of this model sit at $600–$800 used, so $450 with extras is well under.", "sourcesChecked": [ { "sourceName": "eBay sold", "sourceType": "marketplace", "status": "checked", "resultCount": 3, "url": "https://www.ebay.com/sch/i.html?_nkw=musser+marimba&LH_Sold=1" }, { "sourceName": "Reverb", "sourceType": "marketplace", "status": "checked", "resultCount": 0 }, { "sourceName": "Facebook Marketplace", "sourceType": "local", "status": "requires_user" } ], "evidence": [ { "claim": "Sold for $720 used, mallets included.", "actor": "ai", "basis": "sold", "sourceUrl": "https://www.ebay.com/itm/1", "observedAt": 1788000000000 }, { "claim": "One sold for $380 with a cracked bar.", "actor": "ai", "basis": "sold", "disagrees": true } ], "prices": [ { "kind": "sold", "amountCents": 72000, "state": "verified", "scenario": "used, with mallets", "learnedBy": "receipt", "channel": "online_marketplace", "sourceUrl": "https://www.ebay.com/itm/1", "observedAt": 1788000000000 }, { "kind": "listed", "amountCents": 45000, "state": "verified", "scenario": "used, with mallets and bag", "learnedBy": "tag", "channel": "estate_sale" }, { "kind": "offer", "amountCents": 38000, "state": "verified", "scenario": "used, with mallets and bag", "learnedBy": "told", "channel": "estate_sale", "note": "They said they would take $380." }, { "kind": "estimate", "minCents": 60000, "maxCents": 80000, "state": "estimated", "learnedBy": "worked_out", "scenario": "used, with mallets and bag" } ], "researchNote": { "text": "Musser student marimbas, used, 2026", "detail": "What sold, what did not, and why..." } } } ``` The reply carries `findingId` (the new Item, no Mission), `itemStatusSet: true`, and each price under its key with `created`. Open `path` to see it on the Item's Appraisals tab. Incremental correction: `buying_get_mission_context` → `buying_save_option` or `buying_save_comparison` with stable keys → read again if the server reports a version conflict. Queue handoff: `buying_list_capture_queue` → claim with `buying_update_capture_queue_item` → batch-fetch images if present → use the returned `missionContext` to read that exact Mission → research → `buying_save_complete_result` for that exact Mission with Queue claim/version. Use `needs_you` only for one exact user action that truly blocks progress. ## Photo-copy and fetch reliability — September 29, 2026 AI photo reads use a separately budgeted stored copy no larger than 1 MiB; oversized page previews are not reused. Repair checks existing copies before marking them ready. Image-URL fetches validate every resolved destination and pin an approved public address while preserving HTTPS hostname verification; redirects remain refused. Metadata extraction failures do not fail the upload, and remain eligible for retry. Replays and links to existing photos include the stored capture-time and location fields, including replayed batch slots. ## Photos keep their location and capture time — September 24, 2026 The person's decision (2026-09-23): photos keep their GPS. They photograph things at sales, bin stores and thrift stores, and where and when a photo was taken is how you file it under the right Place and visit. - NOTHING IS STRIPPED. Every `buying_attach_photo` mode (inline, begin_upload/finish_upload, begin_batch/finish_batch, fetch_url, and `photoUrls` through it), every photo the person uploads from their phone, and every research picture is stored exactly as sent, byte for byte, EXIF included. `buying_get_capture_image(s)` returns the stored bytes as they are. - WHERE AND WHEN, READ FOR YOU. The server reads the photo's EXIF (it never changes it) and returns, wherever you read photos: `takenAt` (epoch ms), `takenAtLocal` (the camera's wall clock, e.g. `2026-09-22T14:03:11`), `takenAtOffset` (e.g. `-07:00`), `takenAtPrecision` (`exact` when the camera wrote its zone or a GPS time, `local_clock` when it did not — fine for ordering one day's photos, not for comparing zones) and `location` `{ lat, lng, altitudeM?, accuracyM? }`. Each is null when the photo did not say (a screenshot, an export without metadata, a photo an AI attached before this date). - WHERE YOU SEE THEM: every attach reply, including replays and `link_existing`, and each `finish_batch` slot; `buying_get_capture_image` and each `buying_get_capture_images` file; each Queue row's `files[]` from `buying_list_capture_queue`; and the new `pictures` list (main image first, bounded by `pictureWindow`) on `buying_get_item`, `buying_get_type` and `buying_get_place`. - USE IT: group a photo dump by `takenAt` into visits, match each visit's `location` to the nearest of the person's saved Places (`placeMatch` names it when a Place has a point; otherwise their Places carry addresses; ask when two are close or none is), and say which Place and time you filed each photo under. - PRIVATE. The location is readable only inside the person's workspace — by them on the picture and ask pages, and by their own connected AI. Never put coordinates in a public location contribution, a share, or anything the person has not asked you to publish. A share page only ever serves a copy made without metadata. ## A Place's standing facts, and saying what you could look up — September 21, 2026 From one sale day on a phone, at a Goodwill and a bin store. **A FACT THAT STAYS TRUE ABOUT A PLACE IS A CONTEXT ENTRY ON THE PLACE.** A weekday price ladder, restock days, a standing discount, how the store works: `buying_save_note` with `kind: "context"` and `targetKind: "place"`, ONE ENTRY PER FACT, the schedule itself in `detail`. The worked example: "Weekday price ladder: Mon $5, Tue $3, Wed $1" is one entry and "Restocks Friday and Saturday" is another. A dated thing you saw on one visit is an observation instead (`buying_add_place_observation`). It is read where the question is asked. An Item's `buying_get_item` reply carries its Place's entries under `venue` — up to eight, newest first, with `venue.contextWindow` — and a `buying_log_appraisal` that named a Place carries the same block. That is what answers "buy it now, or come back tomorrow for $3?" without a second call nobody makes at a sale. The whole list is on the Place: `buying_get_place`, and the Place's Context tab. The Item's own Context tab shows a short "About the Place" block from the same entries. **SAY WHETHER YOU COULD READ THE SOURCE YOURSELF.** If your client cannot browse the web at all, or can only when a setting is turned on, say that plainly and name the setting if you know it. When a site blocks automated reads — Amazon does — name the site, say what you used instead and how far to trust it, and say what the person can check themselves (the store's own app, a desktop browser). Never present a third-party tracker's number as the store's price. It is on `buying_save_prices`, `buying_record_list_price`, `buying_log_appraisal`, `buying_save_price_sheet`, `buying_save_item` and `buying_capture`, and in the standing brief. **THREE SHAPES THAT USED TO REFUSE A CORRECT CALL.** - `buying_log_appraisal` takes `condition` — the same field and the same words `buying_save_item` and `buying_capture` take — and it lands on the ITEM the appraisal creates or updates. The reply answers `conditionStored`. - A Type's `priceRange.basis` takes `list_price` and stores it as `listed`; the reply's `derived[]` says so. The four words are `listed`, `comparable`, `sold`, `estimate`. A Type saved before 2026-09-18 may still READ BACK the retired `observed` or `asking`. - `parentTypeName: null` means NO PARENT on `buying_save_type` and `buying_save_types_batch` — a top-level Type, the same as leaving the field out. It is not a clear: removing a parent a Type already has is still `clearParent: true`. **AND A PLACE SAVED WITH HOURS IS NO LONGER TOLD IT HAS NONE.** `buying_create_place` and `buying_import_sale` reported "No hours on this Place" in the same reply that had just written them. Fixed. ## The day and the store — September 24, 2026 From a full day at three bin stores (Golden Ticket, 805 Liquidation, Fun & Find) with no Trip open, a $1 Thursday at a store whose notes said closed, and no day totals at the end. **TODAY'S TRIP IS MADE FOR YOU.** When something is saved at a PHYSICAL Place — an Item (`buying_save_item`, `buying_save_items_batch`, `buying_capture`), a receipt line (`buying_save_purchases_batch`), a visit (`buying_record_visit`), or an ask the person captured there — it goes on today's Trip. If no open Trip is dated today (in the Place's timezone, else the person's; the day turns over at 4am), one is started, named from the Places' kinds ("Thu Sep 24 · bin stores"), with that Place as its first stop; every later Place becomes the next stop, in order. The reply says so in `tripStop` and `derived`. Never for an online Place, a sale not running today, or something saved for another day; never when today's Trip has been closed or archived; a stop the person archived is never put back. The one exception is the late write-up rule: while nothing is dated today, a Place that is already a stop on yesterday's or tomorrow's open Trip is filed there. A receipt line at a Place with no `tripId` joins the day's Trip the same way (a receipt for an earlier day joins a Trip dated that day that stops there, and starts nothing). **`buying_get_trip` ADDS THE DAY UP** under `day`, per stop and in total: `spendCents`, `pieces`, `lines`, `unpriced` (price-pending lines, flagged, never added as $0), `voided` (left out of every figure), companion lines (someone with the person paid — not their spending), `bundles`, and estimated retail (each Item's newest list price, else estimate, times quantity) against `paidAgainstRetailCents` for those same lines, plus a one-line `sentence`. A bundle line counts its allocated share, so a bundle adds up to what was paid. The Trip page shows the same card first on its Overview. **A BIN STORE'S WEEK IS DATA ON THE PLACE.** Send `priceSchedule` on `buying_create_place` or `buying_add_place_observation`: `days` weekday → cents or `"closed"` (`{mon: 500, tue: 300, wed: 100, thu: "closed"}`), `restockDays`, `confirmedBy` (`person`, `ai_research` — the default for you — or `observed`). Only on a Place of the person's own; a shared directory Place is refused — use it in theirs first. Keep a context entry for anything the schedule cannot hold. **"TODAY IT'S $1."** `todayPrice: {priceCents}` or `{closed: true}` on `buying_add_place_observation` records the day's price seen in the store, on that note. When it disagrees with the schedule the schedule is marked unconfirmed and the reply carries `contradiction` in words ("Thursday was $1 on Sep 24; the schedule says closed"); when it agrees the schedule is confirmed as `observed`. The person has the same one tap on the Place page and above a Queue ask tied to the Place. **TODAY'S PRICE, WHEREVER THE PLACE IS READ.** `buying_get_place` returns `priceSchedule` and `today`, and `buying_get_item` returns `venue.today`: `{day, weekday, priceCents or closed, confidence, source, confirmedBy, restockToday, sentence}`. `confidence` is `high` (seen today), `medium` (a confirmed schedule) or `low` (contradicted, or unconfirmed for sixty days); `source` is `observed_today` or `schedule`; `today` is null when nothing says. A NEW Item saved at a Place with a price today, and no price of its own in the call, gets it as a `listed` row with `sourceLabel` "from schedule" — the reply says `schedulePrice`. A tag you send always wins. **SOURCES, FINISHED.** The Sources block on an Item and a Type now shows each link's newest recorded price beside its site name ("amazon.com · $18.99"), Amazon first, then the maker, then others. An Item's own `sourceUrl` joins it, and so does a barcode's Amazon link on one of its photos when the photo carries one; a Type gathers its newest Items' own links too. ## Queue titles, and which Place it is — September 22, 2026 From a day at three bin stores in Oxnard and Ventura. **EVERY QUEUE ASK YOU FILE, CLAIM OR FINISH GETS A SHORT TITLE.** Asks the person typed read as short questions on their Queue; asks an AI wrote down showed their whole first line, because `buying_queue_for_later` could not carry a title. It can now: send `title` on every ask — a few words naming the question, "Is the Marantz 2230 worth $120?". It is credited to your AI ("titled by your AI") and sits beside the ask, whose words are never rewritten. A filed ask with no title comes back with `titleMissing`. `buying_update_capture_queue_item` takes `title` too: send it when you claim or complete an ask that has none. Its reply carries `title`, `titleBy` and `titleOutcome` — `written`, or `kept_person_title` when the person wrote one, which is never replaced. `buying_list_capture_queue` shows `title` and `titleBy` on every ask; `titleBy: null` means nobody has titled it yet. The older untitled asks, the ones an AI wrote down included, are listed by `buying_list_records_needing_titles` with `kinds: ["queue"]` and `missing: "title"`; title them with `buying_save_titles_and_recaps` (`{ kind: "queue", id, title }`). **NEVER SILENTLY OVERWRITE A PLACE'S ADDRESS.** An address that changed, one you could not confirm, or two that disagree — an Instagram page naming one town while the person stands in the store in another, a store that moved — is a context entry on the Place (`buying_save_note`, `kind: "context"`, `targetKind: "place"`), each address with where it came from: "Address unconfirmed: Instagram says Oxnard; the person was at the Ventura store on Sep 22". The Place's Overview shows its context entries at the top, under "Know before you go", so the person reads them on the way in. **WHETHER A PLACE IS GENERAL OR ONE LOCATION IS THE PERSON'S CHOICE.** "Costco" can be one Place, or each store its own. Bin stores differ by location — the price ladder, the restock days — so each is usually its own Place. When it is unclear, follow how the person already names their Places (`buying_list_places`), or ask. This is on `buying_create_place`, `buying_add_place_observation` and `buying_save_place_description`, and in the standing brief. ## When Buying itself is wrong — `buying_report_issue` WHEN A TOOL HERE IS BROKEN, SAY SO. You are the first to know — you are the one holding the refusal — and until 2026-09-18 there was no way for you to tell anybody. The only route from "the AI found a bug" to "somebody fixed it" was the person remembering it that evening, and by then they had lost whatever the bug cost them. `buying_report_issue` files ONE issue with the people who run Assist With Buying. IT DOES NOT REACH THE PERSON YOU ARE WORKING WITH. Nothing appears in their workspace, no record of theirs changes, and it is NOT a Suggestion — a Suggestion is an idea you raise WITH them, shown on their screen and accepted only by them. This is a bug report about the site, to the site's owner. Do not tell them you filed one unless they asked; carry on with whatever still works. WHERE IT ACTUALLY GOES, EXACTLY (2026-09-21): one row in this site's own `siteIssues` table, deduplicated by title, read on ONE operator page, `/admin/issues`, by the people who run Assist With Buying. It reaches nothing outside the site — no email, no issue tracker, no third party — and the project's tracker is updated by hand from that page. FILE ONE WHEN: a call is refused for something you sent correctly; a refusal does not say which field is wrong; a description names a value the schema then rejects; a save reports success and the record does not show it; two tools disagree about the same field's name or meaning; or you had to guess more than twice to make one call land. DO NOT FILE ONE FOR: a permission the person has not granted (that is `buying_manage_connection`), a record that genuinely does not exist, or your own mistake once you have understood it. A REQUEST TO CHANGE THE SITE GOES HERE TOO (2026-10-14), with `kind: "request"`: when the person asks for a feature, a fix or a docs change to Buying itself, or you need one to do the work. It lands in Admin → Issues marked Request. It is NOT Queue work — `buying_queue_for_later` is for work an AI can do through these tools. For a request, `title` is the change in one sentence, `toolName` the tool it concerns (or `"site"`), and `severity` may be left out. `kind` defaults to `"bug"`, and a bug needs `severity`. - `title` is the one sentence an operator reads first, AND it is how duplicates merge, so write the DEFECT rather than the occasion. Good: "buying_save_trip_stop returns INTERNAL_ERROR when coordinateSource is missing". Bad: "error while saving a stop". The same title from a hundred connections is one issue with a count of a hundred. - `severity` is `blocks_work` (you gave up), `wrong_result` (it stored the wrong thing, or said it stored something it did not — the worst kind to leave unreported, because nobody finds out later), `confusing` (it worked after you guessed) or `papercut`. - `toolName` is the exact catalogue name. - `request` is the call that failed, pasted as you sent it. SEND IT — it is the most useful thing in the report. Anything in it that looks like a token, key, password or Authorization header is stripped by the server before anything is stored, and the reply says how many values were removed. You do not need to edit it yourself, and you must not leave the arguments out to be safe. - `expected` is what you thought would happen; `suggestedFix` is what you think would fix it. An opinion is stored as one, and a good one saves somebody an hour. ## Failure recovery - Missing/stale tools: disconnect and reconnect the connector. Every refusal is JSON `{code, message, recovery, retryable, field?, reference?, requestId}` — `requestId` is one id per HTTP request, also sent as the `x-request-id` header and logged with the error; quote it in a report, and an `INTERNAL_ERROR`'s `reference` is the same id. Advice to resend "with the SAME `operationId`" appears only on a tool whose input takes one; a read is simply sent again. And the same failure has the same code on a whole call, a batch row, a photo-URL row and a refused `buying_queue_for_later` ask. Branch on the code first: - `INVALID_INPUT`: your input, deliberately classified. The refusal names the `field` when there is one (a tool-schema failure lists every key it could not accept), and carries `retryable: false`. Fix what it names and send the call again with the SAME `operationId`. Retrying it unchanged cannot succeed. - `REFUSED`: the server's own check refused the call, in its own words, without classifying it. Read the `message`: if it names something you sent, change that and resend with the SAME `operationId`; if you sent it correctly, report it with `buying_report_issue`. `retryable: false`. - `INTERNAL_ERROR`: NOT your input; do not rewrite it. The `message` is generic and the raw error is kept in the server log under `reference`. `retryable: true` (a crash, a write conflict or a timeout): send the same call once more with the SAME `operationId`; if it fails again, stop and report it with `buying_report_issue`, quoting the `reference`. `retryable: false` (a per-call limit): ask for less at once (smaller limit, fewer rows, follow the cursor) and report it. - BATCHES (`buying_save_items_batch`, `buying_save_types_batch`, `buying_save_prices`, `buying_capture`, `buying_queue_for_later`): a SERVER FAULT in any row aborts the whole call. Nothing is saved and no receipt is recorded, so the call's `INTERNAL_ERROR` means: resend the same call, unchanged, with the SAME `operationId`. Only a DELIBERATE refusal stays a row, with its `code`, `retryable: false` and `recovery`; the rest of the call saved and its reply is recorded under the `operationId`, so resend that row alone, corrected, in a new call with a NEW `operationId`. - A REFUSED SUB-ROW UNDER A SAVED PARENT (a price under an Item, a capture's outcome or purchase) carries the parent's `findingId`. Resending the parent only replays it, so send the sub-row on its own: a price with `buying_save_prices` (a row with that `findingId`); a capture outcome as one `buying_capture` row with its key, title and that `findingId` and no prices or photoUrls, under a NEW `operationId` (a row with `findingId` is applied, never replayed or duplicated); a purchase with `buying_save_purchases_batch`. Each refusal's `recovery` names the call. A refusal checked before anything is written says so; one that cannot pass, such as a full status history (`STATUS_HISTORY_FULL`), says not to resend. - PHOTO URLS are fetched after the record saved, so a failed one stays on its row, with `retryable` and any `reference`. The default retry is the call that carried `photoUrls`, resent unchanged with the SAME `operationId`: the record replays and each picture is tried again by its place, never stored twice. A different address for an Item goes through `buying_attach_photo`. A `buying_import_sale` listing photo is retried ONLY by its sale call (it is the one that links the photo to the listing); one that STORED but did not link carries `stored: true` and its `fileId`. - "No approval received" from your own client: nothing reached this server, so there is nothing here to retry or reconcile. Use `buying_capture` to need fewer approvals, and ask the person to set Buying's write tools to Always allow. - `VERSION_CONFLICT`: read the Mission again, merge, and retry with the new version. - `IDEMPOTENCY_CONFLICT`: inspect the saved record or use a new stable operation key only if the semantic work is genuinely different. - `QUEUE_CONFLICT`: the ask's version, claim or state does not allow this transition (a stale `expectedVersion`, an expired or different claim, an ask waiting on the person). List Queue again and use the current version/active claim. A missing field on a Queue call, such as `requiredAction` for `request_user`, is `INVALID_INPUT`, not this. - `NOT_FOUND`: search/read and use a returned id; cross-workspace ids intentionally look missing. - Blocked source: record `blocked` or `requires_user`; do not loop the same request. - Large request: split incremental saves. MCP requests are capped at 256 KiB. ## Current / Partial / Later Current in this source release and isolated synthetic proof: - product-owned grants enforced on every call at call time, a caller-invariant tool catalogue with per-call `insufficient_scope` challenges, immediate revocation, per-client binding with a person-controlled release path, and a verbatim consent snapshot; - a scripted nine-step lifecycle harness that re-proves the server's behaviour on every deployment: anonymous challenge, catalogue listing, bounded read, idempotent write, cross-owner refusal, malformed/oversized/batched/replayed handling, revoke denial, reconnect, and marked synthetic cleanup proved by re-query; - exact-audience JWT verification, Clerk audience-less scoped-access JWT compatibility, issuer-verified opaque-token compatibility, and protected-resource metadata; - stateless modern and older-client tool discovery; - exact 96-tool catalog; - server-derived workspace identity and cross-workspace denial; - bounded Mission reads/search; - replay-safe Mission/product/option/comparison/source/decision writes; - append-only observation/source/decision history; - an append-only authority ledger recording every issuance, request, approval, widening, narrowing, and revocation with the revision before and after; - starter-policy provenance on each grant, so a connection keeps the policy version it was issued under; - atomic complete-result + Queue closeout; - private inline image tools; - AI photo attach under the `media.write` permission (default-tier since 2026-08-29), with store-first-then-commit ordering, byte-level verification of the stored object, the bytes kept exactly as sent with their EXIF location and capture time read onto the file (private), and `operationId` replay; - a per-connection record of every AI media read and attach, shown to the person at https://assistwithbuying.com/connect as "what your AI looked at"; - normal Mission/Finding/Queue web reads of saved AI changes, including the latest decision on the Mission Board and recent bounded source/decision history on Timeline. The desk loads at most 20 recent decisions and 50 recent source checks; append-only older records remain stored. - an ordinary signed-in person can create a typed Mission with a title and useful outcome; - the Mission desk lets that person revise the question, add or review stable criteria, keep a personal note or source-backed Finding, and append a bounded decision whose earlier versions remain in Timeline; - the Mission desk can create and list Mission-scoped Queue handoffs, and its timeline records both the person's handoff and the chosen AI's saved completion; The dated production acceptances below all predate the grant boundary. They remain true about the Mission, Queue, save, render, and cleanup lifecycle, and they are history rather than a current compatibility claim for any client. Dated production acceptance on 2026-08-12 proved Codex browser OAuth, empty-workspace reads, a source-backed complete-result save, granular corrections, normal Mission UI rendering, exact marked-fixture cleanup, and empty post-cleanup MCP/UI reads. A second dated acceptance on 2026-08-13 proved the joined v0.10 path: normal signed-in Mission and Queue creation, real Codex claim and Mission read, one atomic three-option/18-cell/five- source/decision save, Done reflection in Board/Findings/Timeline/Queue, exact graph inspection and cleanup, absent post-cleanup UI/read, and removal of both temporary OAuth client registrations. The retained non-privileged test identity remains; its workspace is empty. A third dated acceptance on 2026-08-13 proved the v0.11.0 return path: an isolated OAuth MCP client read a person-created marked Mission, claimed its scoped Queue handoff, and atomically saved two illustrative Findings, four criteria/eight cells, two public source checks, a `needs_more_research` decision, and Done. The normal Board rendered the decision, rationale, uncertainty, AI/client provenance, and evidence-history link; Findings, Timeline, and Queue rendered the same result. Exact application-data cleanup returned the retained workspace to Active 0 / Queue 0. A fourth dated acceptance on 2026-08-14 proved the deployed v0.12.0 person-side backend lifecycle: one marked Mission question, accepted criterion, sourced Finding/source check, bounded decision, and Mission Queue handoff all reflected through normal Mission reads, then exact cleanup returned the retained workspace to Active 0 / Queue 0. The supported browser harness could not control Clerk's long-lived impersonation tab, so live authenticated rendering of the new owner controls remains Partial rather than inferred. Partial or environment-dependent: - client compatibility. The lifecycle harness proves the server's side of the contract; no AI client's own environment — registration, consent screen, tool refresh, platform limits — has been proved under the grant boundary, and none is named as supported; - authenticated rendered observation of the owner controls; - broader person-side comparison-cell editing, status controls, deletion, contact handling, and legacy-data migration remain unshipped; - every other client/environment still requires its own live OAuth, reconnect/tool-refresh, authenticated function, and UI compatibility proof; - provider-side token revocation. Buying's own grant revocation is proved and immediate; revoking the underlying Clerk credential is a separate mechanism and was not re-proved; - Client ID Metadata Documents. Buying validates and records a client's registration path as the resource server, and prefers the metadata-document path, but the authorization server performs registration and redirect binding; whether that path is offered is a provider configuration, not a code path here; - old interest/dossier/session rows and tools remain code/data compatibility material but are not the current remote MCP catalog; public-location tools remain supported; - seller contacts remain stored privately where present but have no current reveal tool. Later: - contact reveal; - seller messaging, offers, reservations, orders, payments, or purchases; - production-data migration into Mission records; - broader collaboration/publication and destructive account/data operations. ## Shopping Trips — September 5, 2026 Trips retain plans, stops, source-backed research, original Facebook listing links, photos and actual visit notes. A Trip keeps the id it already had; dated list sublists remain separate and can be linked. Trips never require a List or Mission. - buying_list_trips: cursor pages of private trips, including completed and archived outings. - buying_get_trip: overview plus a page of stops, or section entries for dated research, or section decisions for the decisions log; also timeLimits, visitLengths and needsVerifying. Follow continueCursor until isDone before claiming a complete plan or journal. - buying_save_trip: stable key for creation; tripId + expectedVersion for correction. YYYY-MM-DD dates retain local day; timezone, area, purpose, linked listIds/missionIds and the day's map links (maps, main first; mapUrl is the main one) are optional. clearFields can remove mistaken date/endDate. - buying_save_trip_stop: save one candidate with original sources, checked dates/status, description, categories, address precision, sale dates/hours, priority/order, and separate recommendation. Stable key and per-trip source identity prevent duplicate creates; updates take stopId and expectedVersion from a fresh read, and name may be left out on an update. To record a choice the person just told you, send only tripId, stopId, selection and userInstruction: no read first (2026-10-16). selection (unreviewed/interested/likely/going/pass) and selectionReason record the person's choice; AI changes require their explicit userInstruction. Never invent user intent from your recommendation. outcome (not_visited/visited/skipped) and actual visitedDate stay separate. Possibility fields: saleType, freshness, fitNote, listedLocation; listing check: verifiedStatus, verifiedNote, verifiedAt; plan: plannedArriveAt, plannedLeaveBy, skipRule; at the sale: arrivedAt, leftAt (epoch ms or "now"). Optional links include venueId, visitIds, findingIds, appraisalIds and listItemIds. clearFields removes incorrect coordinates/date/place; clearing any coordinate component clears the whole coordinate set. - buying_save_trip_stops: up to 30 stops on one Trip in one all-or-nothing call, in order, so a community sale listed first can be named by its houses with partOfKey. - buying_add_trip_entry: append note, seller_description, photo_insight, visit, correction, map evidence or a decision with stable key, sourceUrl, date and optional ready private fileIds. Corrections use a new key and correctsEntryId. Earlier stop versions remain in the journal. Read tools need mission.read and write tools mission.write. All writes require stable operationId; retries replay and conflicting payloads refuse. The owner can use /trips on phone or desktop, compare cards/table, select likely stops, record notes/photos, archive and reopen. Export stops for your AI retrieves every stop page for external mapping; the account portable export includes full Trip/Stop/Entry metadata (media bytes remain separate). Your AI performs Facebook and other research using its own authorized access; Buying does not browse or message sellers. Save direct original Facebook URLs, source-check dates, address precision and uncertainties. Retain description evidence separately from photo interpretations. Dates do not imply current availability; a good but distant sale can be passed over for one trip without becoming a permanent exclusion. Never infer exact coordinates from an approximate Facebook map. buying_attach_photo now also accepts target trip_stop with the stable stop ID. The existing begin_upload → first-party PUT → finish_upload path attaches a private photo and creates a journal entry, retaining caption and AI provenance. Existing media grant requirements and per-target photo limits still apply; use separate journal entries to link already uploaded files. Owner fallback: Trips accepts a research JSON file or pasted JSON with a stops array (up to 30 stops and 20 entries per stop). Each stop uses buying_save_trip_stop fields without tripId, plus an optional entries array using buying_add_trip_entry fields without tripId/stopId. Keep stable stop and entry keys. Import attributes research as AI and requires userInstruction for explicit user choices. The same ownership, source-reuse and validation rules apply; photos use the separate private upload flow. Store Check respects the connection's live selected Places before picking or claiming a batch, reading its photos or notes, and saving results. Excluded-Place batches are withheld entirely, including image-read calls; narrowing after a claim also denies later reads and saves. Queue listing and activity also withhold excluded-Place Store batches. Under selected Places, Queue-wide counts are withheld: queueCounts and waitingOnPerson are null and countsWithheld is true. ## Personal rules at the first call (2026-10-23) `buying_get_brief` starts with `notesForYou`: up to six active, AI-readable standing Preferences, keyed first then newest. Each carries {id, text, savedBy}, with optional key/scope when present. Read and follow notesForYou at the start of every conversation: these are the person's private notes for their AI. What they say in this conversation overrides saved guidance. `notesForYouInfo` states shown, withheld and omitted counts; profile kinds remain in lookingFor. A connection that withholds preferences receives no personal notes. The brief has six summary rules; `buying_get_preferences` returns the full work-style playbook, preference history, keys and suggestedDefaults on demand. `buying_save_preference` accepts optional `preferenceKey` (lowercase letter then letters, digits or underscores, at most 64 characters). For the account holder's kind preference only, reusing a key revises the same row even after a title change. To key an existing unkeyed Preference, also send its preferenceId. A key cannot be reassigned or used to revive an archived row. Only-me rows stay private. Calls without a key retain existing behavior. Save only when the person explicitly asks, with theirWords; inferred rules use buying_propose_preference and wait. Trip stop field patches (#300 item 5): buying_save_trip_stop accepts baseValues instead of expectedVersion for a safe field-level merge. Send stopId plus the values from your read for every patched field (null means absent); related coordinate, date, time, verification and arrival/visit fields must be checked together. Omitted name is preserved. Conflicts name the fields to re-read; without a version or baseline the patch is refused. Existing expectedVersion and explicit person-selection saves keep their behavior. Batch import outcomes: buying_import_sales rows have status complete, partial or failed. A partial row keeps ok:true because some records were committed; its problems list says what did not finish. summary.partial counts these separately from fully succeeded rows and failed rows. Keep the returned IDs, fix only the reported problems, and use a fresh operationId for changed input. An unchanged retry replays the original partial receipt rather than duplicating its Places. Prior sightings: `buying_find_prior_sightings` takes photo `fileId` and/or `text`, optional `placeId`, `booth`, `missionId`, `limit` (1–20), `cursor`. Requires mission.read and media.read for a photo. Returns earlier Items and standalone observations, first/last evidence dates, daysOnShelf (elapsed evidence interval only, never continuous availability), dated prices/tags, booth, confidence and whyMatched. Follow pageInfo.continueCursor with unchanged inputs, including after empty pages. Phases search Item titles, older stored photo hashes, then standalone observation text. Histories are bounded and state caps. A matching photo proves photograph reuse, not object identity or a second visit. Missing hashes, unfiled photos, unknown dates/booths and grant/Mission/Place/media filters limit recall; no prior sightings is not proof of absence. No site AI, paid embeddings, writes or external photo reads. Purchase lines (2026-10-24): buying_save_purchases_batch lines (and buying_capture purchase lines) take fees [{label, cents}] (labelled extras such as a card surcharge, kept beside totalCents and never folded into it; [] clears) and paymentLabel ("Visa 8325", "cash": a label only, at most the last 4 digits of a card, a longer run of digits is refused; null clears); the call takes paymentLabel and missionId as defaults for every line, and a line may name its own missionId. A Mission the connection cannot reach is refused (OUT_OF_MISSION_SCOPE). A line naming an Item and no typeId is counted under the Item's Type. buying_list_purchases rows carry fees, feesTotalCents, paymentLabel and missionId (null when the connection cannot see that Mission). The "paid" price an Item shows is still built from its purchase lines when read, so nothing is counted twice. Antique malls and booths (2026-10-24): Place kinds antique_mall (many dealers' booths under one roof) and booth (one dealer's booth). buying_create_place with kind booth and parentPlaceId set to an antique_mall Place of this workspace puts the booth in its mall (parent.relation mall); only a booth sits in a mall (PARENT_NOT_ALLOWED otherwise). buying_get_place returns mall {venueId, name, path} on a booth and booths {rows, capped} (newest 50) on a mall. A photo attached to a Place never replaces the picture it leads with unless makeMain is true: with no main picture chosen yet, the one the page was showing is kept as the main picture. Linking a Place of the person's own to the shared directory (2026-10-25): when the person made a Place themselves and the directory has the same Place, buying_link_place {operationId, venueId (theirs), publicVenueId (the directory's), replace?, dryRun?} points their row at the directory entry. It sets one field, publicVenueId, on their row: notes, visits, prices, Items, Missions, booths, photos and their description all stay, and nothing is written to the shared row. A name or address that does not match comes back as warnings, because picking both rows is the confirmation. Already linked there: nothing written, alreadyLinked true. Linked elsewhere: refused ALREADY_LINKED_ELSEWHERE unless replace is true. Another of their Places already on that entry: refused DUPLICATE_PLACE, naming it. dryRun returns before, after, counts and warnings and writes nothing. buying_unlink_place {operationId, venueId, dryRun?} clears the link the same way. Use buying_use_public_place instead when they have no row for the Place yet.