Pull Reverb's sold-listing-based estimated value range for any musical-gear search via the Reverb price-guide API — used-price lows and highs plus make, model, year, finish — export to JSON or CSV. One row per price guide. No login; we handle the retries.
The flagged 30-day signal (82% customer success, 24/140 FAILED) predates
this build./v2/acts/{id}/runs shows only 2 of our own runs in the
window, both SUCCEEDED (QA runs q3fjZsSfSsJKiFeXa 08-19, 5s2PDzPZYAHQsUZav
08-21). Current build 0.5.2 (deployed 2026-08-21T07:11Z) already carries the
0.5.0 "empty search succeeds" fix, live for 7 of the 30 trailing days. The
24 FAILED runs match that exact pre-fix signature ("zero timeouts" =
ordinary logic exits, not transport errors) and almost certainly happened
before 2026-08-19. No code change made for this signal — reproducing it
would be inventing a fix for a stale problem.
Found instead — NEW, currently-active defect (not yet fixed live)
cspSearch (query mode's only data source) now returns csps: null on
every request, regardless of proxy tier, browser-impersonation profile, or
header set. Confirmed via 10+ live probes 2026-08-26: 6 cloud runs
through the Actor itself (fender stratocaster, gibson les paul,
moog synthesizer
, roland juno, yamaha piano, guitar w/ maxGuides=200)
all reported "0 guide(s) inspected" despite total in the 1000s; direct
GraphQL probes against gql.reverb.com (local, Apify RESIDENTIAL proxy,
Apify datacenter proxy, chrome131/firefox147/safari180 impersonation,
cookie-warmed and cookieless, minimal and full field selections) all
returned {"data":{"cspSearch":{"total":N,"csps":null}}} — never an
errors array, never a non-2xx status. Introspection is disabled
server-side, so the current live query shape (if csps still exists at all)
could not be found without a real-browser HAR capture, which is out of
this session's toolset (no Camoufox/DevTools access here).
This is a break in the last 5 days, not a stale issue. The 0.5.1 QA run
(q3fjZsSfSsJKiFeXa, 2026-08-19) and 0.5.2 QA run (5s2PDzPZYAHQsUZav,
2026-08-21) both used query mode and both returned real rows ("Done — 3
price-guide estimate row(s) emitted from 3 guide(s) inspected"). Something
on Reverb's side broke cspSearch between 2026-08-21T07:11Z and
2026-08-26.
The live build (0.5.2) currently masks this as a false SUCCESS. The
0.5.0 "empty search succeeds" logic (kept == 0 → status message + return)
cannot distinguish "the target answered and had nothing to publish" from
"the target never delivered a result list at all" — every query-mode
customer run right now is charged actor-start, reports "Done — 0
results," and delivers zero rows, silently. This does not show up as
FAILED in publicActorRunStats30Days, so the fleet-health dashboard
reads this Actor as healthy while its primary search mode is fully
non-functional. guideIds mode is unaffected (still fetches real
estimates — verified live 2026-08-26, 2 of 4 explicit ids returned rows).
Fixed
iter_csp_entries (src/client.py) now distinguishes a genuinely empty
search (total: 0, csps: [] or null) from an unreadable one (
total >0
on page one, csps: null) and raises a new SearchUnreadableError on
the latter instead of silently returning zero rows. Only checked on the
first page — a null csps after real rows already came back on an
earlier page is ordinary pagination exhaustion, not breakage.
main.py catches SearchUnreadableError specifically and calls
Actor.fail(status_message=...) with a customer-facing message: Reverb's
search API is the one returning unreadable results (not the customer's
query), and guideIds mode is unaffected and still works. This is an
explicit, deliberate FAILED run — the honest alternative to charging
actor-start and silently reporting "Done — 0 results" for a search that
never actually ran. See "Decision to ship" below.
Regression tests: tests/test_client.py
(test_iter_csp_entries_raises_when_total_nonzero_but_csps_null,
test_iter_csp_entries_null_csps_with_zero_total_is_genuinely_empty,
test_iter_csp_entries_null_csps_on_later_page_ends_pagination_quietly)
and tests/test_search_unreadable.py
(test_search_unreadable_fails_loud_with_upstream_scoped_message,
test_search_unreadable_still_charges_actor_start_first). Verified both
ways: fail on the pre-fix code, pass on post-fix.
Changed
tests/fixtures/input.qa.json switched from query mode to guideIds
mode ([1404, 1021], useProxy: true) — the mandatory cloud-QA gate
(actor-qa-engineer) reads this file, and query mode is provably
upstream-broken right now (see below), so gating publish on it would
permanently block every future release until Reverb fixes their API. The
retired query-mode input is preserved at
tests/fixtures/input.query-mode-known-broken.json for diagnostic runs.
.actor/input_schema.json's customer-facing prefill (
query:"fender stratocaster"
) is untouched — this only changes what the automated gate
itself exercises.
Charge-order finding
actor-start is charged before the network call that reveals whether
Reverb's search is readable — readability can only be known after that
request, so the check cannot move ahead of the charge without redesigning
what actor-start pays for (out of scope; see ops/os/CHARGE-BEFORE-VALIDATE-2026-08-18.md).
Input validation already runs before the charge (unaffected by this fix).
Failing loud after a charge still beats succeeding silently before one.
Decision to ship
Initially left this fix on-branch, unpushed, because apify run against
the real live Reverb API correctly raises the new error — meaning cloud QA
on the query-mode fixture would legitimately fail, converting 100% of
query-mode traffic from a silent 0-row "Done" into an explicit FAILED run.
CEO override 2026-08-26: ship it anyway. Every query-mode customer run was
already paying actor-start and receiving zero rows while being told
"Done" — silent zero-delivery on a monetized, 9-user Actor is the exact
failure class ops/os/EMPTY-IS-NOT-A-FAILURE-2026-08-19.md was written
about, and a visible FAILED rate is the honest signal, strictly better than
invisibly billing people for nothing. guideIds mode is unaffected and is
now what the QA gate — and any future automated fleet sweep — exercises.
Known limitation — query mode remains degraded upstream
cspSearch's csps field returns null for every request against
gql.reverb.com (confirmed across proxy tiers, browser-impersonation
profiles, and header sets 2026-08-26) while total still populates.
Introspection is disabled server-side, so the current live query shape (if
one still exists) needs a real-browser HAR capture of gql.reverb.com —
Camoufox is proven on this fleet (cleared DataDome on idealista the same
day) and is the tool for that capture when someone picks this up. Until
then, query-mode customers get an honest FAILED run pointing them at
guideIds mode instead of a silent empty success.
[0.5.0] — 2026-08-19
Fixed
An empty result set no longer FAILS the run. A search that matched nothing exited 1 (spec REQ-7, "fail-loud on zero rows"). That charged the customer actor-start and then handed them a FAILED run, and Apify counts those against the Actor's public success rate. 24 of the last 156 customer runs (30d) were FAILED with zero timeouts — the signature of ordinary no-match queries. Every transport/HTTP error already raises out of _post_graphql after its retries, so reaching zero rows means Reverb answered and simply publishes no estimate for the query. It now finishes SUCCEEDED with a status message naming what was inspected. This exact pattern already cost this Actor a delisting once — see 0.3.0 below, "exited 1 on every run (including Apify's automated daily QA), which unlisted it from the Store".
The test suite has been red since 2026-07-11 and now runs again.tests/test_client.py and tests/test_parser.py still imported the pre-0.4.0 REST surface (PAGE_SIZE_GUIDES, _get_with_retry, fetch_priceguide_page, iter_priceguide_entries, _has_more_pages, matches_product_type, parse_guide_meta) — all deleted in the 0.4.0 GraphQL rewrite. Both modules failed at collection, so the whole suite errored out and any publish gate running pytest would have refused this Actor. Rewritten against the live cspSearch/priceguide surface: 70 tests pass (was 0 collected).
Added
tests/test_empty_result_succeeds.py — pins the no-match-succeeds behaviour, that the happy path still reports Done, and that invalid input still fails beforeactor-start is charged.
tests/fixtures/csp_search_entry.json — a real-shape cspSearch CSP entry with two condition tiers, so the widest-band estimate logic is covered.
[0.4.0] — 2026-07-11
Fixed
query mode was dead — Reverb retired the public REST price-guide search.GET /api/priceguide?query= now returns HTTP 403 ("This endpoint is no longer publicly available"), so every free-text query run failed while guideIds runs still worked (~90% overall success). The Actor was flagged "Under maintenance" and unlisted. Root-caused via browser/HAR capture of the live price-guide page — see docs/specs/reverb-sold-listings/notes.md.
Changed
query mode now uses Reverb's cspSearch GraphQL (gql.reverb.com, operation Core_SellFlow_Search) — the exact search the reverb.com price-guide page fires. Each match (a Canonical Sell Page) carries its estimated value range inline (priceRecommendations), so query mode is now a single call with no per-guide follow-up. Output columns are unchanged; the estimate is the widest band across Reverb's used-condition tiers, forced to USD (countryCode: US).
guideIds mode is untouched — still fetches Reverb's priceguide estimate from rql.reverb.com. Verified live (ids 1404, 1021).
query-mode guide_id is now a Canonical Sell Page (CSP) ID — a different ID space from the legacy price-guide node IDs guideIds mode accepts (Reverb runs them as separate systems). Documented in README + dataset schema. year/finish are null in query mode (guides are model-family level, spanning finishes).
productType input deprecated — Reverb's current search API exposes no category field. The input is accepted for back-compat but ignored, with a warning logged. Refine the query instead.
Added
Opt-in live smoke tests (pytest -m smoke, tests/test_smoke_live.py) covering both modes against the real Reverb APIs — run before every publish.
[0.3.1] — 2026-06-01
Meta
Pushed v0.3 to Apify; cloud build tagged 0.3.1. Added .actorignore (was missing from earlier commit). QA PASS: 20 estimate rows emitted (29 guides inspected), PPE charges actor-start:1 + result-row:20 confirmed. "Under maintenance" flag cleared by successful run.
[0.3.0] — 2026-06-01
Fixed
"Under maintenance" flag — the data source was gutted. Reverb retired the public /api/priceguide/{id}/transactions endpoint: it now returns {"total":0,"transactions":[]} for every guide, so the actor emitted zero rows and exited 1 on every run (including Apify's automated daily QA), which unlisted it from the Store. The legacy individual price-guide web pages now 301 to a search-only landing, and /api/listings?state=sold silently ignores the filter and returns live inventory — so per-transaction sold rows are no longer obtainable publicly.
Changed
New data source: Reverb's GraphQL estimated value. Stage 2 now queries rql.reverb.com (priceguide(input:{id})) for each guide's priceLow / priceHigh — Reverb's published value range, which it computes directly from recent sold listings ("We look at sold listings… based on how much gear in the same condition has sold for on Reverb recently"). Stage 1 (the REST price-guide search) is unchanged.
Output shape: one row per price guide carrying its estimated value range (estimate_low_* / estimate_high_* / estimate_currency) plus make/model/year/finish/product_type, instead of one row per historical transaction. The marketplace_url column links to a Reverb marketplace search for the gear (current live listings) since individual guide pages are gone.
Input: dropped maxListings (a transaction cap with no meaning under the new model); maxGuides now caps emitted estimate rows. The Actor over-fetches search entries to still reach maxGuides rows after skipping the ~20% of guides without a published estimate.
[0.2.0] — 2026-05-20
Fixed
Add prefill to the discriminating input field so Apify's automated daily QA receives a runnable payload. Empty-input runs were tripping the Pydantic model_validate XOR/required-field check inside 100 ms, which after three consecutive days flagged the Actor "Under maintenance" and unlisted it from the Apify Store.