Files
OpenFUT/openfut-utas-host/ROUTE_AUTHORITY.md
T
OpenFUT Agent 49c5185ae3 docs(utas-host): update economy cutover progress (readers + match writer landed)
Core API + purchase_items + entitlement import + host reader handlers
(credits/purchasegroup/userMassInfo) + match reward writer landed and tested;
classifier still unflipped (barrier pending BUY/pack-open/quick-sell item shaping,
market listing state, differential/concurrency/restart).
2026-08-13 19:32:45 +00:00

7.5 KiB

FIFA17 UTAS Route Authority (economy cutover gate)

Machine-auditable ownership of every FIFA17 UTAS route that touches the economy cluster. This is the deployment gate for the Rust economy cutover (R1/E1): before Rust economy authority is enabled, every row's Target must be reached and no Python (proxied) row may still write Core-owned state.

Cluster state = coins, owned inventory (items/purchased), unopened pack entitlements (unopenedPackIds). points has no writer (read-only). EASFC powFunds is a separate balance, out of cluster.

Legend: R = Rust/Core authoritative, P = Python proxied (oracle). Evidence lines refer to fifa17-recon/tools/{utas_server.py,fut_store.py}.

Writer routes (mutate cluster state)

Route Method Python handler Writes Current Target Core primitive
/ut/game/<sku>/match POST match_routerecord_match (fut_store 554) coins P R grant_reward
/store/transaction PUT store_buyopen_packspend (utas 3702) coins, purchased, packsOpened, nextItemId P R purchase_entitlement (+ redeem_entitlement)
/purchased POST purchased_itemsopen_pack+consume_unopened_pack+move_items (utas 3716) coins, purchased, unopenedPackIds, items, nextItemId P R redeem_entitlement
/ut/game/<sku>/item/<id> DELETE quick_sell_url_routequick_sell (utas 1234) coins, items, purchased P R sell_item
/ut/delete/game/<sku>/item POST quick_sell_routequick_sell (utas 1273) coins, items, purchased P R sell_item
/ut/game/<sku>/item PUT item_routemove_items (utas 1342) items, purchased P R redeem_entitlement/move (inventory-only)
/ut/game/<sku>/trade/<id> POST/PUT trade_route buy-now spend+add_items (utas 3895/3899) coins, items, nextItemId P R purchase_item (synthetic-seller mint)
/auctionhouse,/transfermarket POST auctionhouse_routelist_for_sale (utas 3865) listings, nextListingSeq P R listing-state (see note)
/ut/delete/game/<sku>/trade/<id> DELETE delete_trade_routeremove_listing (utas 3935) listings P R listing-state (see note)
/ut/game/<sku>/squad PUT squad_routesave_squad (utas 3430) squads (item refs) R (SquadReplace→Core) R Core squad tx (already migrated)

Note (market listings): listings/nextListingSeq are the user's own sale pile; the buyable auction inventory is synthetic (PACK_POOL-derived, not persisted). There is no sale-credit, expiry-return, or fee (audit §5). Listing/cancel move no coins and no ownership, so they are low-risk; a minimal durable listing store (or keeping the synthetic-only model) is the market slice's only decision.

Reader routes (emit cluster state; go STALE if Rust writes while these read Python)

Route Method Python handler Reads Current Target
/user/credits GET credits_route (utas 3765) coins, unopenedPackIds count P R (balance + entitlement count)
/userMassInfo GET massinfo currencies (utas 578) coins, points, record, items, unopenedPackIds, squad P (.squad overlaid R) R economy fields (coins/packs), squad already R
/store/purchasegroup GET store_catalogunopened_packs (utas 3614) unopenedPackIds P + R topology overlay R full-gen (catalog + SessionStore mode + Core entitlements)
/tradePile GET tradepile_route (utas 3911) items, listings, coins P R (reads Core inventory/balance/listings)
/hub,/tradePile/counts,/watchList GET hub_data/auction_counts/watchlist items, listings, coins P R
/club,/club/stats,/user/list,/clubUser GET club readers→items items R (/club Core-backed) / P others R

Writer → Core primitive map (Phase 2)

Python writer Reachable Core primitive (services::economy)
spend (pack buy leg) YES purchase_entitlement debit leg
open_pack YES purchase_entitlement + redeem_entitlement (buy→entitlement→open split)
consume_unopened_pack YES redeem_entitlement (consume-once)
move_items (purchased→club) YES inventory add within redeem_entitlement / move op
add_items (market mint) YES purchase_item (debit + mint)
quick_sell YES sell_item (remove + credit)
record_match (coins) YES grant_reward (credit)
new_item_id YES adapter numeric-id via openfut-identity (Core ids opaque)
list_for_sale / remove_listing YES listing-state (market slice)
grant_coins NO (dead) — drop
grant_unopened_pack NO (test-only) — drop
save_squad YES already Core-authoritative (SquadReplace)

Single-writer rule

Coins live only in Python fut_profile.json today (Core clubs.coins is a separate imported value). Because every coin reader (credits, userMassInfo, tradePile, market bodies) reads that same JSON, the coins cluster must flip readers and writers together — a partial flip desyncs the client's counter (audit "STALENESS RISK"). The coherent first cut is therefore the whole coins bundle: 4 writer routes + credits/userMassInfo/purchasegroup readers, all on Core, seeded by a one-time profile import into Core.

Proxied-route safety (R1 requirement: no unsafe YES)

After cutover, every remaining Python (proxied) route MUST have economy state touched = NONE. Routes with economy state touched != NONE are part of the migration cluster and MUST be Rust before R1. This table is the audit source; the host classify() is the enforcement point (NEVER BOTH).

Cutover progress (2026-08-13)

Landed (Core authority + transport + several handlers; classifier NOT yet flipped):

  • Core economy HTTP API (d32dc6e, bcc4f51): generic /economy/{balance, entitlements,purchase-entitlement,redeem-entitlement,sell-item,grant-reward, purchase-item,purchase-items}, server-side club resolution, atomic. Import now seeds unopenedPackIds→entitlements. Core 178 tests green.
  • Host CoreEconomy client (d240a61): typed reqwest, fail-closed, no Python fallback by contract.
  • Reader handlers: /user/credits (handle_credits), /store/purchasegroup full-gen (handle_purchasegroup, no Python body dependency), userMassInfo economy overlay (overlay_massinfo_economy). Invariant test: all three read one Core state.
  • Writer handler: /match reward (handle_match_end → Core grant_reward, oracle destroy_match_body shape).
  • Adapter policy mappers (181bd94): match reward, pack price.
  • All Core-backed, fail-closed (503, never Python), FakeEconomy-tested.

Not flipped: classify() still routes every economy route to Python. Per the single-writer rule the flip is one coherent barrier once ALL writers+readers are implemented and Core is seeded — a partial flip would desync coins.

Remaining before barrier: Store BUY, pack-open, quick-sell (need item-wire shaping via club_response shapers + openfut-identity numeric ids + pack-content policy); market listing durable state + list/cancel/buy; move-items metadata; Core-backed adapter live wiring (retire in-memory ProfileEconomy from prod path); openfut-economy-import CLI; differential harness + concurrency + restart; then the classifier barrier + no-Python-fallback classifier tests.