Environment
- opencli 1.8.6 (Node v24.14.1, Windows 11)
- Adapter:
clis/taobao/detail.js
- Reproduced with a logged-in Taobao session (
opencli taobao whoami → logged_in: true)
Summary
opencli taobao detail <id> returns a decoy price instead of the real price
for items whose detail page lives on detail.tmall.com and is served
through Taobao's "百亿补贴" (Billion Subsidy) channel. The decoy is not a
scraping artifact picked up by mistake — it's a real anti-scrape anchor
value Alibaba's own server bakes directly into the page's SSR hydration
state (window.__ICE_APP_CONTEXT__) when the request doesn't carry a
session-scoped channel token.
Important correction from an earlier draft of this report: the decoy
value is not always the literal ¥9999. Adversarial re-testing against a
wider item sample found decoys of ¥3999, ¥4499, and others — any
round-number anchor the seller/campaign happens to configure. A fix that
pattern-matches a specific literal value (e.g. price === '9999') will
silently pass these through as if they were real prices. The only reliable
signal is structural: the real, final price lives in
componentsVO.priceVO.extraPrice and that key is absent from the SSR
payload on a gated render, regardless of what number ends up in the
price fallback field. A second, narrower bug in the same function also
causes wrong prices on ordinary (non-subsidy) tmall items in some cases.
Reproduction
$ opencli taobao search "罗技鼠标" # or any query landing on a 百亿补贴 tmall listing
...
$ opencli taobao detail 1009201607950
- field: 价格
value: ¥9999
Search-page price for the same id that day: ¥639.
| item id |
domain |
search-page price |
detail price (before fix) |
| 1009201607950 |
detail.tmall.com (百亿补贴) |
¥639 |
¥9999 |
| 837347358130 |
detail.tmall.com (百亿补贴) |
¥439 |
¥9999 |
| 1046289235959 |
detail.tmall.com (百亿补贴) |
¥1609 |
¥3999 (not 9999 — a literal-value check misses this) |
| 1039885607033 |
detail.tmall.com (百亿补贴) |
¥2388.89 |
¥4499 (not 9999 either) |
| 900757934496 |
detail.tmall.com (百亿补贴) |
¥149 |
¥9999 |
| 800417757444 |
detail.tmall.com (ordinary) |
¥253.73 |
¥7 (wrong, unrelated small amount picked up) |
| 1054085314971 |
detail.tmall.com |
¥639 |
¥639 (this one happened to work) |
Root cause
clis/taobao/detail.js's func extracts the price with a blind regex scan
of document.body.innerText:
const pricePattern = /[¥¥]\s*(\d+(?:\.\d{1,2})?)/g;
const prices = [];
let m;
while ((m = pricePattern.exec(text)) && prices.length < 3) {
const p = parseFloat(m[1]);
if (p > 0.1 && p < 100000) prices.push(p);
}
if (prices.length > 0) {
results.push({ field: '价格', value: '¥' + Math.min(...prices) });
}
Two independent problems:
-
Wrong signal source, and the "obviously fake" value isn't reliably
detectable by its literal number. For 百亿补贴-channel items, Taobao's
server does not lazily inject the real price via a follow-up XHR — the
entire SSR payload already contains only the anchor/decoy value.
Confirmed via
window.__ICE_APP_CONTEXT__.loaderData.home.data.res.componentsVO.priceVO:
direct navigation to a gated item yields e.g.
{"price":{"priceText":"3999", ...}} with no extraPrice key at
all. The anchor number varies by item/campaign (9999 / 3999 /
4499 all observed) — it is whatever the seller/campaign configured as
the pre-subsidy list price, not a single well-known sentinel. The
structural signal (extraPrice present vs. absent) is reliable; the
numeric value is not. The real price only appears once the request
carries the session's umpChannel / fpChannelSig query params, which
are attached to the <a href> Taobao's own search-results page renders
for the item (see Fix below). No amount of selector tuning on the
current page recovers the real price — the page genuinely does not
contain it without the channel-authenticated navigation.
Caveat that makes this harder than "check for extraPrice": a plain
item that simply has no coupon/subsidy tier at all has the exact same
JSON shape (priceVO = { price: {...}, isNewStyle }, no extraPrice)
as a gated decoy render, and its price.priceText is the real
price (confirmed on id 883464195875, a small C2C listing, real ¥9.9,
no extraPrice, ever). So extraPrice absence alone cannot be treated
as "this is a decoy" — it can only be treated as "this page needs
verification". See Fix step 2 for how the patch resolves that
ambiguity instead of just value-matching harder.
-
Math.min over an unstructured 3-match scan is fragile even for
ordinary items. For id 800417757444, document.body.innerText
contains ¥253.73 (the real price), ¥449 (pre-coupon anchor), and an
unrelated ¥7 elsewhere on the page (observed to be some other on-page
amount, not further identified) — Math.min picks ¥7. This is a latent
bug independent of the decoy issue above.
Suggested fix
-
Prefer the structured price state over regex text-scraping.
window.__ICE_APP_CONTEXT__.loaderData.home.data.res.componentsVO.priceVO
exposes extraPrice.priceText (final after-coupon/after-subsidy price,
e.g. "券后"/"补贴后") and price.priceText (pre-discount anchor / decoy
fallback). Use extraPrice.priceText ?? price.priceText as the primary
source; fall back to the existing regex scan only when
__ICE_APP_CONTEXT__ isn't present at all (older/non-tmall templates).
-
Treat extraPrice absence as "unverified", not "decoy" — verify by
re-fetching through the item's own search-result link, then trust
whatever that authenticated page shows, unconditionally. Concretely:
- Search
s.taobao.com/search?q=<title prefix> using the item's title
(already extracted from state/DOM). A full raw title can return zero
hits, so try a few progressively shorter prefixes (e.g. 20 / 12 / 8
chars) until one surfaces a matching anchor.
- Match the candidate anchor by strict
id query-param equality,
not substring (href*="id=123" also matches an unrelated item whose
id is e.g. 1234567 — 123 is a numeric prefix of it).
- Navigate to that exact href (it already carries
umpChannel=bybtqdyh&fpChannelSig=... etc. — this is reused verbatim
from the page's own DOM, not computed or forged) and re-extract price
from state.
- Trust the post-recovery fetch outright, whether or not
extraPrice
appears on it. Some gated items resolve the corrected price
straight into price.priceText with no separate extraPrice tier at
all (confirmed on id 900757934496: decoy price.priceText "9999"
→ real "149" after recovery, extraPrice never appears on that item
even once unlocked). A fix that only accepts the recovered value when
extraPrice shows up, or only when the value is unchanged from
before, will still drop this case. If the item was never actually
gated (the 883464195875 case), the recovered fetch simply confirms
the same value the original fetch had — harmless, just an extra
round trip.
- This is a legitimate reuse of a URL the site serves to any logged-in
session that reaches the item via search; it does not bypass any auth
or anti-bot check, it just follows the same path a real user's click
would.
-
Never surface an anchor/decoy value as if it were a real price, and
never rely on a literal-value denylist to detect one. If the search
recovery link can't even be found (item not indexed under any tried
title prefix), omit the 价格 row entirely rather than falling back
to the original, unverified fetch. At that point there is no positive
way to tell "no coupon, price.priceText is real" apart from "gated,
price.priceText is a decoy we couldn't verify" — falling back to the
original value would silently risk leaking a decoy in exactly the cases
this whole patch exists to catch. A missed price (exit as if scraping
failed for this item) is always preferable to a wrong one.
I have a working drop-in replacement for clis/taobao/detail.js implementing all
three points above, verified against the 9-item regression fixture described in the
Verification section (opencli validate taobao/detail passes, exit code 0). Happy to
open a PR with it if that's welcome.
Verification
Do not read this section as "N items passed, therefore fixed." The
patch went through two rounds: an initial literal-value (=== '9999')
version passed a 7-item sample cleanly, then adversarial re-testing against
a different, independently-chosen sample immediately found two items
(1046289235959, 1039885607033) whose decoy was 3999/4499 and which
silently passed the literal-value check through as real data — plus a
third regression (883464195875, a genuine no-coupon item) where an
overly aggressive "extraPrice-absence = decoy" rule wrongly suppressed a
real price. The current patch (structural detection + unconditional trust
of the post-recovery fetch, described above) was specifically shaped to
survive both failure modes, not just to pass the original sample.
Machine-checked against well over a dozen items across three rounds of
adversarial re-testing, including at least 8 confirmed-gated 百亿补贴 items
spanning three different decoy anchor values (9999 / 3999 / 4499) and
several non-subsidy tmall + taobao.com C2C items (including ones with no
extraPrice tier at all, to guard the no-false-positive direction). All
resolve to the search-page reference price (or a small, live, same-day
price-drift delta — 百亿补贴 prices appear to be per-request A/B-test
bucketed, given the aplus_abtest query param on their channel links)
with zero decoy values of any kind leaked into the output. The 9-item
permanent regression fixture lives in scripts/verify_taobao_opencli.py
and currently passes with exit code 0. opencli validate taobao/detail
passes.
Fixed: recovery-failure fallback no longer leaks unverified prices
An earlier draft of this report flagged the following as an open gap: if
the recovery hop can't find the item's own search-result link under any
tried title prefix, the patch had no positive way to distinguish "no
coupon, price.priceText is real" from "gated, price.priceText is a
decoy we couldn't verify" — and at the time it fell back to the original,
unverified fetch, which meant a sufficiently obscure gated item could
still leak a decoy. This has been closed: when recovery can't locate a
channel href under any prefix, the patch now omits the 价格 row entirely
instead of falling back to the unverified value.
We got a live, unplanned confirmation of exactly this path: id
883464195875 (a small individual-seller phone-case listing, genuinely
priced at ¥9.9, not gated) was reliably recoverable via title search when
this patch was first written, then a few hours later dropped out of
s.taobao.com search results entirely (ordinary C2C listing churn — small
sellers' items move in and out of top search ranking with stock/sales, not
a site-defense behavior). All three title-prefix searches now return zero
matches for it. Its underlying page still genuinely shows price.priceText: "9.9" (still the real, non-decoy value — it was just never gated in the
first place), so the old "fall back to original" behavior would have
happened to still emit the correct ¥9.9 by coincidence here. The current
adapter instead correctly omits the 价格 row for this id — the right
call in general, since the adapter can no longer prove the value wasn't
a decoy once recovery fails, even though in this specific instance the
un-omitted value would have been correct.
Residual tradeoff (accepted, not a bug): this trades a small amount of
recall — a genuinely correctly-priced item can now surface with no price
at all if it happens to be temporarily unfindable via search — for the
much stronger guarantee that a decoy value can never leak through the
"recovery failed" path. Given the cost of a false price (data poisoning
downstream: corrupted price history, spurious price-drop alerts) versus
the cost of a missed price (one skipped scrape, retryable next cycle),
this is the correct tradeoff for a price-tracking use case. Machine
verification: scripts/verify_taobao_opencli.py's fixture was
swapped from 883464195875 to a currently-stable equivalent
(1001997074113, also no-extraPrice, also taobao.com C2C) specifically
because the original anchor's search-findability is no longer guaranteed
to be stable day-to-day — its unpredictability now demonstrates the omit
path rather than the "boundary preserved" path, so it stopped being a
useful fixed regression anchor for that specific behavior.
Environment
clis/taobao/detail.jsopencli taobao whoami→logged_in: true)Summary
opencli taobao detail <id>returns a decoy price instead of the real pricefor items whose detail page lives on
detail.tmall.comand is servedthrough Taobao's "百亿补贴" (Billion Subsidy) channel. The decoy is not a
scraping artifact picked up by mistake — it's a real anti-scrape anchor
value Alibaba's own server bakes directly into the page's SSR hydration
state (
window.__ICE_APP_CONTEXT__) when the request doesn't carry asession-scoped channel token.
Important correction from an earlier draft of this report: the decoy
value is not always the literal
¥9999. Adversarial re-testing against awider item sample found decoys of
¥3999,¥4499, and others — anyround-number anchor the seller/campaign happens to configure. A fix that
pattern-matches a specific literal value (e.g.
price === '9999') willsilently pass these through as if they were real prices. The only reliable
signal is structural: the real, final price lives in
componentsVO.priceVO.extraPriceand that key is absent from the SSRpayload on a gated render, regardless of what number ends up in the
pricefallback field. A second, narrower bug in the same function alsocauses wrong prices on ordinary (non-subsidy) tmall items in some cases.
Reproduction
Search-page price for the same id that day: ¥639.
detailprice (before fix)Root cause
clis/taobao/detail.js'sfuncextracts the price with a blind regex scanof
document.body.innerText:Two independent problems:
Wrong signal source, and the "obviously fake" value isn't reliably
detectable by its literal number. For 百亿补贴-channel items, Taobao's
server does not lazily inject the real price via a follow-up XHR — the
entire SSR payload already contains only the anchor/decoy value.
Confirmed via
window.__ICE_APP_CONTEXT__.loaderData.home.data.res.componentsVO.priceVO:direct navigation to a gated item yields e.g.
{"price":{"priceText":"3999", ...}}with noextraPricekey atall. The anchor number varies by item/campaign (
9999/3999/4499all observed) — it is whatever the seller/campaign configured asthe pre-subsidy list price, not a single well-known sentinel. The
structural signal (
extraPricepresent vs. absent) is reliable; thenumeric value is not. The real price only appears once the request
carries the session's
umpChannel/fpChannelSigquery params, whichare attached to the
<a href>Taobao's own search-results page rendersfor the item (see Fix below). No amount of selector tuning on the
current page recovers the real price — the page genuinely does not
contain it without the channel-authenticated navigation.
Caveat that makes this harder than "check for extraPrice": a plain
item that simply has no coupon/subsidy tier at all has the exact same
JSON shape (
priceVO = { price: {...}, isNewStyle }, noextraPrice)as a gated decoy render, and its
price.priceTextis the realprice (confirmed on id
883464195875, a small C2C listing, real ¥9.9,no
extraPrice, ever). SoextraPriceabsence alone cannot be treatedas "this is a decoy" — it can only be treated as "this page needs
verification". See Fix step 2 for how the patch resolves that
ambiguity instead of just value-matching harder.
Math.minover an unstructured 3-match scan is fragile even forordinary items. For id
800417757444,document.body.innerTextcontains
¥253.73(the real price),¥449(pre-coupon anchor), and anunrelated
¥7elsewhere on the page (observed to be some other on-pageamount, not further identified) —
Math.minpicks¥7. This is a latentbug independent of the decoy issue above.
Suggested fix
Prefer the structured price state over regex text-scraping.
window.__ICE_APP_CONTEXT__.loaderData.home.data.res.componentsVO.priceVOexposes
extraPrice.priceText(final after-coupon/after-subsidy price,e.g. "券后"/"补贴后") and
price.priceText(pre-discount anchor / decoyfallback). Use
extraPrice.priceText ?? price.priceTextas the primarysource; fall back to the existing regex scan only when
__ICE_APP_CONTEXT__isn't present at all (older/non-tmall templates).Treat
extraPriceabsence as "unverified", not "decoy" — verify byre-fetching through the item's own search-result link, then trust
whatever that authenticated page shows, unconditionally. Concretely:
s.taobao.com/search?q=<title prefix>using the item's title(already extracted from state/DOM). A full raw title can return zero
hits, so try a few progressively shorter prefixes (e.g. 20 / 12 / 8
chars) until one surfaces a matching anchor.
idquery-param equality,not substring (
href*="id=123"also matches an unrelated item whoseid is e.g.
1234567— 123 is a numeric prefix of it).umpChannel=bybtqdyh&fpChannelSig=...etc. — this is reused verbatimfrom the page's own DOM, not computed or forged) and re-extract price
from state.
extraPriceappears on it. Some gated items resolve the corrected price
straight into
price.priceTextwith no separateextraPricetier atall (confirmed on id
900757934496: decoyprice.priceText"9999"→ real "149" after recovery,
extraPricenever appears on that itemeven once unlocked). A fix that only accepts the recovered value when
extraPriceshows up, or only when the value is unchanged frombefore, will still drop this case. If the item was never actually
gated (the
883464195875case), the recovered fetch simply confirmsthe same value the original fetch had — harmless, just an extra
round trip.
session that reaches the item via search; it does not bypass any auth
or anti-bot check, it just follows the same path a real user's click
would.
Never surface an anchor/decoy value as if it were a real price, and
never rely on a literal-value denylist to detect one. If the search
recovery link can't even be found (item not indexed under any tried
title prefix), omit the
价格row entirely rather than falling backto the original, unverified fetch. At that point there is no positive
way to tell "no coupon,
price.priceTextis real" apart from "gated,price.priceTextis a decoy we couldn't verify" — falling back to theoriginal value would silently risk leaking a decoy in exactly the cases
this whole patch exists to catch. A missed price (exit as if scraping
failed for this item) is always preferable to a wrong one.
I have a working drop-in replacement for
clis/taobao/detail.jsimplementing allthree points above, verified against the 9-item regression fixture described in the
Verification section (
opencli validate taobao/detailpasses, exit code 0). Happy toopen a PR with it if that's welcome.
Verification
Do not read this section as "N items passed, therefore fixed." The
patch went through two rounds: an initial literal-value (
=== '9999')version passed a 7-item sample cleanly, then adversarial re-testing against
a different, independently-chosen sample immediately found two items
(
1046289235959,1039885607033) whose decoy was3999/4499and whichsilently passed the literal-value check through as real data — plus a
third regression (
883464195875, a genuine no-coupon item) where anoverly aggressive "extraPrice-absence = decoy" rule wrongly suppressed a
real price. The current patch (structural detection + unconditional trust
of the post-recovery fetch, described above) was specifically shaped to
survive both failure modes, not just to pass the original sample.
Machine-checked against well over a dozen items across three rounds of
adversarial re-testing, including at least 8 confirmed-gated 百亿补贴 items
spanning three different decoy anchor values (9999 / 3999 / 4499) and
several non-subsidy tmall + taobao.com C2C items (including ones with no
extraPricetier at all, to guard the no-false-positive direction). Allresolve to the search-page reference price (or a small, live, same-day
price-drift delta — 百亿补贴 prices appear to be per-request A/B-test
bucketed, given the
aplus_abtestquery param on their channel links)with zero decoy values of any kind leaked into the output. The 9-item
permanent regression fixture lives in
scripts/verify_taobao_opencli.pyand currently passes with exit code 0.
opencli validate taobao/detailpasses.
Fixed: recovery-failure fallback no longer leaks unverified prices
An earlier draft of this report flagged the following as an open gap: if
the recovery hop can't find the item's own search-result link under any
tried title prefix, the patch had no positive way to distinguish "no
coupon,
price.priceTextis real" from "gated,price.priceTextis adecoy we couldn't verify" — and at the time it fell back to the original,
unverified fetch, which meant a sufficiently obscure gated item could
still leak a decoy. This has been closed: when recovery can't locate a
channel href under any prefix, the patch now omits the
价格row entirelyinstead of falling back to the unverified value.
We got a live, unplanned confirmation of exactly this path: id
883464195875(a small individual-seller phone-case listing, genuinelypriced at ¥9.9, not gated) was reliably recoverable via title search when
this patch was first written, then a few hours later dropped out of
s.taobao.comsearch results entirely (ordinary C2C listing churn — smallsellers' items move in and out of top search ranking with stock/sales, not
a site-defense behavior). All three title-prefix searches now return zero
matches for it. Its underlying page still genuinely shows
price.priceText: "9.9"(still the real, non-decoy value — it was just never gated in thefirst place), so the old "fall back to original" behavior would have
happened to still emit the correct ¥9.9 by coincidence here. The current
adapter instead correctly omits the
价格row for this id — the rightcall in general, since the adapter can no longer prove the value wasn't
a decoy once recovery fails, even though in this specific instance the
un-omitted value would have been correct.
Residual tradeoff (accepted, not a bug): this trades a small amount of
recall — a genuinely correctly-priced item can now surface with no price
at all if it happens to be temporarily unfindable via search — for the
much stronger guarantee that a decoy value can never leak through the
"recovery failed" path. Given the cost of a false price (data poisoning
downstream: corrupted price history, spurious price-drop alerts) versus
the cost of a missed price (one skipped scrape, retryable next cycle),
this is the correct tradeoff for a price-tracking use case. Machine
verification:
scripts/verify_taobao_opencli.py's fixture wasswapped from
883464195875to a currently-stable equivalent(
1001997074113, also no-extraPrice, also taobao.com C2C) specificallybecause the original anchor's search-findability is no longer guaranteed
to be stable day-to-day — its unpredictability now demonstrates the omit
path rather than the "boundary preserved" path, so it stopped being a
useful fixed regression anchor for that specific behavior.