shop · git:20261103.8a23fb3 · 2026-11-03 · sha256 603a1de999dcd951

shop git:20261103.8a23fb3A

Immutable. This exact content never changes and is served at /api/v1/blob/603a1de999dcd951.

---
name: shop
description: >
  Multi-store shopping CLI. Search products, check prices, compare items, read reviews, manage
  carts, place orders, and track packages via the `shop` CLI. Use when asked to buy something, find a product,
  look up prices, add to cart, checkout, order, search for products, compare products, check
  availability, look at reviews, find deals, check a shipment, or save package tracking numbers. Currently supports
  Amazon (US/UK/DE/JP/CA/AU) and Facebook Marketplace (anonymous search and listing details).
---

# shop

Data commands default to JSON on stdout, with JSON errors on stderr. Pipe with `jq`.
Use `--json` for machine parsing, including when `SHOP_OUTPUT=text` is set. `--text` is a human-readable display mode, not
a stable parsing interface; it also formats errors as text without changing exit codes.

**All prices are cents** (minor units). $29.99 = 2999. JPY = whole yen.

## Auth

Auth is per-store. Amazon uses a device code flow — two calls to `shop login`:

```bash
# 1. Start — returns challenge URL + code for user to enter
shop login amazon
# → .challenge.url and .challenge.code — user must visit URL and enter code

# 2. Complete — run same command again after user authorizes
shop login amazon
# → .authenticated = true
```

```bash
shop whoami -s amazon                    # check auth state
shop logout amazon                       # revoke + clear tokens
```

Auth persists in `~/.config/shop/auth/`. Store operations may require auth. Shipment tracking and its local shipment collection do not.
On `auth_required` (exit 10) or `auth_expired` (exit 11), re-run login flow.

## Global Flags

```
-s, --store <name>   # target store (default: config or $SHOP_STORE)
--json               # force compact JSON
--pretty             # force pretty JSON
--text               # human-readable output; incompatible with --json/--pretty
--timeout <dur>      # request timeout (default 30s)
--config <path>      # config and local state directory
```

Set a default store: `shop config set defaults.store amazon`

`SHOP_OUTPUT=text` selects human-readable output by default; `json` and `pretty`
are also supported. Explicit output flags override the environment, which overrides
saved JSON formatting preferences.

## Commands

### Search

```bash
shop search "protein powder"
shop search "usb-c cable" --sort price_low --page 2
shop search "headphones" --min-price 2000 --max-price 10000 --min-rating 4.0
shop search "desk" --store facebook --filter latitude=30.2677 --filter longitude=-97.7475 --filter radius=15mi
```

Flags: `--sort` (relevance|price_low|price_high|rating|newest|best_seller), `--page`, `--page-size`, `--min-price`, `--max-price` (cents), `--min-rating`, `--category`, `--filter key=value` (repeatable)

Facebook Marketplace: `--store facebook --filter latitude=<latitude> --filter longitude=<longitude>`. Both coordinates are required; `city` is no longer supported. Search is stateless and uses one GraphQL request per native batch, with no HTML bootstrap, JavaScript discovery, or provider caches. Optional `--filter radius=` (miles, or `km` suffix). Use `--sort newest` for the newest local-pickup feed (shipping is off unless `--filter shipping=true`) and `--filter days_since_listed=N` to limit listing age to 1–365 days. Search uses native Marketplace GraphQL with explicit city coordinates, radius, sort, and listing-age variables. Search results include `attributes.location`, Facebook's Unix `attributes.creationTime` when available, with no independent distance verification. Radius is requested upstream; Facebook may include outside-radius recommendations. No `attributes.distanceKm` is supplied. `--page` follows Facebook's native cursor batches using `loadNext(24)`. `--page-size` caps a page locally without changing cursor boundaries, and `hasMore` mirrors Facebook's next-cursor state. `--min-price` / `--max-price` work. No cart or checkout.

Response: `.products[]` has id, title, price, rating, url, badge, availability. `.hasMore` for pagination.

### Product Details

```bash
shop product B0D1XD1ZV3
```

Amazon product IDs are ASINs. Facebook Marketplace IDs are numeric listing IDs or `https://www.facebook.com/marketplace/item/<id>/` URLs. Returns title, brand, price, listPrice, rating, images, specs, features, description, seller, availability, variantInfo.

### Variants

Use when a product has multiple options (colors, sizes, styles) and you need to show them or let the user pick one.

`shop product` tells you which variant you're currently looking at via `variantInfo.selected` (e.g. `{"Color": "Black"}`). To get the full option tree and all other ASINs, call `shop variants`.

```bash
shop variants B0D1XD1ZV3
```

Response:
- `.dimensions[]` — each axis of variation (Color, Size, etc.) with all available `.options[].value` strings and optional `.options[].imageUrl` swatches
- `.combinations[]` — every variant as `{ productId, values: {"Color": "Black"}, price, available }`

```bash
# Show all color options + their ASINs
shop variants B0C33XXS56 | jq '[.combinations[] | {asin: .productId, color: .values.Color, price: (.price.amount / 100)}]'
# → [{"asin":"B0C33XXS56","color":"Black","price":208.99}, ...]

# Get ASIN for a specific color
shop variants B0C33XXS56 | jq -r '.combinations[] | select(.values.Color == "Silver") | .productId'
```

If the user asks about a product with variants (e.g. "does it come in white?", "show me all sizes"), call `shop variants` and present the options cleanly — don't just show the ASIN they happened to land on.

### Reviews

```bash
shop reviews B0D1XD1ZV3
shop reviews B0D1XD1ZV3 --sort recent --rating 5 --page 2
```

Flags: `--sort` (recent|helpful for Amazon), `--rating` (1-5), `--page` (1-100), `--page-size` (1-100, default 10).

Review bodies use the full text supplied by Amazon. Preserve `attributes.possiblyTruncated` when present; do not call that body complete. Aggregate ratings are product-wide even when filtering reviews. Use `hasMore` to decide whether to request another page. Pagination follows a live feed and later pages take additional requests; errors are not evidence that a product has no reviews.

### Offers

```bash
shop offers B0D1XD1ZV3
shop offers B0D1XD1ZV3 --condition used_good
```

Flags: `--condition` (new|used_like_new|used_good|used_fair|refurbished), `--page`, `--page-size`

### Cart

```bash
shop cart add B0D1XD1ZV3             # add 1 unit
shop cart add B0D1XD1ZV3 --qty 3     # add 3
shop cart add B0D1XD1ZV3 --offer '<offer-id>' # add a specific result from `shop offers`
shop cart view                        # view cart
shop cart remove B0D1XD1ZV3           # remove item
shop cart clear                       # empty cart
```

All cart commands return full cart snapshot: `.items[]` (product + quantity) and `.subtotal`.
`--offer` accepts a provider-neutral offer ID returned by `shop offers` for the
same product. Omit it to use the provider-selected default offer.

### Checkout (preview only)

```bash
shop checkout
shop checkout --address <id> --payment <id> --shipping <id> --coupon <code>
```

Returns checkoutId, items, subtotal, shipping, tax, discount, total, shippingAddress, paymentMethod, shippingOptions, estimatedDelivery, warnings. **Does NOT place the order.**

### Place Order

```bash
shop order place <checkout-id>
```

⚠️ **IRREVERSIBLE. Spends real money. Always confirm with the user before running.**

Fails with `cart_changed` (exit 41) if cart was modified after checkout preview.

### Shipment Tracking

Use for package status and scan history. No login or `--store` is needed.

```bash
shop track <tracking-number>
shop track <tracking-number> | jq '{trackingNumber, source, latest: .events[0], url}'
```

Supported formats are USPS, standard UPS, 12- or 15-digit FedEx numbers, GOFO US waybills (`GFUS` followed by 14 digits), and Yanwen Express waybills (`YWE` followed by 14 digits). On `not_supported`, explain that the number's format is unsupported; do not treat it as proof that the shipment does not exist. Provider failures are returned without switching sources.

Response: `trackingNumber`, `source`, `url`, `fetchedAt`, `freshness`, optional `expectedDelivery`, and `events[]` with `date`, optional `time`, `description`, and optional `location`.

To remember what a package belongs to, save it in the local shipment collection:

```bash
shop track add <tracking-number> --label "Desk equipment" \
  --merchant "Example Store" --order-id "ORDER001" --note "Office delivery"
shop track list
shop track list --all                 # Include older deliveries
shop track refresh --delivered-since 2026-01-01
shop track refresh                    # Refresh active + delivered today
shop track refresh <tracking-number>  # Refresh one saved shipment
shop track remove <tracking-number>
```

`add` returns the saved entry with `addedAt`; all attribution flags are optional. `list` returns an array of entries without network access. `remove` returns `{"removed":true}` and is idempotent. Duplicate additions return `invalid_input` and preserve the existing entry. A lookup never saves an entry automatically; local attribution is not sent upstream.

For a named saved package, run `track list --all`, match its label/merchant/order ID, then look up its `trackingNumber`. Ask which package if multiple entries match; do not guess. Do not invent attribution or overwrite an existing record by removing/re-adding it unless requested.

Report the latest scan's description, date/time, location, and tracking link. Only report an ETA if the source explicitly supplies it; distinguish an estimate from a guarantee. `fetchedAt` is lookup time, not carrier freshness. `freshness: "unknown"` means freshness is unverified. Preserve explicit timezone offsets; do not infer a timezone when one is absent or promise automatic monitoring.

#### Refresh summaries

Each shipment is stored as one JSON record in `state/shipments/<tracking-number>.json`, with attribution and a `tracking` snapshot holding the last successful lookup and its scan events. `track list` reads these records offline.

`shop track refresh` looks up selected saved shipments and returns `{total, refreshed, failed, shipments}`. Each shipment includes its saved attribution, `latest` scan, `fetchedAt`, tracking URL, `freshness`, and `refreshed` flag. Failed entries include a structured `error` and retain the previous successful snapshot when one exists; never present those as newly refreshed.

Ordinary `shop track <tracking-number>` remains a one-off lookup. Refreshing an unsaved number returns `not_found`. An empty collection returns an empty summary without network requests.

`list` and batch `refresh` default to undelivered shipments plus deliveries dated today. Use `--all` for every saved shipment or `--delivered-since YYYY-MM-DD` for an inclusive delivery-date cutoff; these flags cannot be combined. Explicit-number refreshes always run. Filtering uses the latest saved scan, not fetch time; unknown statuses/dates remain included. Dates with offsets are compared in the CLI host’s local timezone; dates without offsets retain the carrier’s calendar date. Newly discovered deliveries remain in that refresh’s output, even when dated earlier. Records are never deleted by filtering.

`expectedDelivery` is optional carrier-provided display text, supported for UPS, USPS via Stamps, FedEx, GOFO, and Yanwen when published by the source. Yanwen provides a same-day window only while in transit; an unavailable optional window does not fail the scan lookup. Keep it separate from the latest scan time; do not infer an ETA when absent or present an estimate as guaranteed. Refresh summaries include it when available.

The timeout applies to the whole batch. Per-shipment failures still produce the summary on stdout and return exit 51 with an error on stderr. When the whole batch is canceled, only attempted shipments appear in any partial summary; unattempted shipments are not presented as refreshed. A successful refresh means the source responded successfully, not that it contacted the carrier just now. No background polling is started.

### Account

```bash
shop addresses     # saved shipping addresses
shop payments      # saved payment methods
```

### Stores

```bash
shop stores                      # list all known stores
shop store info        # store details + capabilities
```

## Search → Buy Pipeline

```bash
# 1. Search
ASIN=$(shop search "thing" | jq -r '.products[0].id')

# 2. Inspect
shop product "$ASIN"

# 3. Cart
shop cart clear
shop cart add "$ASIN"

# 4. Preview
CID=$(shop checkout | jq -r '.checkoutId')

# 5. Place (CONFIRM WITH USER FIRST)
shop order place "$CID"
```

## jq Patterns

```bash
# top 5 results: title + price in dollars
shop search "query" | jq '.products[:5][] | {title, price_usd: (.price.amount / 100)}'

# cheapest result
shop search "query" --sort price_low | jq '.products[0] | {id, title, price: .price.amount}'

# check availability
shop product ASIN | jq '.availability.status'

# extract checkout total in dollars
shop checkout | jq '{total: (.total.amount / 100), currency: .total.currency}'

# list address IDs
shop addresses | jq '.[].id'

# is authenticated?
shop whoami | jq '.authenticated'
```

## Error Handling

Errors JSON on stderr: `{"code": "...", "message": "..."}`. Key exit codes:

- 10 `auth_required` / 11 `auth_expired` — re-login
- 30 `not_found` — bad ASIN / 31 `out_of_stock` — unavailable
- 40 `cart_empty` / 41 `cart_changed` — re-run checkout before placing
- 50 `rate_limited` — back off and retry; honor `details.retryAfter` when supplied
- 51 `upstream_error` — tracking history unavailable or unexpected source response; report the failure, not “invalid tracking number”
- 2 `invalid_input` — check arguments; duplicate saved shipments leave existing metadata unchanged

## Presenting Results to Users

When showing a product, don't dump JSON. Format it for humans.

**In markdown-capable surfaces (Discord, Telegram, etc.):**

```
**[Sony WF-1000XM5](https://amazon.com/dp/B0C33XXS56)** — $248.00
⭐ 4.0 (5,813 reviews) · In Stock · Prime
https://m.media-amazon.com/images/I/example.jpg
```

- Title is a markdown link to `.url`
- Price: divide cents by 100, format as `$X.XX`
- Rating: `⭐ X.X (N reviews)`
- Image: post `.imageUrl` as a bare URL on its own line — Discord and Telegram auto-embed it
- Availability + badge on one line, only if relevant

**Search results (multiple items):** 3–5 max unless asked for more. One line each — title-link + price + rating. Only pull full product details if the user wants to dig in.

```
1. **[Sony WF-1000XM5](url)** — $248.00 · ⭐ 4.0 (5,813)
2. **[Sony WF-1000XM6](url)** — $299.00 · ⭐ 4.5 (192) 🔥 Deal
3. **[Technics EAH-AZ100](url)** — $249.00 · ⭐ 4.0 (1,221)
```

**Cart:**
```
🛒 2 items · $547.00
• [Sony WF-1000XM5](url) × 1 — $248.00
• [Apple AirPods Pro](url) × 2 — $299.00
```

**Checkout preview:**
```
📦 Order Summary
[Sony WF-1000XM5](url) × 1 — $248.00
Shipping: Free (Prime, arrives Wed Apr 2)
Tax: $22.32
**Total: $270.32**
📍 John D. · 123 Main St, New York NY 10001
💳 Visa ····4242
```
Always show this and ask for explicit confirmation before running `order place`.

**Order placed:**
```
✅ Order placed — #113-4567890-1234567
[Sony WF-1000XM5](url) × 1 — $270.32
Estimated delivery: Wed, Apr 2
```

**Reviews (when asked):**
```
⭐⭐⭐⭐⭐ **Great ANC, comfortable fit** — verified purchase
> "Best earbuds I've owned. ANC is incredible for commuting..."
— JohnD · 2 days ago · 47 helpful
```
Show 2–3 reviews max by default, top helpful first.

**In plain-text surfaces:** same patterns, skip markdown links, bare URL on its own line for images.

**Fields worth surfacing:** title, price, rating, availability, badge, url, imageUrl.
**Omit by default:** listPrice (show only if discount > 10%), specs, description, features — surface on request.

## Gotchas

- Prices are **always cents**. `--min-price 2000` = $20.00. Display: divide by 100.
- `order place` is **irreversible** — real charge, no undo.
- `addresses` and `payments` require items in cart to return results (Amazon-specific quirk).
- Cart state is per-store, persisted locally. `cart clear` before starting a new purchase flow.
- Amazon only returns the buy-box offer from `offers` (single seller). Other providers may differ.
- `checkout` returns a `checkoutId` tied to cart state — if anything changes, re-checkout.
- Facebook Marketplace is read-only. Search requires paired `--filter latitude=` and `--filter longitude=`; city-slug lookup is not supported. Product details still read HTML.