Add CSV price guides and PriceCharting alongside eBay
An eBay production keyset needs account verification, which leaves pricing blocked on someone else's review queue. These are two routes that are not. CSV price guide, POST /api/prices/import: no account, works immediately. Column names are matched by alias, so a PriceCharting bulk export (product-name / console-name / loose-price) and a hand-kept title,system,loose,cib,new sheet both parse, along with currency symbols, thousands separators and blank cells. Rows match on title + system, so the same game on two consoles is priced separately, and rows for games not in the library are reported rather than silently added. PriceCharting adapter: paid, but access is immediate with no review, and it quotes the same three tiers this app stores, so no inference is needed. Their API docs are not reachable without an account, so the parser follows the widely-used convention — integer pennies under hyphenated keys — and is tolerant enough that a naming difference degrades to "no price" instead of throwing. One method to adjust if it differs. Providers are now a registry rather than a single service. /api/prices/status lists each one with what it is configured for, what its numbers actually mean, and how to enable it; refresh takes an optional provider name and falls back to the first configured one. With none configured it answers 503 pointing at the CSV route. Checked against the real library: a five-row guide in PriceCharting's own column names priced four games and reported the fifth as not owned, with each effective value following that copy's condition. Demo figures were cleared afterwards. 135 backend tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -216,43 +216,52 @@ keys: `rating`, `value`, `price`, `purchased`.
|
||||
|
||||
### Market value
|
||||
|
||||
Prices come from **eBay's Browse API**, which needs a free developer account:
|
||||
register at developer.ebay.com, create a production application keyset, and put
|
||||
the App ID and Cert ID in `.env` as `EBAY_CLIENT_ID` / `EBAY_CLIENT_SECRET`.
|
||||
Without them the pricing endpoints answer 503 with an explanation and nothing
|
||||
else is affected.
|
||||
Three routes, deliberately independent, because each is blocked differently.
|
||||
|
||||
| Route | Blocked by | Cost | Basis |
|
||||
| --- | --- | --- | --- |
|
||||
| **CSV price guide** | nothing | free | whatever you supply |
|
||||
| **PriceCharting** | nothing — immediate on subscribing | paid | sale-derived, per condition |
|
||||
| **eBay Browse** | production keyset needs account verification | free | asking prices, reads high |
|
||||
| Manual entry | nothing | free | your own judgement |
|
||||
|
||||
```
|
||||
GET /api/prices/status is a provider configured?
|
||||
POST /api/prices/refresh {dryRun, limit} price some games
|
||||
GET /api/prices/status which sources are usable
|
||||
POST /api/prices/refresh {provider, dryRun} price from a live source
|
||||
POST /api/prices/import (multipart CSV) apply a price list
|
||||
```
|
||||
|
||||
**These are asking prices, not sold prices.** Browse returns active listings.
|
||||
eBay's completed-sales data lives behind the Marketplace Insights API, which is
|
||||
a limited release closed to new developers, and PriceCharting — the usual
|
||||
alternative — requires a paid subscription. Asking prices skew high: sellers
|
||||
list optimistically and unsold listings linger. Treat the numbers as an upper
|
||||
bound. The provider name (`ebay-asking`) is stored with every value it writes,
|
||||
so the source is always visible next to the figure.
|
||||
**The CSV route needs no account and works today.** Column names are matched by
|
||||
alias, so `product-name` / `console-name` / `loose-price` from a PriceCharting
|
||||
export and a hand-kept `title,system,loose,cib,new` sheet are both accepted, as
|
||||
are `$`, thousands separators and blank cells. Rows are matched on title +
|
||||
system, so the same game on two consoles is priced separately; rows for games
|
||||
you do not own are reported rather than added.
|
||||
|
||||
Deriving a price from listings takes more than an average:
|
||||
Free sources that do **not** work for this: eBay's completed-sales data sits
|
||||
behind the Marketplace Insights API, a limited release closed to new
|
||||
developers; NEXARDA and CheapShark price current retail and digital
|
||||
storefronts, not collectibles. Scraping PriceCharting violates their terms.
|
||||
|
||||
- **Listings are classified into loose / CIB / new** from the title, because a
|
||||
feed of mixed conditions has no single price. Accessories are discarded
|
||||
outright — a "box only" listing at $45 counted as a copy would halve the loose
|
||||
estimate for a $130 cartridge. So are reproductions and multi-game lots.
|
||||
- **The qualifier is required when discarding.** An early version matched a bare
|
||||
"box", which threw away "complete in box" and "with box and manual" — most of
|
||||
the CIB tier — while keeping the cheap box-only listings the filter existed to
|
||||
remove. Caught by a test asserting on the tier, not on the count.
|
||||
- **Median, not mean,** with an interquartile trim. One optimist asking 50x drags
|
||||
a mean past the point of usefulness; a median ignores them.
|
||||
- **Sample counts travel with the estimate.** A tier from two listings deserves
|
||||
less confidence than one from thirty.
|
||||
Deriving a price from eBay listings takes more than an average:
|
||||
|
||||
- **Listings are classified into loose / CIB / new** from their titles, since a
|
||||
mixed feed has no single price. Accessories are discarded — a "box only"
|
||||
listing at $45 counted as a copy would halve the loose estimate for a $130
|
||||
cartridge — as are reproductions and multi-game lots.
|
||||
- **The discard qualifier is required.** An early version matched a bare "box",
|
||||
which threw away "complete in box" and "with box and manual" — most of the CIB
|
||||
tier — while keeping exactly the listings the filter existed to remove. Caught
|
||||
by a test asserting on tiers rather than counts.
|
||||
- **Median with an interquartile trim.** One optimist asking 50x moves a mean
|
||||
and not a median.
|
||||
- **Sample counts travel with the estimate**, because a tier drawn from two
|
||||
listings deserves less confidence than one drawn from thirty.
|
||||
|
||||
Three prices are stored per game, and `marketValue` is whichever tier matches
|
||||
that copy's condition. Changing a game's condition re-prices it from the stored
|
||||
tiers without another lookup.
|
||||
that copy's condition — so changing a condition re-prices it with no further
|
||||
lookup. Every value carries the source that wrote it and the moment it was
|
||||
captured.
|
||||
|
||||
### Database changes
|
||||
|
||||
|
||||
Reference in New Issue
Block a user