#!/usr/bin/env python3 """Scrape SAND's PlayFab Economy (catalog + prices + inventory) via the public REST API. Backend is Azure PlayFab (Economy v2). Auth is `LoginWithSteam`. Everything here is the documented PlayFab REST surface (https://learn.microsoft.com/gaming/playfab/), run from *outside* the game process — no BattlEye interaction. Two ways in: --steam-ticket do Client/LoginWithSteam to get a fresh SessionTicket + EntityToken --entity-token skip login, use an EntityToken you captured (e.g. from your MITM) The only constant you must supply is --title-id (the PlayFab TitleId). Get it from a live capture: `reverse/capture_hosts.py` prints it from the `.playfabapi.com` host. Modes (combine freely): --catalog Catalog/SearchItems -> every item with PriceOptions + DisplayProperties --inventory Inventory/GetInventoryItems + GetTransactionHistory (your wallet/items) --titledata Client/GetTitleData (config key/values, if any) Read-only endpoints only. Output -> extracted/playfab_.json, item ids resolved to names. Example: venv/bin/python reverse/playfab_scrape.py --title-id ABCDE --steam-ticket 14000000... --catalog """ import sys, os, json, argparse import requests ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) NAMES_PATH = os.path.join(ROOT, "extracted", "item_names.json") def load_names(): try: d = json.load(open(NAMES_PATH))["items"] return {k: (v.get("name") or k) for k, v in d.items()} except Exception: return {} class PlayFab: def __init__(self, title_id): self.title = title_id self.base = "https://%s.playfabapi.com" % title_id self.session_ticket = None self.entity_token = None self.s = requests.Session() self.s.headers["Content-Type"] = "application/json" def _post(self, path, body, headers=None): r = self.s.post(self.base + path, data=json.dumps(body), headers=headers or {}, timeout=30) try: j = r.json() except Exception: r.raise_for_status() raise if r.status_code != 200 or j.get("code") not in (200, None): raise RuntimeError("%s -> %s %s" % (path, r.status_code, j.get("errorMessage") or j.get("error") or j)) return j.get("data", j) def login_steam(self, ticket_hex, service_specific=False): body = {"TitleId": self.title, "SteamTicket": ticket_hex, "CreateAccount": False, "TicketIsServiceSpecific": service_specific} d = self._post("/Client/LoginWithSteam", body) self.session_ticket = d.get("SessionTicket") et = d.get("EntityToken") or {} self.entity_token = et.get("EntityToken") print(" logged in: PlayFabId=%s entity=%s" % (d.get("PlayFabId"), (et.get("Entity") or {}).get("Id"))) return d # ---- Economy v2 (Entity API; needs X-EntityToken) ---- def search_items(self): items, token = [], None while True: body = {"Count": 50, "Filter": "", "Search": ""} if token: body["ContinuationToken"] = token d = self._post("/Catalog/SearchItems", body, headers={"X-EntityToken": self.entity_token}) items += d.get("Items", []) token = d.get("ContinuationToken") print(" catalog: %d items so far..." % len(items)) if not token: break return items def inventory(self): out = {} out["items"] = self._post("/Inventory/GetInventoryItems", {"Count": 50}, headers={"X-EntityToken": self.entity_token}).get("Items", []) try: out["transactions"] = self._post("/Inventory/GetTransactionHistory", {"Count": 50}, headers={"X-EntityToken": self.entity_token}).get("Transactions", []) except Exception as e: out["transactions_error"] = str(e) return out def title_data(self): return self._post("/Client/GetTitleData", {}, headers={"X-Authorization": self.session_ticket}).get("Data", {}) def summarize_catalog(items, names): """Flatten each catalog item to {id, friendlyId, name, type, prices, displayProperties}.""" rows = [] for it in items: alt = {a.get("Type"): a.get("Value") for a in it.get("AlternateIds", [])} fid = alt.get("FriendlyId") or it.get("Id") prices = [] for po in (it.get("PriceOptions") or {}).get("Prices", []): legs = [{"currency": a.get("ItemId"), "currencyName": names.get(a.get("ItemId"), a.get("ItemId")), "amount": a.get("Amount")} for a in po.get("Amounts", [])] prices.append(legs) title = it.get("Title", {}) rows.append({ "id": it.get("Id"), "friendlyId": fid, "name": (title.get("NEUTRAL") or names.get(fid) or fid), "type": it.get("Type"), "contentType": it.get("ContentType"), "prices": prices, "displayProperties": it.get("DisplayProperties"), }) return rows def main(): ap = argparse.ArgumentParser() ap.add_argument("--title-id", required=True) ap.add_argument("--steam-ticket") ap.add_argument("--entity-token") ap.add_argument("--ticket-service-specific", action="store_true") ap.add_argument("--catalog", action="store_true") ap.add_argument("--inventory", action="store_true") ap.add_argument("--titledata", action="store_true") args = ap.parse_args() if not (args.steam_ticket or args.entity_token): sys.exit("need --steam-ticket OR --entity-token") if not (args.catalog or args.inventory or args.titledata): args.catalog = True # default names = load_names() pf = PlayFab(args.title_id) if args.entity_token: pf.entity_token = args.entity_token print(" using captured EntityToken") if args.steam_ticket: pf.login_steam(args.steam_ticket, args.ticket_service_specific) os.makedirs(os.path.join(ROOT, "extracted"), exist_ok=True) if args.catalog: items = pf.search_items() rows = summarize_catalog(items, names) out = os.path.join(ROOT, "extracted", "playfab_catalog.json") json.dump({"_source": "PlayFab Catalog/SearchItems", "count": len(rows), "items_raw": items, "items": rows}, open(out, "w"), indent=1, ensure_ascii=False) print("wrote %s (%d items)" % (out, len(rows))) priced = [r for r in rows if r["prices"]] print(" %d items carry a price. sample:" % len(priced)) for r in priced[:10]: pr = " | ".join(" + ".join("%s %s" % (l["amount"], l["currencyName"]) for l in legs) for legs in r["prices"]) print(" %-32s %s" % (r["name"], pr)) if args.inventory and pf.entity_token: inv = pf.inventory() out = os.path.join(ROOT, "extracted", "playfab_inventory.json") json.dump(inv, open(out, "w"), indent=1, ensure_ascii=False) print("wrote", out) if args.titledata and pf.session_ticket: td = pf.title_data() out = os.path.join(ROOT, "extracted", "playfab_titledata.json") json.dump(td, open(out, "w"), indent=1, ensure_ascii=False) print("wrote %s (%d keys)" % (out, len(td))) if __name__ == "__main__": main()