# Sorsa

> Amazon FBA arbitrage toolkit: product analysis, deal feeds, price alerts and sourcing management.

- name: app.sorsa/sorsa
- version: 1.0.0
- connection class: R1 (autonomous)
- trust: 76/100 flags: no-repository
- quality: 64/100 (needs work)
- owner: not claimed
- descriptor: https://api.protogrid.dev/v1/servers/app.sorsa%2Fsorsa
- tools: https://api.protogrid.dev/v1/servers/app.sorsa%2Fsorsa/tools

**An agent can connect on its own if it already holds `X_API_KEY`, `X_API_KEY`.** Remote endpoint behind a static API key or header. The registry never sees the value; the agent fills the placeholder.

## Connection (mcpServers)

```json
{
  "mcpServers": {
    "sorsa": {
      "type": "http",
      "url": "https://sorsa.app/mcp",
      "headers": {
        "X-API-Key": "${X_API_KEY}",
        "Authorization": "${X_API_KEY}"
      }
    }
  }
}
```

Placeholders to fill: `${X_API_KEY}`, `${X_API_KEY}`.

## Trust

- hygiene: 80
- liveness: 100
- freshness: 65
- provenance: 55
- drivers: +dns-namespace +website +reachable +uptime-100% ~updated-102d-ago -no-repository
- Derived from observable signals (official registry feed and our own credential-free probes); no code audit performed.

## Quality 64/100 (needs work)

### protocol: 75 (weight 25)

- warn `protocol.modern`: newest supported version is 2025-11-25; 2026-07-28 not supported, older versions are deprecated until 2027-07-28
- n/a `protocol.stateless`: only applies to 2026-07-28 servers
- pass `protocol.transport`: streamable HTTP
- n/a `protocol.list_ttl`: only applies to 2026-07-28 servers

### authorization: n/a (weight 15)

- n/a `auth.prm`, `auth.as_metadata`, `auth.cimd`: no remote uses OAuth
- n/a `auth.secret_in_url`: no templated URL

### tool hygiene: 50 (weight 30)

- pass `tools.descriptions`: every tool has a description
- warn `tools.description_length`: 1 tools have descriptions over 1,024 characters, which crowds the context
- pass `tools.schemas`: every tool has an object input schema
- fail `tools.annotations`: no tool declares readOnlyHint or destructiveHint, so clients cannot tell safe calls from risky ones
- fail `tools.directory_hints`: no tool declares all four of readOnlyHint, destructiveHint, idempotentHint and openWorldHint; missing: get_storefront_stats (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); get_product_sellers (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); add_storefront_monitor (readOnlyHint, destructiveHint, idempotentHint, openWorldHint); …
- warn `tools.token_cost`: about 10,731 tokens to load every tool
- warn `tools.api_dump`: 70 tools, 79% named like REST operations: looks like an API mapped one endpoint per tool

### stability: 75 (weight 15)

- warn `stability.changes`: 17 tool changes in 30 days
- pass `stability.rug_pull`: no tool changed its meaning under the same name

### dependencies: n/a (weight 15)

- n/a `deps.known_vulns`, `deps.mcp_sdk_version`, `deps.resolvable`: no npm or PyPI package

- tools: 70, about 10,731 tokens to load them all
- tool set hash: 9e9343a401d19fef655fad62995a25c7
- tools last changed: 2026-10-09T12:36:40.455Z
- badge: [![protogrid quality](https://protogrid.dev/badge/app.sorsa/sorsa.svg)](https://protogrid.dev/servers/app.sorsa/sorsa)
- Checks run on what our credential-free, read-only probes observe; tools are never called and no code audit is performed.

### Tool changes

- 2026-10-09 `start_reverse_source_scan` changed (description)
  - before: Start a Reverse Source scan: for each ASIN, search Google Shopping for a cheaper source than Amazon — i.e. where to buy the product to flip it. Up to 250 ASINs. Uses the shared Keepa scan key (counts against the user's daily scan credits). Returns a job_id — poll get_reverse_source_status(job_id) for results. Premium feature.
  - after: Start a Reverse Source scan: for each ASIN, search Google Shopping for a cheaper source than Amazon — i.e. where to buy the product to flip it. Up to 250 ASINs. Uses the user's saved Keepa key if they've added one in settings; otherwise the shared scan key (which counts against their daily scan credits). Returns a job_id — poll get_reverse_source_status(job_id) for results. Premium feature.
- 2026-10-09 `get_ungate_brands` added
  - The curated Sorsa ungate brand list (~1300 brands), each with a representative ASIN that can be run through request_ungate_check. Use this to look up the ASIN for a brand the user names (e.g. "can you ungate me for Adidas?" -> find Adidas here -> request_ungate_check([that asin])), or to enumerate/batch brands. Pass `search` to filter by brand substring. Returns {count, brands:[{brand, asin}]}. `count` is the full list size even when `search`/`limit` narrows the returned rows.
- 2026-10-09 `start_a2a_finder_scan` changed (description)
  - before: Start an A2A Finder scan: check a list of ASINs for profitable Amazon-to-Amazon flips across marketplaces (buy on one Amazon country, sell on another). Up to 100 ASINs. Optional `countries` = list of country codes to compare (e.g. ["UK","DE","FR"]); defaults to all. Uses the shared Keepa scan key (counts against the user's daily scan credits). Returns a job_id — poll get_a2a_finder_status(job_id) for results. Premium feature.
  - after: Start an A2A Finder scan: check a list of ASINs for profitable Amazon-to-Amazon flips across marketplaces (buy on one Amazon country, sell on another). Up to 100 ASINs. Optional `countries` = list of country codes to compare (e.g. ["UK","DE","FR"]); defaults to all. Uses the user's saved Keepa key if they've added one in settings; otherwise the shared scan key (which counts against their daily scan credits). Returns a job_id — poll get_a2a_finder_status(job_id) for results. Premium feature.
- 2026-10-07 `confirm_ungate_supplier` added
  - Record that a supplier's invoice got the user approved (ungated) for a brand. This increments that supplier's confirmation count for the brand so other users see which suppliers actually work.
- 2026-10-07 `get_ungate_suppliers` added
  - Suppliers (manufacturers / authorised distributors / wholesalers that issue proper invoices) for a brand or category, with how many Sorsa users confirmed each one got them ungated for that brand. Provide a brand and/or a category.
- 2026-10-07 `get_ungate_status` added
  - Status + outcome of an ungate job. Status is queued | in_progress | completed | failed | expired; outcome (when set) is approved, already_approved, documents_required, not_accepting_applications, login_required or unknown. While status is 'queued' or 'in_progress', CALL THIS AGAIN after a short wait (e.g. 20-40s) — the job is processed by the user's extension serially. For 'documents_required' the response also includes Amazon's requirements text, a `suppliers` list for the brand/category, and `suggested_asins` (easier wins in the same brand). For 'login_required', tell the user to log into Seller Central and retry.
- 2026-10-07 `request_ungate_check` added
  - Request an ungate check for one or more gated ASINs. For each ASIN this confirms it's gated (via the eligibility check) and, if so, QUEUES a job that the user's Sorsa browser extension works through — it opens the Seller Central approval request in their own logged-in session and reports the outcome. Returns per-ASIN results, each one of: 'not_gated', 'already_approved', 'checking' (eligibility still resolving — re-call shortly), 'recent_attempt', 'queue_full', or 'queued' with a `job_id` (and the brand's auto-approval rate when known). Jobs run STRICTLY ONE AT A TIME, ~daily-capped, and carry over across days — so a long list is fine; it just drains serially. The user needs their Amazon (SP-API) connected AND the Sorsa extension installed and online. After queuing, poll get_ungate_status(job_id) after a short wait (jobs are processed serially, each can take tens of seconds to a few minutes). Tell the user if their extension is offline (the response includes `extension_online`).
- 2026-10-07 `create_finder_monitor` added
  - Create a LIVE Keepa Product Finder monitor from the same filters as find_products. It runs on the user's OWN Keepa key and sends a Discord alert to `webhook_url` whenever a NEW product starts matching the filters — the saved/recurring version of find_products. REQUIRED: a `name` for the monitor and a Discord `webhook_url` (https://…) — ALWAYS ask the user for their webhook URL; never invent one. The user must also have their Keepa key connected (call get_keepa_key_status first). Provide at least one filter (same semantics/units as find_products). This creates a real, active monitor that will start pinging the webhook — confirm with the user before creating it. Set active=False to create it paused.
- 2026-10-02 `find_products` changed (inputSchema)
- 2026-10-01 `run_sheet_action` added
  - Trigger a function on the user's OWN connected Google Sheet by POSTing to their deployed Apps Script web app. Call get_sheet_status first; if not connected, tell them to connect their sheet in Sorsa Settings. `action` is the name their script's doPost dispatches on (e.g. 'send_invoice', 'add_row'). `params` is a dict of values that action needs. The user's email is always sent automatically, so the script can target the right row/recipient. Only do what the user asked — don't invent actions or params. If you're not sure which action name or params the user's script expects, ASK them rather than guessing. Returns whatever the script returns.

Full history: https://api.protogrid.dev/v1/servers/app.sorsa%2Fsorsa/changes

## Remotes

- https://sorsa.app/mcp (streamable-http, auth api_key ${X_API_KEY}, reachable true, uptime30d 1)

## Packages

_none_

## Tools (70)

- `add_purchase`: Add an item the user has BOUGHT to their Items Purchased list (the
    next step after sourcing). Returns the created item incl. its `id` —
    use that id with generate_sku and create_listing. cost is per unit;
    sourced_from is the retailer/site they bought from.

    projected_sale_price: default this to the product's 90-day average price
    (from analyse_product) and tell the user what you've set — don't guess a
    price. It's what create_listing will list at unless changed.
- `add_storefront_monitor`: Start monitoring another Amazon seller's storefront — new products they
    list will appear in the user's store results. storefront_id is the Amazon
    seller ID (e.g. A1B2C3D4E5F6G7, found in the seller page URL). Call when
    the user says 'track this seller'.
- `add_to_sourcing_list`: Add a product to the user's sourcing list (their buy list). Call when
    the user says 'add this to my buy list' — typically after analysing a
    product, passing through the numbers from the analysis. sourced_from is
    where they'd buy it (retailer/site).
- `analyse_product`: Quick analysis of an Amazon product by ASIN: title, brand, current/90-day
    pricing, sales rank and estimated monthly sales, FBA fees and profit basics.
    Call this first whenever the user asks about a specific ASIN. Costs the user
    1 analyse credit unless cached. Domain is the Amazon marketplace: GB, US,
    DE, FR, ES or IT.
- `analyse_product_full`: Full analysis of an Amazon product by ASIN — everything in analyse_product
    plus seller/stock breakdown, buy-box history and variations. Slower and
    heavier than analyse_product; use when the user wants seller-level or
    historical detail in one shot. Costs 1 analyse credit unless the ASIN was
    analysed in the last 24 hours.
- `calculate_profit`: Calculate Amazon FBA profit and ROI for given sell/cost prices using the
    full fee engine (tiered referral fees, digital services tax, VAT on fees).
    Call for what-if questions like 'what would I make selling at £24.99 if I
    buy at £12?'. Prices are in the marketplace currency.
- `check_eligibility`: Check whether the user is allowed to sell (ungated for) one or more
    ASINs on their Amazon account. Call for 'am I ungated for this?' or 'can I
    sell these ASINs?'. Returns per-ASIN {state, gated, apply_link}. Uncached
    ASINs are queued for a fresh check, so re-call shortly for any still
    pending. Requires a connected Amazon account.
- `confirm_ungate_supplier`: Record that a supplier's invoice got the user approved (ungated) for a
    brand. This increments that supplier's confirmation count for the brand so
    other users see which suppliers actually work.
- `create_deal_task`: Create an A2A deal-monitor task: the user gets notified whenever the
    deal feed finds products matching these filters. Call when the user says
    e.g. 'notify me about deals over £5 profit and 40% ROI in Toys'.
- `create_finder_monitor`: Create a LIVE Keepa Product Finder monitor from the same filters as
    find_products. It runs on the user's OWN Keepa key and sends a Discord
    alert to `webhook_url` whenever a NEW product starts matching the filters —
    the saved/recurring version of find_products.

    REQUIRED: a `name` for the monitor and a Discord `webhook_url` (https://…) —
    ALWAYS ask the user for their webhook URL; never invent one. The user must
    also have their Keepa key connected (call get_keepa_key_status first).

    Provide at least one filter (same semantics/units as find_products). This
    creates a real, active monitor that will start pinging the webhook — confirm
    with the user before creating it. Set active=False to create it paused.
- `create_listing`: Create the Amazon listing for a purchased item — the final step.
    Requires the item to have an ASIN, a listing_sku (run generate_sku first)
    and a sale price.

    SALE PRICE RULE: set `sale_price` to the product's 90-day average price
    (from analyse_product) as a sensible default — but ALWAYS tell the user
    the price you're about to list at and get their OK first. Never invent a
    price. If sale_price is omitted, the item's existing projected_sale_price
    is used (and the call fails if it has none).

    fulfillment: AMAZON_EU (FBA) or DEFAULT (FBM). This creates a REAL live
    Amazon listing on the user's account.
- `create_price_monitor`: Watch an ASIN and alert the user when its price drops to the target.
    Call when the user says things like 'watch this' or 'alert me if it goes
    below £20'. source is the marketplace: Amazon UK, Amazon DE, Amazon IT,
    Amazon ES, Amazon FR, Amazon US or Amazon Business UK.
- `find_products`: Search Amazon via Keepa's Product Finder and return the matching ASINs
    (up to 100). Runs on the USER'S OWN Keepa key — call get_keepa_key_status
    first; if not connected, tell them to add their key in Sorsa Settings.

    Provide at least one filter. Prices are in the marketplace's currency
    (e.g. £ for UK). marketplace: UK, US, DE, FR, IT, ES.

    Drop filters find items whose price fell by a % over a period — set any of
    drop_1d/7d/30d/90d (e.g. drop_30d=20 = 'dropped 20%+ in 30 days'), applied
    to drop_type: BUY_BOX_SHIPPING, AMAZON, or PRIME_EXCL.

    category is a Keepa root-category id (omit for all categories). Returns
    {count, asins[]} — analyse specific ones with analyse_product.
- `generate_sku`: Generate a SKU for a purchased item (from its title/cost/qty using the
    user's SKU convention) and save it to the row. Pass the `id` returned by
    add_purchase or shown in get_items_purchased. Do this before create_listing.
    Returns the item with its new listing_sku.
- `get_a2a_finder_jobs`: List the user's recent A2A Finder scan jobs and their status. Call to
    find a job_id whose results to fetch.
- `get_a2a_finder_status`: Status and results of an A2A Finder scan started with
    start_a2a_finder_scan. Returns progress % and, once complete, the
    profitable cross-marketplace flips found (per ASIN, per country).
- `get_aged_stock`: Age profile of the user's on-hand FBA stock, bucketed by days on shelf
    (0-30, 31-90, 91-180, 181-365, 365+), with capital tied up and how much is
    aged 90+ days (long-term storage-fee risk). Call for 'what stock is old or
    at risk?' or 'how much capital is stuck in aged inventory?'. Requires a
    connected Amazon account.
- `get_amazon_inventory`: The user's live FBA inventory: SKUs, quantities, conditions. Call when
    the user asks what stock they have or about a specific SKU/product they
    sell. Requires their Amazon account to be connected (closed beta).
- `get_amazon_orders`: The user's Amazon orders with fees and COGS-based profit. Call for
    questions like 'how are sales today?' or 'show orders for this SKU'.
    timeframe: today, yesterday, week, month; or use date_from/date_to
    (YYYY-MM-DD). Requires a connected Amazon account (closed beta).
- `get_analyse_history`: The user's recent product analyses (ASIN, title, when). Call when the
    user asks what they looked at recently, or to re-find a product they
    mention having analysed. limit max 50.
- `get_best_sellers`: The user's own top-selling ASINs by net profit for each of the last N
    months, derived from their sales. Call for 'what are my best sellers?' or
    'top products this year'. months up to 24, top_n up to 50. Requires a
    connected Amazon account.
- `get_credits`: The user's remaining monthly analyse and AI credits and the reset date.
    Call when the user asks about their usage limits, or after a credit-limit
    error from an analysis tool.
- `get_dashboard_summary`: The user's Sorsa dashboard summary — headline stats across their
    monitors and results. Call for a general 'how's it looking' overview before
    drilling into specific feeds.
- `get_deal_results`: Search Sorsa's Amazon-to-Amazon (A2A) deal database — products found by
    the deal monitors with profit, ROI, sales rank and pricing. Call when the
    user asks for current deals, e.g. 'show me deals over £5 profit and 50%
    ROI'. Free to call. sort_field: timestamp, profit, roi_percentage,
    asin_rank or current_price.
- `get_deal_tasks`: List the user's A2A deal-monitor tasks (saved filters that generate
    notifications). Call before creating one, or when the user asks what
    monitors they have set up.
- `get_fba_refunds`: Refunds issued to the user's customers. Call when the user asks about
    refunds or money returned to buyers. Requires a connected Amazon account
    (closed beta).
- `get_fba_reimbursements`: Reimbursements Amazon has paid the user (lost/damaged stock etc).
    Call when the user asks what Amazon has paid them back. Requires a
    connected Amazon account (closed beta).
- `get_fba_returns`: The user's FBA customer returns with dispositions (sellable, damaged,
    etc). Call when the user asks about returns. Requires a connected Amazon
    account (closed beta).
- `get_fba_shipment_items`: The line items inside one FBA shipment (expected vs received
    quantities). Call after get_fba_shipments when the user asks what was in
    a specific shipment or whether everything was received.
- `get_fba_shipments`: The user's inbound FBA shipments with summary stats. Call when the
    user asks about shipments they've sent to Amazon. Requires a connected
    Amazon account (closed beta).
- `get_international_prices`: Prices for the same ASIN across international Amazon marketplaces. Call
    when the user asks about cross-market price differences or EU arbitrage
    opportunities for a product. Costs 1 analyse credit unless the ASIN was
    analysed in the last 24 hours.
- `get_items_purchased`: Items the user has actually purchased from their sourcing list,
    including listing status (SKU, listing errors). Call when the user asks
    what they've bought or about listing progress.
- `get_items_sourced`: The user's sourcing list — products they've found and are considering
    buying, with cost/profit projections and status. Call when the user asks
    about their buy list or sourcing pipeline.
- `get_keepa_key_status`: Whether the user has connected their own Keepa API key (needed for
    find_products). If not connected, tell them to add it in Sorsa Settings.
    Never returns the key itself.
- `get_monthly_pnl`: Per-SKU profit & loss for one calendar month: revenue, Amazon fees,
    COGS, refunds and net profit, plus account totals. Call for 'what was my
    profit in <month>?' or 'break down last month's P&L'. month is 'YYYY-MM'.
    Requires a connected Amazon account.
- `get_notifications`: The user's price-alert notifications from their A2A monitor tasks,
    grouped by task. Call when the user asks what alerts or notifications they
    have. show_checked: 'unchecked' (default), 'checked' or 'all'.
- `get_oa_deals`: Profitable online-arbitrage (OA) deals: retail products matched to Amazon
    listings with profit and ROI. Call when the user asks about OA or retail
    sourcing opportunities. sort: roi, profit, price, newest, sell, rank,
    monthly_sold or sellers.
- `get_oa_retailers`: List the retailers available in the OA deals feed. Call to know which
    retailer names are valid filters for get_oa_deals.
- `get_price_monitors`: List the user's ASIN price monitors (watched products with target
    prices). Call before creating one to avoid duplicates, or when the user
    asks what they're watching.
- `get_product_performance`: Per-SKU profitability over the last N days: units, revenue, profit, ROI,
    return rate, current price vs break-even and a selling-at-a-loss flag. Call
    for 'which products make or lose me money?' or 'what's selling below
    break-even?'. days 7-365. Requires a connected Amazon account.
- `get_product_sellers`: Current sellers and their stock levels for an Amazon ASIN. Call when the
    user asks who is selling a product, how many sellers there are, or how much
    stock competitors hold. Costs 1 analyse credit unless the ASIN was analysed
    in the last 24 hours.
- `get_purchases`: The user's logged purchase history. timeframe: all, day, week, month
    or year. Call when the user asks what they've bought recently.
- `get_purchase_stats`: Purchase statistics: total spent, projected profit, items bought and
    ROI, broken down daily/monthly/yearly and by category and source. Call
    for questions like 'how much have I spent this month?'.
- `get_reconciliation_claims`: Individual reclaim opportunities with evidence — what to claim from
    Amazon and why. Call after get_reconciliation_summary to list the actual
    claims. status: open, dismissed, filed or all. Requires a connected
    Amazon account (closed beta).
- `get_reconciliation_summary`: Totals of reclaimable money Sorsa has detected the user is owed by
    Amazon, broken down by claim type (shipment shortages, lost inventory,
    unreturned refunds...). Call for 'how much does Amazon owe me?'.
    Requires a connected Amazon account (closed beta).
- `get_repricer_items`: The user's repricer SKUs with live pricing state: current price, Buy Box
    price, lowest FBA price, cost/min/max, strategy, computed profit/ROI at the
    current price, floor price and stock. Call when the user asks what the
    repricer is doing, whether they hold the Buy Box on something, or why an
    item isn't repricing. search matches SKU or ASIN. status filters to one of:
    active, off, out_of_stock, needs_cog, needs_min, awaiting_fees, error, or
    'attention' (needs_cog + needs_min + error). limit max 500. Read-only.
- `get_repricer_log`: Recent repricer price changes, newest first: SKU, old → new price, the
    reason, the reference price it repriced against (Buy Box / lowest FBA) and
    whether the floor or ceiling capped the move. Call when the user asks what
    the repricer changed recently or why a price moved. limit max 500.
- `get_repricer_settings`: The user's global repricer settings: whether repricing is globally
    enabled, seller ID, default strategy, default minimum ROI, allowed
    strategies and VAT registration. Read-only — changing settings is
    web-app only.
- `get_repricer_stats`: Repricer dashboard summary: total/active SKU counts, per-status
    breakdown, Buy Box wins and win %, items matching the Buy Box exactly,
    units actively repricing and the last update time. Call first for broad
    questions like 'how's the repricer doing?'.
- `get_restock_suggestions`: What the user should reorder now. Per-SKU sales velocity vs stock on
    hand -> days of cover left, a suggested reorder quantity and its potential
    profit. Call for 'what should I restock?' or 'what's running low?'.
    lead_time_days = how long a resupply takes to land; cover_days = days of
    stock the reorder should provide. Requires a connected Amazon account.
- `get_reverse_source_jobs`: List the user's recent Reverse Source scan jobs and their status. Call
    to find a job_id whose results to fetch.
- `get_reverse_source_status`: Status and results of a Reverse Source scan started with
    start_reverse_source_scan. Returns progress % and, once complete, the
    cheapest Google Shopping source found per ASIN (retailer, price, link).
- `get_sheet_status`: Whether the user has connected their own Google Sheet (a deployed Apps
    Script web app) that run_sheet_action can trigger. If not connected, tell
    them to add their web-app URL in Sorsa Settings. Never returns the URL.
- `get_storefront_monitors`: List the Amazon storefronts (competitor sellers) the user is
    monitoring. Call before adding one, or when the user asks which
    storefronts they track.
- `get_storefront_stats`: Summary stats for the user's storefront monitors (stores tracked,
    result counts). Call for an overview before drilling into
    get_store_results.
- `get_store_results`: Products found by the user's storefront monitors (tracking other sellers'
    Amazon storefronts), newest first. Call when the user asks what their
    storefront monitors have found; optionally filter to one store_id.
    Paginated — 'total' in the response is the full count, request further
    pages for more. per_page max 200 (keep small: rows are wide).
- `get_ungate_brands`: The curated Sorsa ungate brand list (~1300 brands), each with a
    representative ASIN that can be run through request_ungate_check. Use this to
    look up the ASIN for a brand the user names (e.g. "can you ungate me for
    Adidas?" -> find Adidas here -> request_ungate_check([that asin])), or to
    enumerate/batch brands. Pass `search` to filter by brand substring.

    Returns {count, brands:[{brand, asin}]}. `count` is the full list size even
    when `search`/`limit` narrows the returned rows.
- `get_ungate_status`: Status + outcome of an ungate job. Status is queued | in_progress |
    completed | failed | expired; outcome (when set) is approved,
    already_approved, documents_required, not_accepting_applications,
    login_required or unknown.

    While status is 'queued' or 'in_progress', CALL THIS AGAIN after a short
    wait (e.g. 20-40s) — the job is processed by the user's extension serially.
    For 'documents_required' the response also includes Amazon's requirements
    text, a `suppliers` list for the brand/category, and `suggested_asins`
    (easier wins in the same brand). For 'login_required', tell the user to log
    into Seller Central and retry.
- `get_ungate_suppliers`: Suppliers (manufacturers / authorised distributors / wholesalers that
    issue proper invoices) for a brand or category, with how many Sorsa users
    confirmed each one got them ungated for that brand. Provide a brand and/or a
    category.
- `get_wholesale_jobs`: List the user's recent Wholesale Scanner jobs and their status. Call to
    find a job_id whose results to fetch.
- `get_wholesale_status`: Status and results of a Wholesale Scanner run. Returns progress % and,
    once complete, each supplier product matched to Amazon with sell price,
    profit and ROI.
- `lookup_by_ean`: Resolve a barcode (EAN/UPC, 8-14 digits) to an Amazon ASIN. Call when the
    user gives a barcode instead of an ASIN, then feed the returned ASIN into
    analyse_product.
- `mark_notification`: Mark one of the user's notifications as checked (reviewed) or unchecked.
    Call after the user says they've dealt with an alert, or to un-mark one.
- `request_ungate_check`: Request an ungate check for one or more gated ASINs. For each ASIN this
    confirms it's gated (via the eligibility check) and, if so, QUEUES a job that
    the user's Sorsa browser extension works through — it opens the Seller Central
    approval request in their own logged-in session and reports the outcome.

    Returns per-ASIN results, each one of: 'not_gated', 'already_approved',
    'checking' (eligibility still resolving — re-call shortly), 'recent_attempt',
    'queue_full', or 'queued' with a `job_id` (and the brand's auto-approval rate
    when known). Jobs run STRICTLY ONE AT A TIME, ~daily-capped, and carry over
    across days — so a long list is fine; it just drains serially.

    The user needs their Amazon (SP-API) connected AND the Sorsa extension
    installed and online. After queuing, poll get_ungate_status(job_id) after a
    short wait (jobs are processed serially, each can take tens of seconds to a
    few minutes). Tell the user if their extension is offline (the response
    includes `extension_online`).
- `run_sheet_action`: Trigger a function on the user's OWN connected Google Sheet by POSTing to
    their deployed Apps Script web app. Call get_sheet_status first; if not
    connected, tell them to connect their sheet in Sorsa Settings.

    `action` is the name their script's doPost dispatches on (e.g. 'send_invoice',
    'add_row'). `params` is a dict of values that action needs. The user's email
    is always sent automatically, so the script can target the right row/recipient.

    Only do what the user asked — don't invent actions or params. If you're not
    sure which action name or params the user's script expects, ASK them rather
    than guessing. Returns whatever the script returns.
- `search_by_title`: Search Amazon by product title via Keepa and return the top matching
    ASINs with thumbnails. Call when the user names a product without an ASIN
    or barcode; present the candidates and let them pick before analysing.
    limit is 1-10.
- `set_cog`: Set the cost of goods (COG) for a SKU — the per-unit price the user
    paid. Feeds profit/ROI across the whole app and the repricer's floors.
    cost in the user's currency (e.g. £). Pass cost=0 or empty to clear it.
- `start_a2a_finder_scan`: Start an A2A Finder scan: check a list of ASINs for profitable
    Amazon-to-Amazon flips across marketplaces (buy on one Amazon country, sell
    on another). Up to 100 ASINs. Optional `countries` = list of country codes
    to compare (e.g. ["UK","DE","FR"]); defaults to all. Uses the user's saved
    Keepa key if they've added one in settings; otherwise the shared scan key
    (which counts against their daily scan credits). Returns a job_id — poll
    get_a2a_finder_status(job_id) for results. Premium feature.
- `start_reverse_source_scan`: Start a Reverse Source scan: for each ASIN, search Google Shopping for a
    cheaper source than Amazon — i.e. where to buy the product to flip it. Up
    to 250 ASINs. Uses the user's saved Keepa key if they've added one in
    settings; otherwise the shared scan key (which counts against their daily
    scan credits). Returns a job_id — poll get_reverse_source_status(job_id)
    for results. Premium feature.
- `start_wholesale_scan`: Start a Wholesale Scanner run against a supplier price list. Each item in
    `products` is a dict with 'title', 'price' (cost per unit), and 'ean'
    (barcode — needed to match, the default scan is EAN-based). Up to 500 items.
    Matches each product to Amazon and returns profit/ROI. Returns a job_id —
    poll get_wholesale_status(job_id) for results. Premium feature.
