Operator drove the expired row 1000000155 (res 158023, 93 RW) in FIFA 17 with the candidate deployed: the row was still actionable, Return to Club was offered, and it worked — "the card is back in my club". Server-verified durable afterwards, which is what a fresh session reconstructs: /tradePile total 0 with zero rows, /tradePile/counts all zero, the card present in /club (1965 -> 1966 items, itemState "free"), zero duplicate ids, no stale active listing anywhere in the store (both rows cancelled), and the ended auction projecting as `closed` (4) on /trade/status. Fix A intact: zero `inactive` rows. So `listFS` -> `forSale` is protocol-correct AND behaviour-preserving on the path that matters, and `listFS` is gone from production serialization. Also worth recording what the result rules out: CARD_OFFERSTATE is NOT a gate term that requires -1. Every actionable row we had ever seen carried itemState -1, which looked like a possible client rule; it was a coincidence of our own invalid token. An expired row decoding CARD_OFFERSTATE = 5 stayed actionable. Deliberately NOT claimed: anything about the Flash action gate itself. STATE and the RESERVEDPRICE/MAX_CREDITS pair are untouched and still confounded, so the gate remains Category C / STRONGLY SUPPORTED / not proven, and AVM1 disassembly of tradepile.isInActiveAuction is still the separate next investigation. One gap left open honestly: no ACTIVE seller row existed during the session, so active-row rendering under `forSale` is unverified. /transfermarket has always emitted `forSale` on active rows, so it is expected-safe, but it has not been seen.
36 KiB
FIFA 17 Transfer Market — wire findings
Reverse-engineering record for the FIFA 17 UTAS transfer-market surface, kept so future agents do not reopen settled questions or re-guess enum spellings.
Every claim carries a confidence tag:
| Tag | Meaning |
|---|---|
| CONFIRMED | Observed from our own FIFA17.exe client or live host capture |
| FIFA17-HISTORICAL | Supported by contemporaneous FIFA 17 implementations (lorenzh/fut-api, futapi/fut v0.2.18 — the last pre-FIFA-18 release) |
| INFERRED | Best explanation, not directly captured |
| UNKNOWN | Requires instrumentation; do NOT implement from guesswork |
Authority reminder: FIFA 17 field names, enum spellings, sentinel ids and empty-state shapes come from captures or the Python oracle — never from a modern FUT toolkit. Later-FIFA API drift is a known hazard, and reversing a container type or inventing an enum is the documented client-freeze class.
The auction record (auctionInfo[])
What we emit today, on /tradePile, /trade/status and market browse:
{
"tradeId": 1000000097,
"itemData": { "...full shaped card...": "", "itemState": "listFS" },
"tradeState": "active",
"buyNowPrice": 15000,
"startingBid": 150,
"currentBid": 0,
"offers": 0,
"bidState": "none",
"expires": 3600,
"tradeOwner": true,
"sellerId": 33068179,
"sellerName": "CAGE",
"sellerEstablished": 1,
"watched": false,
"coinsProcessed": 0
}
| Field | Confidence | Note |
|---|---|---|
tradeOwner (bool) |
FIFA17-HISTORICAL | Exists in FIFA 17 auctionInfo. That it is the Actions-panel gate is UNKNOWN pending live confirmation. |
sellerId |
FIFA17-HISTORICAL exists; type numeric is INFERRED | Set to the configured persona so it agrees with tradeOwner. Never baked in. |
sellerName |
CONFIRMED it must be the player | fut_account.py annotates the persona property as "Blaze PDTL.DSNM / LSX GetProfileResponse Persona / UTAS sellerName". EA's "EASFC" here is wrong for an own listing. |
offers |
FIFA17-HISTORICAL | 0 valid for active/unbid. |
bidState: "none" |
FIFA17-HISTORICAL | Valid for active/unbid. Other observed concepts: highest, buyNow. Do NOT "fix" this. |
expires |
FIFA17-HISTORICAL | SECONDS REMAINING, not an epoch. Historical durations: 3600, 10800, 21600, 43200, 86400, 259200. |
itemData.itemState: "listFS" |
UNKNOWN | Plausible and unchanged. Public FIFA 17 material gives no trustworthy enumeration. Do not guess replacements — capture. |
itemData.untradeable |
FIFA17-HISTORICAL field; our blanket false is INFERRED |
See "Known debt" below. |
marketDataMinPrice / marketDataMaxPrice |
do NOT add | These entered the public parser only after its FIFA 18 migration. |
Why the differential could not catch the missing fields
Our record's key set was identical to the Python oracle's, so field-for-field
parity was green. The oracle omits tradeOwner / sellerId / offers as well,
because its remove flow was never driven by a real client either — the only
historical live datapoint is a counts-tile bug. Oracle parity is therefore
necessary but not sufficient for any flow the oracle never actually served.
The differential now asserts we cover every oracle key AND that our extra keys are
exactly {offers, sellerId, tradeOwner}, so the deliberate superset is pinned
while a new unexplained divergence still fails.
Routes
| Route | Confidence | Behaviour |
|---|---|---|
GET …/trade/status |
CONFIRMED the client polls it continuously | Live auction-state refresh. It previously fell through starts_with("trade") into the buy/view arm, where the tail has no numeric id, so every poll returned {"auctionInfo": []}. Now a real handler: optional tradeIds filter, else the whole active pile. Unknown ids are absent, never an error. |
DELETE /ut/game/<sku>/trade/<id> |
FIFA17-HISTORICAL | The spelling contemporaneous FIFA 17 clients use, no body, no meaningful response body. Previously landed in the buy/view arm and silently cancelled nothing while returning 200. Now maps to MarketCancel. |
DELETE /ut/delete/game/<sku>/trade/<id> |
CONFIRMED (oracle) | The oracle's spelling; retained because the differential exercises it. Whether FIFA17.exe ever uses it is UNKNOWN. |
POST …/auctionhouse |
CONFIRMED | List for sale. The client sends only itemData.id; the server resolves wire id → Core instance → card_id/resourceId and enforces ownership. |
GET …/tradePile/counts |
INFERRED | Five scalar ints (count, maxAuctionsAllowed, offered, selling, sold); a DISTINCT deserializer from /tradePile. Exact FIFA 17 semantics of count (active auctions vs whole pile) is UNKNOWN — we report active auctions and deliberately did NOT speculate. |
PUT …/item (move) |
FIFA17-HISTORICAL | `{"itemData":[{"pile":"trade" |
Pile encoding
- MOVE commands take a string pile (
"trade","club") — FIFA17-HISTORICAL. - Returned
itemData.pileis documented numeric in FIFA 17 auction data — FIFA17-HISTORICAL. - The numeric mapping is UNKNOWN. Do not unify the two representations, and do
not derive a mapping from unrelated
pileSizekeys.
Q2 — Transfer List item that is not currently auctioned
A real FUT state: an item in the Transfer List with no active auction (freshly moved, or expired unsold). CONFIRMED to exist as a concept (the external hub spec §27, and move-vs-list being separate operations).
Its wire representation is UNKNOWN: tradeId 0 / omitted / null, tradeState
value or omission, and itemData.itemState are all unestablished.
Consequence, and the reason this matters: our /tradePile renders only active
listings, so keying the /club exclusion on the trade pile stranded 4 cards
in no screen at all (hidden from the club, absent from the Transfer List).
Commit f2c4927 keys exclusion on the active listing instead, which is
self-healing. That is a workaround, not fidelity — the faithful model needs the
unlisted state represented.
Required capture (four states, full structural diff, not just a shortlist):
A. moved Club -> Transfer List, NEVER listed
B. actively listed
C. listing expired unsold
D. listing sold
Diff at least: tradeId, tradeOwner, tradeState, bidState, expires,
offers, currentBid, startingBid, buyNowPrice, sellerId, sellerName,
itemData.id, itemData.itemState, itemData.pile, itemData.untradeable.
Do NOT drop the unlisted state from the model just because its encoding is unknown.
Deferred, with reasons
- 5% transfer tax — INFERRED architecture only: auction closes → Core
settles → seller credited gross × 0.95, with
auctionInfocontinuing to carry gross. No trustworthy FIFA 17 field namedtax/netPrice/sellerProceedswas recovered, and no separate settle operation. Not blocking; do not couple settlement to clearing the sold auction without a capture. - Bid / Transfer Targets — not implemented. Watched / active bid / winning / outbid / won / expired are distinct states and must not collapse to a flat list.
- Unassigned — FIFA 17 had a dedicated Unassigned service; the exact FIFA 17
URL is UNKNOWN. Our 29-item
purchasedpile is this state and is currently rendered inside/club. Do not manufacture a route from a modern toolkit. - Match CREATE / READY / PLAY — UNKNOWN and explicitly not portable from public FUT web-app work (the web app could not start matches). Instrument the real client from Play Match to kickoff before implementing.
Known debt
shape_item reports untradeable: false for every owned instance. Correct
today (Core models no untradeable items) and necessary — a hardcoded true greyed
out both list buttons — but it will misrepresent SBC / promo / loan rewards once
those exist. untradeable belongs on the owned-item instance as authoritative
state, not inferred from definition, resourceId or rarity.
MEASURED in the live client — 2026-08-17
Read out of the running FIFA17.exe (pid-resolved, CardsDLL slide proven against
the on-disk FNV prologue) with fifa17-recon/tools/trade_gate_probe.py, which
extends gate_byte_probe.py to vtable slot +0x270 as the transfer-market
analysis asked for. Read-only: /proc/<pid>/mem O_RDONLY + pread.
| Gate | Python era (2026-08-06) | Now | Owner |
|---|---|---|---|
IS_TRADING_ENABLED model+0x1fd2e (slot +0x270) |
0 | 1 | settings struct +0x28; was zeroed by userInfo.feature.trade |
TRADE_PILE_SIZE model+0x1fd1c |
0 | 100 | userMassInfo.pileSizeClientData key 2 |
watch-list size model+0x1fd20 |
0 | 50 | same member, key 4 |
storeEnabled model+0x1fd2f |
1 | 1 | control |
IS_FRIENDLY_SEASON / IS_DRAFT_MODE / packOpeningAnimation |
1 | 1 | controls |
CONFIRMED: every CardsDLL-supplied input the transfer-market analysis named as a
blocker is now open. The Rust host does this by construction — it emits
userInfo.feature as {} (no trade member, so the kill switch at 0x180174f19
never arms: it fires only when atom 0x330 inside 0x11c parses as exactly 1) and
it already sends pileSizeClientData keys 2 and 4. Serving tradingEnabled: 1 in
the settings configs array would NOT have worked, because that tail runs after
every member is parsed and would overwrite it.
What this rules out
The Transfer List Actions panel not opening on an own listing is therefore not:
- an ownership field — FIFA 17's auctionInfo has no
tradeOwner/sellerIdatom; IS_TRADING_ENABLED,TRADE_PILE_SIZEor the watch-list size — all measured open;- the cancel route —
DELETE ut/delete/{ns}/trade/{tradeId}is the PE's spelling and is what we serve; tradeState/bidState/expiresspellings — all three are the PE's own vocabularies and values.
Per the analysis's own falsifier ("if the byte reads 1 and the screen still refuses, the exe-side predicate has a term we have not enumerated"), the remaining term is exe-side UI script, which CardsDLL does not own and the server cannot set. Status: UNKNOWN, and it is now the narrowest it has ever been.
Confirmed fidelity bug found on the way
expires was a frozen 3600 on every poll, so the client's live countdown never
moved and an auction could never run out. Now derived from created_at + duration
(duration taken from the ISStart body), clamped at 0, with an aged-out active
listing projecting as expired/none — FIFA 17's relistable state. Verified live:
the standing listing correctly reads expires: 0 once past its hour.
RESOLVED against the live client — 2026-08-17 (supersedes the tags above)
Driven by the real FIFA 17 client end to end: list → expire → relist → active, with the operator confirming each UI state. Live-client behaviour OUTRANKS both the Python oracle and contemporaneous Web App implementations wherever they disagree.
tradeOwner — do not re-litigate this
tradeOwner exists in the FIFA 17-era FUT API:
FIFA17-HISTORICAL
tradeOwner required by FIFA17.exe Transfer List Actions:
DISPROVEN for the current client path
It is absent from the twelve atoms FIFA 17's auctionInfo deserializer
(0x18013e410) reads, so the client value-SKIPs it at 0x180135ff0. It was
implemented, deployed, observed to change nothing, and REMOVED. A future agent
rediscovering the old Web App sources will find this note before spending
deployments on it again.
What actually gated the Actions panel — all lifecycle/state bugs
CONFIRMED — FIFA17.exe live client
Own Transfer List interactivity does NOT depend on tradeOwner in this path.
trade/status polling is LOAD-BEARING for Transfer List state. The tail carries no
numeric id, so it fell through the `trade…` buy/view arm and answered every poll
with an empty auctionInfo. Route is ISVIEWTRADE:
GET ut/{ns}/trade/status?tradeIds=a,b,c
auctionInfo.expires is SECONDS REMAINING *and must evolve with wall-clock time*.
A structurally valid but FROZEN value breaks lifecycle behaviour: the auction never
ages into expired/relistable, which is the state where Re-list appears.
Relisting must PERSIST. FIFA 17 relists by re-sending ISStart (POST /auctionhouse)
for an item that already has a listing row, so the primary-key conflict IS the
relist. Swallowing it as success left the stale expired row intact and produced a
client-visible lifecycle failure behind an HTTP 200.
Known-good own active auction (frozen fixture)
Captured at docs/evidence/market-lifecycle-2026-08-17/, including
_index.json._countdown_proof which records expires decrementing (frozen: false) so the live clock is machine-checkable rather than asserted in prose.
tradeState = active
bidState = none
expires = decrementing (seconds remaining)
sellerName = CAGE
auction record = 12 atoms
response envelope = 4 members (auctionInfo, credits, duplicateItemIdList, total)
ISViewTrade body = 2 members (auctionInfo, credits)
pricelimits = BARE ARRAY (container type is load-bearing)
Do not "improve" this shape without a live-client retest.
The development rule this established
A response can be structurally plausible, pass differential parity, and render
perfectly while still being behaviourally wrong, because FIFA expects an evolving
server-side state machine rather than a static object that looks like one. The
frozen expires is the canonical example: every field was the right name, type and
vocabulary, and the feature was still broken.
INVESTIGATION CLOSED — active own auction is not seller-actionable
Do not reopen without new direct FIFA17.exe evidence contradicting the lifecycle below. The correct FIFA 17 lifecycle is:
ACTIVE AUCTION tradeState=active, expires>0/counting down
-> seller CANNOT withdraw it through Transfer List actions
-> the item is not seller-actionable while the auction runs
EXPIRED UNSOLD tradeState=expired, expires=0
-> the item becomes actionable again
-> relist / return-to-club / other expired-item actions
Confidence tags
CONFIRMED — live FIFA17.exe:
active listing with ticking expires is NON-selectable
expired listing IS selectable
relisting makes it active and therefore non-selectable again
no cancel request is ever emitted by the client
expires must advance with the wall clock
no button prompt is offered on the Transfer List for an active auction
CONFIRMED — local RE corpus:
every CardsDLL-side prerequisite passes (IS_TRADING_ENABLED=1,
TRADE_PILE_SIZE=100, watch-list=50, item+0x49 tradeable)
MAY_BE_REMOVED is a CONSTANT 1 — it cannot be the gate and the server
cannot move it
the eight-flag array FUN_18003e370 publishes is the CLUB-CARD Actions menu
and contains NO transfer-auction cancellation flag
the auction parser is limited to the known twelve atoms
HISTORICAL FUT / FIFA17-era:
active auctions are committed until sale or expiry
expired Transfer List items expose relist/return actions
FIFA 17 trading guidance tells players to relist once auctions expire
UNKNOWN, and no longer required for backend fidelity:
the exact Flash/ActionScript branch that makes active cards non-selectable
Explicitly out of scope now
Do NOT add auction fields, change itemState, revisit tradeOwner, probe
MAY_BE_REMOVED, add an active-auction cancel feature, or disassemble Flash in
order to make active auctions selectable. Three of those were already tried and
refuted; the rest are ruled out above.
Return-to-club transition (implemented)
A pile move to club now ENDS any active auction on that item. Without it the
pile reads club while the listing row stays active, so the card is filtered out
of /club (exclusion keys on active listings) AND still rendered in the Transfer
List — the move silently appears to do nothing. reserved (mid-sale) and sold
rows are never touched, so a card can never be both sold and returned.
Q2 — the unlisted transfer-list item, MEASURED 2026-08-17
The operator moved a card Club -> Transfer List without listing it
(PUT /ut/game/fifa17/item, no POST /auctionhouse). State captured immediately,
read-only. Fixture: docs/evidence/market-lifecycle-2026-08-17/.
Server truth at capture: trade pile held 6 items, of which 1 had an active auction and 5 were unlisted.
| Surface | Unlisted item | Actively-listed item (contrast) |
|---|---|---|
/tradePile auctionInfo |
absent | present, tradeState: active |
/tradePile/counts |
not counted (count 1, selling 1) |
counted |
/club |
present, clubPlayers unchanged at 1965 |
absent (excluded) |
itemData.itemState |
free |
listFS |
itemData.pile |
field not emitted at all | not emitted |
itemData.untradeable |
false |
false |
Finding: we do not represent the unlisted state anywhere on the wire. Such an item is byte-identical to a club item; only the host's own pile store knows it is in the trade pile, and nothing the client receives says so.
Still UNKNOWN (do not guess): the FIFA 17 encoding of that state. The PE
documents returned itemData.pile as NUMERIC with an unrecovered mapping, and
tradeState decodes through a closed table walk (active=1 inactive=2 expired=3 closed=4) where an unrecognised bidState is silently swallowed as none — so a
wrong enum yields a plausible-looking but WRONG UI. The corpus explicitly warns that
inactive decodes but "no client path treats it specially; do not emit it".
Open discriminator, needs the client only: after backing out of FUT and re-entering,
does the Transfer List still show an unlisted item? If it does not, the move is not
durable from the client's point of view and the state must be represented; if it
does, tradePile is auctions-only by design and there is nothing to fix.
Domain boundary: pile membership vs auction lifecycle
Two SEPARATE facts, but several mutations require a COORDINATED transition. Both limbo bugs this session came from conflating them, so this is modelled explicitly and covered by tests rather than patched route by route.
| Mutation | Pile | Auction |
|---|---|---|
| Club -> Transfer List | becomes trade |
none necessarily exists |
| list item | stays trade |
becomes active |
| clock runs out unsold | stays trade |
becomes expired (projection; no row mutated) |
| relist | stays trade |
active again, clock restarted, new prices |
| return expired item to Club | becomes club |
any active association MUST end |
| sold / reserved | — | MUST NOT be ended by a generic move-to-club |
Invariant, stated so it can be tested rather than remembered: an item must never be
simultaneously excluded from /club and absent from /tradePile. That is the limbo
state, and it is reachable in two ways, both now closed:
- Excluding by pile rather than by active listing (fixed in
f2c4927) — the pile can hold items with no auction, and/tradePilerenders auctions only. - Moving to
clubwhile leaving the auctionactive(fixed in4e31fb9) — the pile says club, the exclusion still fires, and the card shows in neither place.
Tests: returning_an_expired_listing_to_the_club_ends_its_auction,
a_pile_move_never_disturbs_a_sale_in_flight,
club_excludes_listed_items_and_paginates_the_visible_set.
Q2 representation — narrowed to ONE candidate by elimination (2026-08-17) — SUPERSEDED, see the final section
Re-entry discriminator result: CONFIRMED BUG. After fully leaving and re-entering
FUT, the active auction reconstructs correctly under LISTED ITEMS, but the unlisted
TRANSFER LIST section is empty — item 100000059 does not survive. The
immediate post-move visibility was client-local only, so our representation cannot
durably reconstruct trade-pile membership.
The representation is now pinned by elimination over PROVEN facts, not chosen:
- Only one route can own a trade-pile list. The complete CardsDLL route table
(
.rdata 0x18021df80, 45 routes + 3 empty admin slots) is dumped verbatim atdocs/evidence/market-lifecycle-2026-08-17/cardsdll-route-table.txtviafifa17-recon/tools/route_table_dump.py(static, read-only, VA→offset resolved through the real PE section table). Row 30ut/%s/tradePileis the ONLY trade-pile route. There is no trade-pile items route. - That route carries only auction records.
FutGetTradePile(0x180170810) deserializes the shared IS-list body (0x18013e7f0), whoseauctionInfoelements go through0x18013e410— the twelve-atom auction record. pilecannot be set from the wire.pile(atom 0x226) has NO arm in the item deserializerFUN_18013fe00(checked in all four dispatch forms);item+0x60is assigned by the OWNING LIST, reading 1 for every/clubitem and 6 for every/purchaseditem. So membership is conferred by arriving in the list, never by a field we can add.- Of the twelve atoms, only
tradeStatecan express lifecycle. tradeState's vocabulary is closed and has exactly one unused value. The table walk at0x180229e40decodesactive=1 inactive=2 expired=3 closed=4, anything else-1.active,expiredandclosedare all already spoken for by the observed lifecycle.
Therefore an unlisted trade-pile item can only be an auctionInfo record with
tradeState: "inactive". That is the sole encoding the client's own parser can
accept for "in the pile, no auction" — reached by elimination, not invention.
Status: INFERRED-BY-ELIMINATION, not yet CONFIRMED. One binary question remains,
and it is about client rendering, not encoding: does the Flash Transfer List place
an inactive record in the unlisted section? The corpus's warning that "inactive
decodes but no client path treats it specially" was established for the market
PRESENTATION function (flagA/flagB), where inactive is indeed unremarkable —
that is consistent with, and does not contradict, using it for list membership.
Ruled out on evidence, do not retry: adding itemData.pile (inert, no deserializer
arm), a second route (none exists), and tradeId: 0 / invented itemState values
(unnecessary — tradeState alone carries the distinction).
Acceptance test for whatever lands
move item Club -> Transfer List
leave FUT entirely
re-enter FUT
item appears in the unlisted TRANSFER LIST section
item can be returned to Club
no auction exists unless explicitly listed
Plus the active lifecycle must remain unchanged: active listing under LISTED ITEMS,
expires counting down, active non-selectable, expiry making it actionable, relist
working.
Revised domain invariant
Core pile membership is NOT equivalent to /tradePile visibility. Core ownership/pile
state and auction lifecycle are separate authoritative facts; the wire may expose them
through different FIFA 17 resources. A transition is complete only when a fresh FIFA
session can reconstruct the same user-visible state — re-entry is the acceptance
test, not the immediate post-mutation response.
Q2 — LIVE-CONFIRMED: tradeState: "inactive" is the unlisted representation — SUPERSEDED, see the final section
Operator-observed in the real FIFA 17 client, 2026-08-17, with the bounded one-item
probe (OPENFUT_FIFA17_UNLISTED_PROBE). Fixture:
docs/evidence/market-lifecycle-2026-08-17/inactive-row-live-confirmed.json.
The observed inactive UI contract
| Wire | Client presentation |
|---|---|
tradeState: "inactive" |
row placed under the right-hand TRANSFER LIST section, NOT under LISTED ITEMS |
startingBid: 0 |
Start Price = 0 |
buyNowPrice: 0 |
Buy Now Price = - (absent) |
currentBid: 0 |
Current Bid = - (absent) |
expires: 0 |
Time Remaining = - (no timer) |
bidState: "none" |
no bid presentation |
itemData.itemState: "free" |
renders as a normal owned card |
An active row in the same body continued to render separately under LISTED ITEMS,
so the two sections are driven by tradeState and the experiment did not disturb the
known-good active path.
Token provenance: dumped from the client's own {const char*, int} table at
0x180229e40 (active=1 inactive=2 expired=3 closed=4) — see
cardsdll-vocab-tables.txt and fifa17-recon/tools/vocab_dump.py. Nothing here was
guessed.
Regression test: market::tests::unlisted_candidate_is_a_non_active_pile_row pins the
whole tuple, plus that the row is never emitted for an item outside the trade pile
and never duplicates a real auction
(unlisted_candidate_never_duplicates_a_real_auction).
Expired -> Club coupled transition, verified
Operator returned the naturally-expired Ronaldo to the club. Server state after:
listing 1000000097 state = cancelled (the coupled transition fired)
pile Ronaldo = club
/tradePile Ronaldo absent
/club Ronaldo present, itemState free
hub clubPlayers 1964 -> 1965, auctionCount 1
counts {count 1, selling 1, sold 0}
All of that is durable store state rather than a client-local view, so it survives a session boundary by construction. Pending the operator's final exit/re-enter confirmation before this is tagged CONFIRMED.
Counts observations so far (semantics still UNCHANGED and unresolved)
| State | count |
selling |
sold |
|---|---|---|---|
| 1 active | 1 | 1 | 0 |
| 2 active | 2 | 2 | 0 |
| 1 active + 1 inactive (probe) | 1 | 1 | 0 |
| unlisted pile items (no probe) | not counted | not counted | 0 |
So count currently tracks AUCTION entries, not total Transfer List membership. Do
not change this until the full state set (empty / unlisted / active / expired / sold /
mixed) has been observed — it remains an open question whether FIFA 17 expects
count to include non-active pile members.
Q2 status downgrade + the "Expired" differential EXPLAINED (2026-08-17) — SUPERSEDED, see the final section
Phase C was a PARTIAL pass. Corrected conclusions:
tradeState "inactive" = CONFIRMED section/lifecycle discriminator
(rows persist across a fresh session and land under
TRANSFER LIST, not LISTED ITEMS)
complete ACTIONABLE unlisted representation = was UNKNOWN; see below
The visual differential, and its cause
| one-item probe | generalized rows | |
|---|---|---|
| Start Price | 0 | 0 |
| Buy Now / Current Bid | - |
- |
| Time Remaining | - |
Expired |
Cause: route coverage, not a field. /tradePile advertised the unlisted
tradeIds, but GET …/trade/status (ISVIEWTRADE) answered them from the market store
ONLY — and an unlisted pile member has no listing row, so the poll returned an empty
auctionInfo. Observed live as route=market-status requested=1 returned=0 repeating
for the row the operator had selected (tradeIds=1000000122), while the same id was
present in /tradePile.
The client polls /trade/status for the row it is displaying, and an empty answer
degrades it: Time Remaining renders Expired and no actions are offered. The earlier
probe showed - simply because the client had not yet polled that id — the logs at
the time show only tradeIds=1000000097. So expires, tradeState,
itemData.itemState and pile were all innocent, and NO field was changed.
This is the same defect class as the original trade/status bug: a route the client
polls being answered with an empty set. The corpus predicted it — tradeId must
resolve across /transfermarket, /tradePile, /watchList AND /trade/status. We
had stability but not coverage.
Fix: both routes now share ONE pile enumeration (Server::resolve_trade_pile), so an
id advertised by /tradePile always resolves on /trade/status. Verified live: the
six inactive ids went from returned=0 to returned=6. /trade/status still answers
only the ids actually asked about, and a real auction always wins over an inactive row
for the same id.
Regression test: trade_status_resolves_the_unlisted_ids_tradepile_advertises.
Investigations NOT needed as a result
itemData.itemState and numeric itemData.pile were queued for PE recovery on the
assumption the encoding was incomplete. The cause was route coverage, so neither was
touched and neither is implicated. If the actions still do not appear, those remain
the next candidates — and itemState's vocabulary should be dumped statically with
fifa17-recon/tools/vocab_dump.py (already validated against the tradeState table)
rather than guessed.
Q2 — RETRACTED, and CLOSED as unrepresentable (2026-08-17, RE of the FUT front-end)
Everything in the two Q2 sections above is superseded. The tradeState:"inactive"
projection has been removed from GET …/tradePile and …/trade/status, and the
UnlistedCandidate machinery deleted. Live after the fix: total 7 → 1, only the real
auction, inactive rows 0, coins unchanged, and all six former rows still present in
/club (invariant intact).
Why the earlier conclusion was wrong
It was LIVE-CONFIRMED that the row renders. That was never in doubt, and it is
not the same claim as the row being usable. The rows were never actionable, and could
not have been:
| Fact | Confidence |
|---|---|
tradeState reaches ONE native branch, cmp …,0x4 at 0x18013e619 ("is it closed?"). inactive(2) and expired(3) take the same edge → bit-identical flagA/flagB. Exhaustive 22-site census of [reg+0x88] reads across the PE. |
CONFIRMED |
tradeState is handed to the movie verbatim as the Flash property STATE (publisher 0x1801bf030); the action gate is in the APT/ActionScript FUT front-end, not CardsDLL. |
CONFIRMED |
The trade-pile movie class partitions rows with getCardsInAuction/isInActiveAuction (traces initPile() - IN AUCTION: / - NOT IN AUCTION:), and only auction rows reach PreCheckCardOptions → handleTradeCardAction. |
CONFIRMED (interned AS symbols + trace strings, live) |
| A client rule excludes non-auction pile rows from the action path (Category C). | STRONGLY SUPPORTED |
The gate term is STATE == 2. |
NOT PROVEN — STATE, CARD_OFFERSTATE and the RESERVEDPRICE/MAX_CREDITS pair all differ together in every row we can observe |
In the shipped game, unlisted pile rows were a CLIENT-SIDE transient built by the movie from a TO_TRADEPILE Flash message (FUN_1801be6a0), never server-delivered. |
STRONGLY SUPPORTED |
This fires the corpus's own pre-registered falsifier E3
(plan-2026-08-06-transfer-market.md:368-373): the native predicates all pass
(IS_TRADING_ENABLED=1, TRADE_PILE_SIZE=100, tradeable=1) and an expired row in
the same section IS actionable, so "the Flash layer has its own gate and CardsDLL is no
longer the right place to look" is now an observation.
The corpus said so before any of this was written, at
plan-2026-08-06-transfer-market.md:731-733: "inactive decodes but no client path
treats it specially; do not emit it." The projection was built anyway, and the
earlier reasoning above (that the warning "was written about the PRESENTATION
function") was motivated rather than evidenced.
Consequence for the product: a server cannot put an unlisted item on the Transfer
List in an actionable state. /tradePile carries only twelve-atom auction records and
pile has no deserializer arm, so anything rendered there must be a real auction.
Unlisted trade-pile membership is now internal state with no wire expression; those
cards live in /club, which is where the client can act on them.
Open: itemData.itemState: "listFS" is not a FIFA 17 token
Independent of the gate, and CONFIRMED: listFS occurs 0 times in
CardsDLL_Win64_retail.dll (md5 4de3493131d7d2ff7f8b360c5ac9b655) and 0 times in
4.26 GiB of live process memory; it decodes to −1 through FUN_180166660, observed
live on our listed row. FIFA 17's value for a listed item is forSale (5), from the
12-row table at 0x180229cc0. We emit listFS on /tradePile and /trade/status, and
so does the Python oracle (utas_server.py:3921) — which is exactly why the
differential never caught it: oracle parity is necessary but not sufficient.
Fix B — forSale PROMOTED, live-confirmed on the expired path (2026-08-17)
Shipped alone, then validated by an operator FIFA 17 session against the already-expired
row 1000000155 (res 158023, 93 RW).
| Claim | Status |
|---|---|
listFS is not a FIFA 17 token and decodes to −1 |
CONFIRMED (binary + 4.26 GiB process scan + live row) |
forSale is FIFA 17-native, value 5 |
CONFIRMED (12-row table at 0x180229cc0) |
An expired row carrying forSale is still ACTIONABLE, and Return to Club works |
LIVE-CONFIRMED — operator returned the card; server verified below |
| The result is durable (a fresh session reconstructs it) | CONFIRMED server-side: /tradePile total:0, /tradePile/counts all zero, card present in /club (1965 → 1966 items, itemState:"free"), zero duplicate ids, listing cancelled in the store and projected as closed (4) on /trade/status |
An ACTIVE seller row carrying forSale renders correctly |
NOT YET OBSERVED — no active listing existed during the session; /transfermarket has always emitted forSale on active rows, so this is expected-safe but unverified |
CARD_OFFERSTATE is the Flash action-gate term |
STILL UNPROVEN — this change does not test it, and the result does not bear on it |
What this rules out: CARD_OFFERSTATE is not a gate term that requires −1. An expired
row decoding CARD_OFFERSTATE = 5 remained actionable, so the earlier worry — that every
actionable row we had ever seen carried itemState = −1 — was a coincidence of our own
bad token, not a client rule. It narrows nothing else: STATE and the
RESERVEDPRICE/MAX_CREDITS pair are untouched and still confounded with each other.
Wire A/B (docs/evidence/market-lifecycle-2026-08-17/itemstate-forsale-ab.json): on the
same expired row, exactly one field changed — itemState listFS → forSale.
tradeId, tradeState, expires, startingBid, buyNowPrice, currentBid,
bidState, sellerName, sellerEstablished, watched, coinsProcessed, the atom
count (12), and the whole itemData card are byte-identical; coins unchanged; Fix A's
zero inactive rows intact.
Risk was lower than it first appeared, for a reason worth recording: the market SEARCH
route (/transfermarket) already emitted forSale, so the client has been decoding
forSale (5) on a live path all along — only the seller's own pile carried the invalid
token.
Differential consequence: market query tradePile moves from PARITY to
DIFFERENT-BY-DESIGN, pinning oracle == "listFS" and rust == "forSale" so the
divergence stays visible. That assertion previously demanded parity and passed while
both sides were wrong — the mechanism by which this defect survived every
differential run. Where the FIFA 17 binary contradicts the Python oracle, the binary
wins.
Revert is one line if the live A/B fails: both auction_record_as(l, "forSale") calls in
market.rs back to "listFS", rebuild, restart prod-host. On failure the rule is to
revert and go to the AVM1 gate work — NOT to compensate with another field change.
Claim boundary: this is protocol correctness of one token. It is not evidence about
the Flash decision tree; STATE, CARD_OFFERSTATE and RESERVEDPRICE/MAX_CREDITS
remain confounded.
Proving the gate (not yet done)
Disassemble the AVM1 action buffer of tradepile.isInActiveAuction (and
PreCheckCardOptions): either walk from the interned symbol at 0xb84d7498 to its APT
function object in the live process, or extract
external/ion_fut/screens/trading/tradepile from ui.sb + the cas archives offline.
Nothing else in reach separates STATE from CARD_OFFERSTATE from the price pair,
because all three differ together in every observable row.