Files
OpenFUT/docs/FIFA17_TRANSFER_MARKET_WIRE.md
T
funman300 7f37b37be3 docs: capture the unlisted transfer-list state and model the pile/auction boundary
Q2 measured, not guessed. The operator moved a card Club -> Transfer List without
listing it (PUT /item, no POST /auctionhouse) and the state was captured read-only.

Finding: we do not represent the unlisted state on the wire AT ALL. Such an item is
byte-identical to a club item -- itemState `free`, no `pile` field emitted, still
returned in /club and counted in clubPlayers -- while an actively-listed item is
correctly excluded from /club and present in tradePile. Only the host's own pile
store knows the difference. Trade pile held 6 items: 1 listed, 5 unlisted.

The FIFA 17 ENCODING of that state stays UNKNOWN on purpose: returned itemData.pile
is numeric with an unrecovered mapping, and tradeState is a closed table walk where
an unrecognised bidState is silently swallowed as `none`, so a wrong enum produces a
plausible-looking but wrong UI. The corpus warns `inactive` decodes but no client
path treats it specially. One client-only discriminator is recorded instead.

Also models the domain boundary both limbo bugs came from: pile membership and
auction lifecycle are separate facts requiring coordinated transitions. States the
testable invariant -- an item must never be simultaneously excluded from /club and
absent from /tradePile -- with the two ways it was reachable and the commits that
closed each.
2026-08-17 20:01:51 +00:00

20 KiB
Raw Blame History

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.pile is 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 pileSize keys.

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 taxINFERRED architecture only: auction closes → Core settles → seller credited gross × 0.95, with auctionInfo continuing to carry gross. No trustworthy FIFA 17 field named tax/netPrice/sellerProceeds was 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 purchased pile is this state and is currently rendered inside /club. Do not manufacture a route from a modern toolkit.
  • Match CREATE / READY / PLAYUNKNOWN 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/sellerId atom;
  • IS_TRADING_ENABLED, TRADE_PILE_SIZE or 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 / expires spellings — 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:

  1. Excluding by pile rather than by active listing (fixed in f2c4927) — the pile can hold items with no auction, and /tradePile renders auctions only.
  2. Moving to club while leaving the auction active (fixed in 4e31fb9) — 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.