{"_id":"@assetpay/steam-mobile","_rev":"5-afd376a01b6df304ec45dbdcccbf8173","name":"@assetpay/steam-mobile","dist-tags":{"latest":"1.0.5"},"versions":{"1.0.0":{"name":"@assetpay/steam-mobile","version":"1.0.0","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"author":"bekesibeni","license":"MIT","_id":"@assetpay/steam-mobile@1.0.0","maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"homepage":"https://github.com/bekesibeni/steam-mobile#readme","bugs":{"url":"https://github.com/bekesibeni/steam-mobile/issues"},"dist":{"shasum":"9d2fc1ad4016dea57129d9c17bf3943980831d82","tarball":"https://registry.npmjs.org/@assetpay/steam-mobile/-/steam-mobile-1.0.0.tgz","fileCount":7,"integrity":"sha512-/OzgB89ISdtDyiX3l8PAKfiKx0b5PVKVu0mfqcOM2aqIHUiceY2djWDsx3lYoW6FMKS77w1jKfPHyNHXg+lH+g==","signatures":[{"sig":"MEQCIDoo7w5sMi7gBRaXImnWpCHfVJj3bO2Mv+Sfcc9cLxwIAiBrOfaBrxKT3VpCIoZE15pJlj7tofg5o94T3A+iSBN8WA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":526876},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"}},"scripts":{"lint":"biome check src test","test":"vitest run","build":"tsdown","proto":"buf generate","smoke":"tsx debug/smoke.ts","trade":"tsx debug/trade.ts","watch":"tsx debug/watch.ts","lint:fix":"biome check --write src test","bootstrap":"tsx debug/bootstrap.ts","typecheck":"tsc --noEmit","test:watch":"vitest","openid-login":"tsx debug/openidLogin.ts","own-inventory":"tsx debug/ownInventory.ts","partner-inventory":"tsx debug/partnerInventory.ts","trade-eligibility":"tsx debug/webTradeEligibility.ts"},"_npmUser":{"name":"bekesibeni","email":"benji.bekesi@gmail.com"},"repository":{"url":"git+https://github.com/bekesibeni/steam-mobile.git","type":"git"},"description":"Headless Steam mobile-app client (web-based, no CM) for trading.","directories":{},"sideEffects":false,"_nodeVersion":"25.8.2","dependencies":{"impit":"^0.14.3","steamid":"^2.1.0","tough-cookie":"^6.0.1","@bufbuild/protobuf":"^2.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.22.4","vite":"^8.0.14","tsdown":"^0.22.3","vitest":"^4.1.9","typescript":"^6","@types/node":"^22","@bufbuild/buf":"^1.72.0","@biomejs/biome":"^2.4.15","@bufbuild/protoc-gen-es":"^2.12.1"},"_npmOperationalInternal":{"tmp":"tmp/steam-mobile_1.0.0_1784373713946_0.1797512248009807","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@assetpay/steam-mobile","version":"1.0.1","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"author":{"name":"bekesibeni"},"license":"MIT","_id":"@assetpay/steam-mobile@1.0.1","maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"homepage":"https://github.com/bekesibeni/steam-mobile#readme","bugs":{"url":"https://github.com/bekesibeni/steam-mobile/issues"},"dist":{"shasum":"d13f065c6beb1497660725aade5ea596849851c9","tarball":"https://registry.npmjs.org/@assetpay/steam-mobile/-/steam-mobile-1.0.1.tgz","fileCount":7,"integrity":"sha512-J38M6scnS0w8p8Dz+hFs+dkpiWWWUwYQ4IlQewSbFKtsag31HNyawcGg4KbIklEL9sxxFzeWxMG6/276W7T90A==","signatures":[{"sig":"MEUCIQCi03oQr3n4E58Bzxlm1x+CIprr57iCdvpc4NzXAwJxeAIgKFVCnjud5X8QaVoMWEKB95lPXzbVPw3JoPSdglmEUwk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":528356},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"}},"gitHead":"11c3a74d8e0f8154f6cb9b96bff2696ee6ea7746","scripts":{"lint":"biome check src test","test":"vitest run","build":"tsdown","proto":"buf generate","smoke":"tsx debug/smoke.ts","trade":"tsx debug/trade.ts","watch":"tsx debug/watch.ts","lint:fix":"biome check --write src test","bootstrap":"tsx debug/bootstrap.ts","typecheck":"tsc --noEmit","test:watch":"vitest","openid-login":"tsx debug/openidLogin.ts","own-inventory":"tsx debug/ownInventory.ts","prepublishOnly":"pnpm build","partner-inventory":"tsx debug/partnerInventory.ts","trade-eligibility":"tsx debug/webTradeEligibility.ts"},"_npmUser":{"name":"bekesibeni","email":"benji.bekesi@gmail.com"},"repository":{"url":"git+https://github.com/bekesibeni/steam-mobile.git","type":"git"},"_npmVersion":"11.8.0","description":"Headless Steam mobile-app client (web-based, no CM) for trading.","directories":{},"sideEffects":false,"_nodeVersion":"25.8.2","dependencies":{"impit":"^0.14.3","steamid":"^2.1.0","tough-cookie":"^6.0.1","@bufbuild/protobuf":"^2.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.1.1","devDependencies":{"tsx":"^4.22.4","vite":"^8.0.14","tsdown":"^0.22.3","vitest":"^4.1.9","typescript":"^6","@types/node":"^22","@bufbuild/buf":"^1.72.0","@biomejs/biome":"^2.4.15","@bufbuild/protoc-gen-es":"^2.12.1"},"_npmOperationalInternal":{"tmp":"tmp/steam-mobile_1.0.1_1784384196843_0.2808265677155102","host":"s3://npm-registry-packages-npm-production"}},"1.0.3":{"name":"@assetpay/steam-mobile","version":"1.0.3","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"author":{"name":"bekesibeni"},"license":"MIT","_id":"@assetpay/steam-mobile@1.0.3","maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"homepage":"https://github.com/bekesibeni/steam-mobile#readme","bugs":{"url":"https://github.com/bekesibeni/steam-mobile/issues"},"dist":{"shasum":"fb304ee34b82f498894327e2f5baacc8728a11cc","tarball":"https://registry.npmjs.org/@assetpay/steam-mobile/-/steam-mobile-1.0.3.tgz","fileCount":7,"integrity":"sha512-kaDg3g6hWnls1nu5TyrOgYTcgbahpGYBl3UnShPQM7hSxJNxldhDJWSCEZlRGyOYGjzqEyHY/Rb6W3Ofon9Dvw==","signatures":[{"sig":"MEUCIHt+JNxVdyABBNdrzwZfBjlgd3E8yFDZvz0OX+ly/AwbAiEAyEcO45gwWMfgMiyJ1Ar+TACQzRzYP+jZNDjkaTqRhko=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@assetpay%2fsteam-mobile@1.0.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":525316},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"}},"gitHead":"9532047a32852b038fbed5f11ae0473a83543874","scripts":{"lint":"biome check src test","test":"vitest run","build":"tsdown","proto":"buf generate","smoke":"tsx debug/smoke.ts","trade":"tsx debug/trade.ts","watch":"tsx debug/watch.ts","lint:fix":"biome check --write src test","bootstrap":"tsx debug/bootstrap.ts","typecheck":"tsc --noEmit","test:watch":"vitest","openid-login":"tsx debug/openidLogin.ts","own-inventory":"tsx debug/ownInventory.ts","prepublishOnly":"pnpm build","partner-inventory":"tsx debug/partnerInventory.ts","trade-eligibility":"tsx debug/webTradeEligibility.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:85b9b8e8-0de4-4e65-bab6-76b15821c12c"}},"repository":{"url":"git+https://github.com/bekesibeni/steam-mobile.git","type":"git"},"_npmVersion":"11.17.0","description":"Headless Steam mobile-app client (web-based, no CM) for trading.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"impit":"^0.14.3","steamid":"^2.1.0","tough-cookie":"^6.0.2","@bufbuild/protobuf":"^2.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.5.0","devDependencies":{"tsx":"^4.22.4","vite":"^8.2.1","tsdown":"^0.22.14","vitest":"^4.1.10","typescript":"^7","@types/node":"^22","@bufbuild/buf":"^1.72.0","@biomejs/biome":"^2.4.15","@bufbuild/protoc-gen-es":"^2.12.1"},"_npmOperationalInternal":{"tmp":"tmp/steam-mobile_1.0.3_1786727220587_0.22717942672829272","host":"s3://npm-registry-packages-npm-production"}},"1.0.4":{"name":"@assetpay/steam-mobile","version":"1.0.4","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"author":{"name":"bekesibeni"},"license":"MIT","_id":"@assetpay/steam-mobile@1.0.4","maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"homepage":"https://github.com/bekesibeni/steam-mobile#readme","bugs":{"url":"https://github.com/bekesibeni/steam-mobile/issues"},"dist":{"shasum":"47f17385239ea1c79287438c4f0908ee10d6bc61","tarball":"https://registry.npmjs.org/@assetpay/steam-mobile/-/steam-mobile-1.0.4.tgz","fileCount":7,"integrity":"sha512-Fy9uX5nBHlBqJgDgYZcfwdmlMwrIlIGrF6+Yfeo1ht5+tPW+yycIKKZucokfm26W0+11MSnQ2T0cQ5Fl6HxT5g==","signatures":[{"sig":"MEQCIBP2LyoBbI9nM2ebCz2W+wPfPbVmGxx43T3aLtMoetSZAiBcWvKS8ZWcsD4Iqcb4FMjQozbJesk522R1cnpwwWD9ng==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@assetpay%2fsteam-mobile@1.0.4","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":526367},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"}},"gitHead":"8144305d5e8eb734a33216e64d2475a800f10d0f","scripts":{"lint":"biome check src test","test":"vitest run","build":"tsdown","proto":"buf generate","smoke":"tsx debug/smoke.ts","trade":"tsx debug/trade.ts","watch":"tsx debug/watch.ts","lint:fix":"biome check --write src test","bootstrap":"tsx debug/bootstrap.ts","typecheck":"tsc --noEmit","test:watch":"vitest","openid-login":"tsx debug/openidLogin.ts","own-inventory":"tsx debug/ownInventory.ts","prepublishOnly":"pnpm build","partner-inventory":"tsx debug/partnerInventory.ts","trade-eligibility":"tsx debug/webTradeEligibility.ts"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:85b9b8e8-0de4-4e65-bab6-76b15821c12c"}},"repository":{"url":"git+https://github.com/bekesibeni/steam-mobile.git","type":"git"},"_npmVersion":"11.17.0","description":"Headless Steam mobile-app client (web-based, no CM) for trading.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"impit":"^0.14.3","steamid":"^2.1.0","tough-cookie":"^6.0.2","@bufbuild/protobuf":"^2.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.5.0","devDependencies":{"tsx":"^4.22.4","vite":"^8.2.1","tsdown":"^0.22.14","vitest":"^4.1.10","typescript":"^7","@types/node":"^22","@bufbuild/buf":"^1.72.0","@biomejs/biome":"^2.4.15","@bufbuild/protoc-gen-es":"^2.12.1"},"_npmOperationalInternal":{"tmp":"tmp/steam-mobile_1.0.4_1786729354017_0.9213823147241582","host":"s3://npm-registry-packages-npm-production"}},"1.0.5":{"_id":"@assetpay/steam-mobile@1.0.5","bugs":{"url":"https://github.com/assetpaygg/steam-mobile/issues"},"dist":{"shasum":"f55950da56f3f82176cb5a6a79b2e97238469d31","tarball":"https://registry.npmjs.org/@assetpay/steam-mobile/-/steam-mobile-1.0.5.tgz","fileCount":7,"integrity":"sha512-yTcfoBK9miu87ahXL5mz7cnaJwy2+Te6rBFT4F0Nl80hte5oouiq9DIlQ44ebGoHkcV5Og37KGHqLgi1Cz5m3Q==","signatures":[{"sig":"MEQCIBqIFCBc7uQ6hop+ZWMrPjakxgHKNm8vY+cG9Wd5na4NAiByE4Nf+YCBB4K+4UwY/toXAysaVG1xtC46WyxLD13xqg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCsJCSOK1aX/sIa2GiGMvIvKAF1O7UETf8+/XFjyKMQ7gIhANgdf1PoPcQ5XkFjzjQwdITMHL1IM88nQoiS65rDyYnx"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@assetpay%2fsteam-mobile@1.0.5","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":528109},"main":"./dist/index.mjs","name":"@assetpay/steam-mobile","type":"module","types":"./dist/index.d.mts","author":{"url":"https://assetpay.gg","name":"AssetPay"},"module":"./dist/index.mjs","engines":{"node":">=22"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs"}},"gitHead":"71b71be4d40511e9a3a84d00b54dc8f27181be8e","license":"MIT","scripts":{"lint":"biome check src test","test":"vitest run","build":"tsdown","proto":"buf generate","smoke":"tsx debug/smoke.ts","trade":"tsx debug/trade.ts","watch":"tsx debug/watch.ts","lint:fix":"biome check --write src test","bootstrap":"tsx debug/bootstrap.ts","typecheck":"tsc --noEmit","test:watch":"vitest","openid-login":"tsx debug/openidLogin.ts","own-inventory":"tsx debug/ownInventory.ts","prepublishOnly":"pnpm build","partner-inventory":"tsx debug/partnerInventory.ts","trade-eligibility":"tsx debug/webTradeEligibility.ts"},"version":"1.0.5","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:8bd19a13-af56-4cff-90aa-942aa7451dd9"}},"homepage":"https://github.com/assetpaygg/steam-mobile#readme","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"repository":{"url":"git+https://github.com/assetpaygg/steam-mobile.git","type":"git"},"_npmVersion":"11.19.0","description":"Headless Steam mobile-app client (web-based, no CM) for trading.","directories":{},"maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.20.0","dependencies":{"impit":"^0.14.3","steamid":"^2.1.0","tough-cookie":"^6.0.2","@bufbuild/protobuf":"^2.12.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.5.0","devDependencies":{"tsx":"^4.22.4","vite":"^8.2.1","tsdown":"^0.22.14","vitest":"^4.1.10","typescript":"^7","@types/node":"^22","@bufbuild/buf":"^1.72.0","@biomejs/biome":"^2.4.15","@bufbuild/protoc-gen-es":"^2.12.1"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/steam-mobile_1.0.5_1790182415932_0.11546256315423808"}}},"time":{"created":"2026-07-18T11:21:53.780Z","modified":"2026-09-23T16:53:36.873Z","1.0.0":"2026-07-18T11:21:54.098Z","1.0.1":"2026-07-18T14:16:37.012Z","1.0.3":"2026-08-14T17:07:00.758Z","1.0.4":"2026-08-14T17:42:34.200Z","1.0.5":"2026-09-23T16:53:36.111Z"},"bugs":{"url":"https://github.com/assetpaygg/steam-mobile/issues"},"author":{"url":"https://assetpay.gg","name":"AssetPay"},"license":"MIT","homepage":"https://github.com/assetpaygg/steam-mobile#readme","keywords":["steam","steam-mobile","steam-trade","steam-trading","tradeoffer","mobileconf","steam-guard","steamcommunity","trading-bot"],"repository":{"url":"git+https://github.com/assetpaygg/steam-mobile.git","type":"git"},"description":"Headless Steam mobile-app client (web-based, no CM) for trading.","maintainers":[{"name":"bekesibeni","email":"benji.bekesi@gmail.com"}],"readme":"# Steam Mobile\n\nA headless, fully-typed **ESM TypeScript** client for the **Steam mobile app**, built for trading. It\nunifies the *web* logic of [`steamcommunity`](https://github.com/DoctorMcKay/node-steamcommunity) and\n[`steam-tradeoffer-manager`](https://github.com/DoctorMcKay/node-steam-tradeoffer-manager) into one\ncohesive, Promise-native package — with **no `steam-user` and no Steam CM binary protocol**.\n\n## Built by AssetPay\n\nThis library is built and maintained by [AssetPay](https://assetpay.gg), B2B skin payment\ninfrastructure for merchants accepting CS2 and Rust skins. AssetPay provides a documented REST API\nfor inventory valuation, trade creation, automated settlement, and webhooks, backed by live pricing\nand fraud controls.\n\nIn production, `@assetpay/steam-mobile` powers AssetPay's Steam-facing layer: it maintains bot\nsessions, loads inventories, sends and monitors trade offers, handles mobile confirmations, and\ndecodes item inspection data. AssetPay's merchant API, orchestration, and ledger are built on top of\nthis lower-level client. See the [AssetPay API documentation](https://assetpay.gg/docs) to learn how\nthe complete payment platform fits together.\n\nEverything the mobile app needs is in-package: credential login, TOTP, mobile confirmations\n(`mobileconf`), and the `IAuthenticationService` protobufs. The only runtime dependencies are\n[`got`](https://github.com/sindresorhus/got), [`proxy-agent`](https://github.com/TooTallNate/proxy-agents),\n[`tough-cookie`](https://github.com/salesforce/tough-cookie), [`steamid`](https://github.com/DoctorMcKay/node-steamid),\nand [`@bufbuild/protobuf`](https://github.com/bufbuild/protobuf-es).\n\nThe client impersonates the Steam mobile app on the wire (a MobileApp-platform access token, per-host\nuser agents, mobile cookies, and `mobileconf` confirmations). MobileApp is the only platform whose\naccess token can self-renew over plain HTTP, which is what lets this library run headless without a\npersistent CM connection.\n\n## Table of contents\n\n- [Built by AssetPay](#built-by-assetpay)\n- [Installation](#installation)\n- [The basics](#the-basics)\n- [Logging in](#logging-in)\n- [SteamMobile](#steammobile) — the root client\n- [bot.trade](#bottrade) — sending, reading, and polling trade offers\n- [TradeOffer](#tradeoffer) — a single offer\n- [bot.community](#botcommunity) — inventories, profiles, trade URLs, API keys\n- [bot.session](#botsession) — token lifecycle and sessions\n- [bot.confirmations](#botconfirmations) — low-level mobile confirmations\n- [decodePreviewToken](#decodepreviewtoken) — decode CS2 item inspect/certificate data\n- [encodePreviewToken](#encodepreviewtoken) — encode CS2 item data back into a token\n- [Data types](#data-types)\n- [Enums](#enums)\n- [Errors](#errors)\n- [Mobile-app impersonation](#mobile-app-impersonation)\n- [Notes & limitations](#notes--limitations)\n- [Development](#development)\n\n---\n\n## Installation\n\n```bash\npnpm add @assetpay/steam-mobile   # Node 22+, ESM only\n```\n\nThis package is ESM-only and targets Node 22+. Import it with `import { SteamMobile } from \"@assetpay/steam-mobile\"`.\n\n## The basics\n\n```ts\nimport { SteamMobile } from \"@assetpay/steam-mobile\";\n\nconst bot = await new SteamMobile({\n  refreshToken: process.env.STEAM_REFRESH_TOKEN!,\n  identitySecret: process.env.STEAM_IDENTITY_SECRET, // only needed to confirm trades\n}).login();\n\nconsole.log(\"Logged in as\", bot.steamID.getSteamID64());\n\nconst { sent, received } = await bot.trade.getTradeOffers();\nconsole.log(`${sent.length} sent, ${received.length} received`);\n```\n\nConstruction is **synchronous** and does no network I/O — your `steamID` is known immediately because\nit is encoded in the refresh-token JWT. The single network step is `await bot.login()`, which mints the\naccess token and applies the login cookie. Every method awaits the token lazily, so you *can* call\nmethods before `login()`, but calling `login()` up front gives you fail-fast behavior and lets you\nauto-start polling.\n\n---\n\n## Logging in\n\nThere are two ways to get a session. In production you almost always want the **refresh-token** path:\nlog in once with credentials, store the long-lived refresh token, and reuse it.\n\n### From a refresh token (the normal path)\n\n```ts\nconst bot = await new SteamMobile({ refreshToken }).login();\n```\n\nA MobileApp refresh token lives ~211 days. The library renews the ~24-hour access token lazily and\nrotates the refresh token automatically as it nears end-of-life — emitting a [`refreshToken`](#event-refreshtoken)\nevent each time so you can persist the new one.\n\n### From credentials\n\nWhen you don't yet have a refresh token, use `loginWithCredentials`. This drives the full\n`IAuthenticationService` credential flow (RSA password encryption → begin session → answer Steam Guard\n→ poll) entirely in-package — no `steam-session` needed.\n\n#### loginWithCredentials(options)\n\n- `options`\n  - `username` — Your Steam account name (login name, not persona).\n  - `password` — Your account password.\n  - `sharedSecret` — Optional. Your TOTP `shared_secret`. If present, device (TOTP) Steam Guard codes are answered automatically. If the account has **no mobile authenticator** (email Steam Guard only), this rejects up-front with a `NoMobileAuthenticatorError` rather than silently falling through.\n  - `steamGuardCode` — Optional. A Steam Guard code you supply yourself (email or device).\n  - `proxy` — Optional. A proxy URL; routes the entire login flow.\n  - `mobileProfile` — Optional. `\"ios\"` (default), `\"android\"`, or an object overriding individual [profile fields](#mobile-app-impersonation).\n  - `signal` — Optional. An `AbortSignal`; aborting stops polling and rejects with a `LoginError`.\n  - `onSteamGuardRequired` — Optional. `async ({ type, message }) => code`. Called when a code is required and none was supplied (e.g. an email code). Resolve it with the code to continue.\n\nReturns a `Promise` that resolves to a **`LoginResult`**:\n\n- `refreshToken` — The MobileApp refresh token. **Store this** and feed it to `new SteamMobile({ refreshToken })`.\n- `accessToken` — The freshly-minted access token, or `undefined` if Steam didn't return one.\n- `steamId` — The account's SteamID64, as a string.\n- `username` — The account name Steam echoed back.\n\n```ts\nimport { loginWithCredentials, SteamMobile } from \"@assetpay/steam-mobile\";\n\nconst { refreshToken } = await loginWithCredentials({\n  username: \"myaccount\",\n  password: \"…\",\n  sharedSecret: \"…\",            // answers the device-code 2FA automatically\n  // onSteamGuardRequired: async ({ message }) => await promptUser(message),\n});\n\nconst bot = await new SteamMobile({ refreshToken }).login();\n```\n\nCredential login is rate-limited by Steam, so do it once per device and reuse the refresh token. For\nfiner-grained control over the state machine (events for each step, manual code submission), use the\n[`CredentialSession`](#credentialsession-low-level) class directly.\n\n#### CredentialSession (low-level)\n\n`loginWithCredentials` is a thin one-shot wrapper around this `EventEmitter`. Use it directly when you\nneed to react to each step.\n\n- `new CredentialSession(http, profile[, pollTimeoutMs])` — `http` is an `HttpClient`, `profile` a resolved [`MobileProfile`](#mobile-app-impersonation), `pollTimeoutMs` defaults to 180000.\n- `start(options)` — Begins the flow. `options` is the credentials (`username`, `password`, `sharedSecret?`, `steamGuardCode?`). Throws `NoMobileAuthenticatorError` synchronously inside `start()` if `sharedSecret` was supplied but the account isn't TOTP-protected. Returns `Promise<void>`.\n- `submitSteamGuardCode(code)` — Supplies a code in response to a `steamGuardRequired` event. Returns `Promise<void>`.\n- `stop()` — Aborts the flow and clears the poll timer.\n- Properties (populated as the flow progresses): `steamID?`, `username`, `accessToken?`, `refreshToken?`.\n- Events:\n  - `authenticated` — A refresh token is available; read it off the instance.\n  - `steamGuardRequired` — `{ type, message }`. No code could be supplied automatically; call `submitSteamGuardCode()`.\n  - `remoteInteraction` — Steam is waiting for the user to approve on their phone (device/email confirmation).\n  - `timeout` — The confirmation window elapsed.\n  - `error` — `error`. The flow failed.\n  - `debug` — `message`. Verbose step logging.\n\n---\n\n## SteamMobile\n\nThe root client. It owns the HTTP layer, the session, and the namespaces, and it re-emits every\n[trade event](#trade-events) so you can listen on `bot` directly instead of `bot.trade`.\n\n### Constructor: new SteamMobile(options)\n\n- `options`\n  - `refreshToken` — **Required.** A MobileApp refresh token (from `loginWithCredentials` or a prior session).\n  - `identitySecret` — Optional. Your `identity_secret`, required to **confirm** trades and other mobile confirmations. Read-only operations and sending don't need it.\n  - `proxy` — Optional. A proxy URL (`http://user:pass@host:port`, `socks5://…`, etc.). All traffic — API, community, and confirmations — routes through it.\n  - `mobileProfile` — Optional. `\"ios\"` (default), `\"android\"`, or a `Partial<MobileProfile>` to override individual fields. See [Mobile-app impersonation](#mobile-app-impersonation).\n  - `polling` — Optional. `true` to start polling with default cadence after `login()`, or a [`PollOptions`](#polloptions) object to tune it / resume from saved `pollData`.\n\nThrows `SteamSessionExpiredError` synchronously if the refresh token is malformed (no SteamID in its\npayload). Does **not** make any network request.\n\n### Properties\n\n- `steamID` — A [`SteamID`](https://github.com/DoctorMcKay/node-steamid) object for the logged-in account. Available immediately after construction.\n- `accessToken` — The current access token (`string`), or `undefined` before the first mint.\n- `refreshToken` — The current refresh token (`string`). This changes when Steam rotates it; listen for the [`refreshToken`](#event-refreshtoken) event to persist updates.\n- `identitySecret` — The `identity_secret` you passed, or `undefined`.\n- `trade` — The [TradeNamespace](#bottrade).\n- `community` — The [CommunityNamespace](#botcommunity).\n- `session` — The [SessionManager](#botsession).\n- `confirmations` — The [ConfirmationManager](#botconfirmations).\n- `http`, `api` — The low-level `HttpClient` and `WebApiClient`. You normally won't need these.\n\n### login()\n\nMints the access token (failing fast if the refresh token is dead), applies the `steamLoginSecure`\nlogin cookie, and — if you passed `polling` — starts the poll loop. Returns a `Promise` that resolves\nto the `SteamMobile` instance itself, so you can write `const bot = await new SteamMobile(opts).login()`.\n\n### reauthenticate(credentials)\n\n- `credentials` — A `ReauthenticateOptions`: the same fields as [`loginWithCredentials`](#loginwithcredentialsoptions) **except** `proxy` / `mobileProfile`, which are reused from this instance.\n\nRecovers a **dead or revoked** refresh token — one the library can no longer renew (past its ~211-day\nlife, after a password change, or a Steam revocation). Runs a fresh credential login, swaps the new\nrefresh token in place, re-mints the access token, and re-emits [`refreshToken`](#event-refreshtoken) so\nyou can persist it. **Rejects if the credentials are for a different account** than this instance.\nReturns `Promise<void>`. Pair it with the [`sessionExpired`](#event-sessionexpired) event.\n\n### ensureApiKey(\\[domain])\n\n- `domain` — Optional. The domain to register the key under (default `\"assetpay.gg\"`). Steam's API-key `domain` is a hostname label, not a URL.\n\nReturns the account's existing Web API key, or registers a new one (auto-accepting the mobile\nconfirmation prompt if `identitySecret` is set), as a `Promise<string | null>`. Resolves to `null` when\nthe account is **ineligible** for a key (unverified email, no mobile authenticator, or a\n[limited account](#notes--limitations)). This is a convenience passthrough to\n[`bot.community.ensureApiKey`](#communityensureapikeydomain).\n\n### request(method, url\\[, opts]) · get(url\\[, opts]) · post(url\\[, opts])\n\nEscape hatch for endpoints the typed namespaces don't cover (like `node-steamcommunity`'s\n`httpRequest`). Each ensures the session is live — minting/renewing the access token and applying the\n`steamLoginSecure` cookie — *before* sending, so the request carries your logged-in session, then\ndelegates to [`bot.http`](#properties). Throws `SteamSessionExpiredError` if the session can't be\nrenewed.\n\n- `method` — `\"GET\"` or `\"POST\"`.\n- `url` — Absolute URL.\n- `opts` — A `RequestOptions`: `searchParams`, `headers`, `referer`, `responseType` (`\"text\"` default, `\"json\"`, or `\"buffer\"`), `signal` (an `AbortSignal` to cancel), `timeoutMs` (per-request timeout override; default 50000), and **exactly one** request body of `form`, `multipart`, `json`, or `body` (raw `string`/`Buffer`).\n\nReturns `Promise<HttpResponse<T>>` (`{ statusCode, headers, body }`). Pass a type parameter and\n`responseType: \"json\"` to get a parsed body.\n\n```ts\nconst { body } = await bot.get<{ success: boolean }>(\n  \"https://steamcommunity.com/market/priceoverview/\",\n  { searchParams: { appid: 730, market_hash_name: \"Mag-7 | Heat (Field-Tested)\", currency: 1 }, responseType: \"json\" },\n);\n\nawait bot.post(\"https://steamcommunity.com/some/endpoint\", {\n  json: { sessionid: await bot.http.getSessionId(), foo: \"bar\" },\n});\n```\n\nFor requests where you *don't* want the session ensured first, use `bot.http` directly.\n\n### shutdown()\n\nStops the poll loop and removes all event listeners (on both `bot` and `bot.trade`). Call this when\nyou're done with the client so it doesn't keep timers alive. Returns a `Promise<void>`.\n\n### Events\n\n`SteamMobile` re-emits all [trade events](#trade-events) (`newOffer`, `sentOfferChanged`,\n`receivedOfferChanged`, `unknownOfferSent`, `pollData`, `pollSuccess`, `pollFailure`, `debug`) plus two\nof its own:\n\n#### Event: refreshToken\n\n- `token` — The new refresh token (`string`).\n\nEmitted when Steam rotates your refresh token (which happens automatically near end-of-life). **Persist\nthe new value** — the old one stops working.\n\n#### Event: sessionExpired\n\n- `error` — A `SteamSessionExpiredError`.\n\nEmitted when the refresh token is rejected or expired and the session can no longer be renewed. It fires\nonly on a **confirmed-terminal** auth failure (not a transient blip), so it's safe to trigger re-auth\non: call [`bot.reauthenticate(credentials)`](#reauthenticatecredentials).\n\n---\n\n## bot.trade\n\nThe trade namespace. It's an `EventEmitter` (see [Trade events](#trade-events)) and holds every method for\nreading, sending, and watching trade offers.\n\n### createOffer(target)\n\n- `target` — An [`OfferTarget`](#offertarget): either `{ tradeUrl }` or `{ steamId, token? }`.\n\nCreates a new, empty [TradeOffer](#tradeoffer) addressed to the partner. **Local only** — nothing is\nsent to Steam until you call [`offer.send()`](#send). Chain `.give()` / `.receive()` / `.setMessage()`\nto build it.\n\n### getTradeOffer(id)\n\n- `id` — The trade offer's numeric id, as a string.\n\nFetches a single offer and returns it as a [TradeOffer](#tradeoffer). Item descriptions are inlined\n(`get_descriptions=1`), so the offer's items are full [`EconItem`](#econitem)s. Rejects with a\n`SteamError` if the offer doesn't exist.\n\n### getTradeOffers(\\[filter]\\[, historicalCutoff])\n\n- `filter` — Optional. A value from [`EOfferFilter`](#eofferfilter) (default `ActiveOnly`).\n- `historicalCutoff` — Optional. A `Date`. When `filter` includes historical offers, only offers updated at or after this time are returned. Defaults to one year in the future (i.e. \"active only\").\n\nReturns `Promise<{ sent: TradeOffer[]; received: TradeOffer[] }>`. This cursor-paginates **all** pages\ninternally and inlines descriptions into every offer's items. If Steam returns the wholly-malformed\n\"data temporarily unavailable\" glitch (all offers missing partner/items), this rejects with a\n`SteamError` rather than handing back garbage.\n\n### getTradeStatus({tradeId})\n\n- `tradeId` — The **trade id** (not the offer id). This is set on a [TradeOffer](#tradeoffer) once it's accepted (`offer.tradeID`).\n\nReturns settlement details for a completed or escrowed trade as a `Promise<`[`ExchangeDetails`](#exchangedetails)`>`,\nincluding where each item *landed* after the trade (`new_assetid` / `new_contextid`, when Steam provides\nthem). See [`offer.getTradeStatus()`](#gettradestatus) for the offer-level shortcut.\n\n### getUserDetails(target)\n\n- `target` — An [`OfferTarget`](#offertarget) (typically `{ tradeUrl }`).\n\nPersona, contexts, escrow days, avatars, and partner probation for both sides — same scrape as\n[`offer.getUserDetails()`](#getuserdetails) but addressed by target, so you don't need an offer in\nhand. Returns `Promise<`[`UserDetails`](#userdetails)`>`. **Steam applies\n`max(me.escrowDays, them.escrowDays)`** as the actual hold — compute that yourself if you need a\nsingle number.\n\n### getEscrow(target)\n\n- `target` — An [`OfferTarget`](#offertarget) (typically `{ tradeUrl }`).\n\nEscrow hold (in **seconds**) via `IEconService/GetTradeHoldDurations`. Returns\n`Promise<`[`EscrowHold`](#escrowhold)`>` shaped `{ me, them, both }` (top-level renamed from Steam's\n`my_escrow` / `their_escrow` / `both_escrow`; inner shape preserved 1:1). Lightweight — just the\nWebAPI, no scrape, no other user data. For everything together (escrow + persona + avatars +\ncontexts), use [`getUserDetails`](#getuserdetailstarget) above (or [`offer.getUserDetails()`](#getuserdetails))\ninstead.\n\n### getTradeHistory(\\[options])\n\n- `options` — Optional [`TradeHistoryOptions`](#tradehistoryoptions):\n  - `maxTrades` — Max trades to return (default 100).\n  - `startAfterTime` / `startAfterTradeId` — Cursor: the time and id of the last trade from the previous page. Pass both to fetch the next page.\n  - `navigatingBack` — Page backwards (toward newer trades) instead of forward.\n  - `includeFailed` — Include failed/rolled-back trades.\n  - `includeTotal` — Ask Steam for the total trade count (populates `totalTrades`).\n\nReturns past trades, newest first, as a `Promise<`[`TradeHistory`](#tradehistory)`>`. Each entry is a\nfully-parsed [`ExchangeDetails`](#exchangedetails) plus its `tradeId` and `partnerSteamId`. To page,\ntake the last entry's `tradeInitTime`/`tradeId` and pass them as `startAfterTime`/`startAfterTradeId`\nwhile `more` is `true`.\n\n### getTradeOffersSummary()\n\nReturns counts of pending/new/historical sent & received offers (and escrow counts) as a\n`Promise<`[`TradeOffersSummary`](#tradeofferssummary)`>`. Cheap; useful as a \"do I have anything to\nlook at?\" check before a full `getOffers`.\n\n### reconcile(ids)\n\n- `ids` — An array of trade offer id strings.\n\nRe-reads each id in parallel and returns a `Promise<Map<string, TradeOffer>>` keyed by id. **Missing or\nerrored ids are skipped**, so one bad id can't fail the whole batch. This is the authoritative,\npoll-free way to track offers you've sent: persist the id from `send()`, then reconcile it on demand.\n\n### getOffersContainingItems(items\\[]\\[, includeInactive])\n\n- `items` — An array of `{ appid, contextid, assetid }` item references.\n- `includeInactive` — Optional. If `true`, searches all offers; otherwise active only (default `false`).\n\nReturns the offers that contain **all** of the given items, as a `Promise<TradeOffer[]>`.\n\n### getInventory(target, appid\\[, contextid]\\[, options])\n\n- `target` — An [`OfferTarget`](#offertarget) identifying whose inventory to load (typically a partner via `{ tradeUrl }`).\n- `appid` — The app id (e.g. `730` for CS2, `252490` for Rust).\n- `contextid` — Optional. The context id (default `\"2\"`).\n- `options`\n  - `tradableOnly` — Optional. If `true`, only tradable items are returned (default `false`).\n\nLoads a **partner's** inventory through the trade-offer page (`/tradeoffer/new/partnerinventory/`),\nwhich is the reliable way to see a partner's items — including trade-protected ones that the public\n`/inventory/` endpoint hides. Returns `Promise<`[`EconItem`](#econitem)`[]>`, paginated automatically.\nFor your *own* inventory, prefer [`bot.community.getInventory`](#getinventoryappid-contextid-options).\n\nOn failure, the partner's trade page is lazily scraped for Steam's `<div id=\"error_msg\">` and the\nmessage is classified into the right typed [error](#errors) (`PrivateInventoryError`,\n`TradeBanError`, `TargetCannotTradeError`, `ItemServerUnavailableError`, …).\n\n### startPolling(\\[options])\n\n- `options` — Optional [`PollOptions`](#polloptions):\n  - `pollInterval` — Active-poll cadence in ms (default `10000`).\n  - `pollFullUpdateInterval` — Full-sweep cadence in ms (default `300000`).\n  - `pollData` — A saved [`PollData`](#polldata) snapshot to resume from (so a restart doesn't re-emit known offers).\n  - `store` — A [`PollDataStore`](#polldatastore) loaded on start and saved after each changed tick (e.g. Redis-backed). Default: in-memory only.\n  - `maxAgeMs` — Retention window in ms (default `2592000000`, 30d). Terminal offers older than this are pruned from the snapshot, and the full sweep is bounded to this window.\n  - `cancelTime` — If set, auto-cancels a sent offer still `Active` this many ms after its last update, emitting [`sentOfferCanceled`](#event-sentoffercanceled). Off by default.\n\nStarts the poll loop. The library does an **active poll** (recently-changed offers only) on the short\ninterval and a periodic **full sweep** (offers updated within the retention window, to catch backdated\nstate changes) on the long interval. State changes are diffed against the last snapshot and surfaced as\n[events](#trade-events). The loop never dies on a rate limit — it backs off and resumes. Calling this\nagain replaces the running loop.\n\n### stopPolling()\n\nStops the poll loop.\n\n### pollOnce(\\[options])\n\n- `options` — Optional. A [`PollOptions`](#polloptions) (used to configure the lazily-created poller on first call) plus `forceFull?` to force a full sweep this cycle.\n\nRuns a **single, timer-less** poll cycle, for driving cadence from an external scheduler (e.g. a BullMQ\ncron) instead of the built-in timer. Returns `Promise<{ changes:`[`PollChange`](#pollchange)`[]; pollData: PollData }>`.\nWith a [`store`](#polldatastore) it loads the snapshot, diffs, emits the usual [events](#trade-events),\nsaves, and returns the diff. `lastFullUpdate` is persisted inside `pollData`, so a fresh instance per\njob still keeps the full-sweep cadence (it won't degrade to a full sweep every call). The poller is\ncreated lazily and reused, so pass config once. Unlike the timer loop, this **throws** on a fetch\nfailure so your scheduler can handle it.\n\n### pollData (getter)\n\nA getter returning the current [`PollData`](#polldata) snapshot, or `undefined` if polling isn't\nrunning. Usually you persist this via the [`pollData` event](#event-polldata) rather than reading it\ndirectly.\n\n### Trade events\n\n`bot.trade` (and `bot`, which re-emits them) emits:\n\n#### Event: newOffer\n\n- `offer` — A [TradeOffer](#tradeoffer) for the newly-received offer.\n\nEmitted when someone sends you a new trade offer.\n\n#### Event: receivedOfferChanged\n\n- `offer` — The [TradeOffer](#tradeoffer) in its new state.\n- `oldState` — The previous [`ETradeOfferState`](#etradeofferstate).\n\nEmitted when an offer you received changes state (e.g. you accepted it elsewhere, or it expired).\n\n#### Event: sentOfferChanged\n\n- `offer` — The [TradeOffer](#tradeoffer) in its new state.\n- `oldState` — The previous [`ETradeOfferState`](#etradeofferstate).\n\nEmitted when an offer you sent changes state (e.g. the partner accepted or declined it).\n\n#### Event: unknownOfferSent\n\n- `offer` — The [TradeOffer](#tradeoffer).\n\nEmitted when polling sees a sent offer it has no record of — either sent out-of-band (not via this\nclient) or seen on the first cold poll.\n\n#### Event: sentOfferCanceled\n\n- `offer` — The [TradeOffer](#tradeoffer) that was canceled.\n- `reason` — Why it was canceled; currently always `\"cancelTime\"`.\n\nEmitted when the poll loop auto-cancels a sent offer because it stayed `Active` past\n[`cancelTime`](#startpollingoptions). The resulting `Active → Canceled` transition also surfaces as a\nnormal [`sentOfferChanged`](#event-sentofferchanged) on a later poll.\n\n#### Event: offerUpdate\n\n- `update` — A [`TradeOfferUpdate`](#tradeofferupdate) `{ offer, previousState? }`.\n\nFires once for **every** offer change, alongside the specific named event above. `previousState` is\n`undefined` the first time an offer is observed; `offer.isOurOffer` distinguishes sent vs received. Use\nthis when you want a single handler for all changes (e.g. forwarding to a queue); use the named events\nwhen you want to react to one kind.\n\n#### Event: pollData\n\n- `data` — The new [`PollData`](#polldata) snapshot.\n\nEmitted whenever the poll snapshot changes. **Persist this** and pass it back as `pollData` on the next\n`startPolling` to resume without re-emitting offers you've already handled.\n\n#### Event: pollSuccess\n\nEmitted after each successful poll cycle.\n\n#### Event: pollFailure\n\n- `error` — The `Error` that caused the failed poll.\n\nEmitted when a poll cycle fails. The loop keeps running; this is informational.\n\n#### Event: debug\n\n- `message` — A human-readable debug string.\n\nVerbose internal logging (cursor pages, mint/renewal, backoff). Listen on this while developing.\n\n---\n\n## TradeOffer\n\nRepresents a single trade offer — either one you're building to send, or one fetched from Steam. Build\noffers with [`bot.trade.createOffer`](#createoffertarget); fetch them with\n[`bot.trade.getTradeOffer`](#gettradeofferid) / `getOffers` / `getTradeOffers`.\n\n### Offer properties\n\n- `id` — The offer id (`string`), or `undefined` before it's been sent.\n- `partner` — A [`SteamID`](https://github.com/DoctorMcKay/node-steamid) for the other party.\n- `token` — The partner's trade-URL token (`string`), if known.\n- `message` — The offer message (≤128 chars).\n- `state` — The current [`ETradeOfferState`](#etradeofferstate).\n- `itemsToGive` — Array of [`TradeItem`](#tradeitem) (full [`EconItem`](#econitem)s on fetched offers) you'd give up.\n- `itemsToReceive` — Array of items you'd receive.\n- `isOurOffer` — `true` if we sent it, `false` if we received it.\n- `tradeID` — The trade id once accepted (`string`), or `undefined`. Needed for [`getTradeStatus()`](#gettradestatus).\n- `confirmationMethod` — An [`EConfirmationMethod`](#econfirmationmethod) (`MobileApp`, `Email`, or `None`).\n- `escrowEnds` — A `Date` when the Steam Guard escrow hold ends, or `undefined`.\n- `settlementDate` — A `Date` when the 2025 trade-protection hold ends and items become final, or `undefined`. Equals the trade's `time_settlement` (~7–8 days after accept); set only once `Accepted`. Separate from `escrowEnds`.\n- `delaySettlement` — `true` when the trade's items are subject to the trade-protection settlement delay.\n- `created` / `updated` / `expires` — `Date`s, or `undefined`.\n- `fromRealTimeTrade` — `true` if this came from a real-time trade session.\n- `glitched` — `true` when the offer is missing item names (descriptions not ready) or has no items. Polling won't advance its cutoff past a glitched offer, so it gets re-polled until complete.\n\n### give(items)\n\n- `items` — An array of [`TradeItem`](#tradeitem) (`{ appid, contextid, assetid, amount? }`).\n\nAdds items **you** will give. Chainable (returns the offer). Array-only.\n\n### receive(items)\n\n- `items` — An array of [`TradeItem`](#tradeitem).\n\nAdds items you'll receive from the partner. Chainable.\n\n### setMessage(message)\n\n- `message` — The offer message (truncated to 128 characters).\n\nSets the message. Chainable. Throws if the offer has already been sent.\n\n### send()\n\nSends the offer. Before sending it best-effort acknowledges the 2025 trade-protection notice (which\nSteam otherwise blocks new offers on). Returns a `Promise` resolving to a **`SendResult`**:\n\n- `\"sent\"` — The offer is live; `offer.id` and `offer.state` (`Active`) are now set.\n- `\"needs_confirmation\"` — The offer was created but needs a mobile/email confirmation. Call [`confirm()`](#confirm).\n\nThrows a typed [error](#errors) on failure (e.g. `OfferLimitError`, `TradeBanError`,\n`SteamSessionExpiredError`). Throws if the offer is empty or already sent.\n\n### accept()\n\nAccepts an offer **you received**. Returns a `Promise` resolving to an **`AcceptResult`**:\n\n- `\"accepted\"` — Done; items have transferred.\n- `\"escrow\"` — Accepted but held in escrow (`offer.escrowEnds` tells you until when).\n- `\"needs_confirmation\"` — Needs a mobile/email confirmation; call [`confirm()`](#confirm).\n\nBecause the accept response doesn't itself flag escrow, this re-reads the offer afterward to tell a\nheld trade from a settled one (best-effort; falls back to `\"accepted\"`). Throws if the offer isn't\nactive, is our own, or on a Steam error.\n\n### cancel()\n\nCancels an offer you sent (or, for a received offer, declines it) via the community endpoint — the Web\nAPI rejects the mobile token for this. Returns `Promise<void>`. Throws if the offer isn't active /\npending-confirmation.\n\n### decline()\n\nAlias for [`cancel()`](#cancel); use it for readability on received offers.\n\n### confirm()\n\nAccepts the pending mobile confirmation for this offer (after `send()`/`accept()` returned\n`\"needs_confirmation\"`). Returns `Promise<void>`. Throws a `ConfirmationError` immediately if the\noffer is unsent, if `identitySecret` wasn't supplied to the `SteamMobile` constructor (named-fix\nmessage: *\"identitySecret is required to confirm trade offers — construct SteamMobile with\n{ identitySecret }\"*), or if no matching confirmation is found.\n\n### counter()\n\nReturns a **new** unsent [TradeOffer](#tradeoffer) to the same partner, pre-filled with this offer's\nitems and message (deep-copied, so editing the counter won't mutate the original). Edit it and call\n`send()`; sending marks the original as countered. Throws if the original isn't active.\n\n### getTradeStatus()\n\nShortcut for [`bot.trade.getTradeStatus({ tradeId: this.tradeID })`](#gettradestatustradeid). Returns\n`Promise<`[`ExchangeDetails`](#exchangedetails)`>`. Throws if the offer has no `tradeID` (i.e. it hasn't\nbeen accepted).\n\n### getPartnerInventory(appid\\[, contextid]\\[, tradableOnly])\n\n- `appid` — App id.\n- `contextid` — Optional context id (default `\"2\"`).\n- `tradableOnly` — Optional. Tradable items only.\n\nLoads the **partner's** inventory, reusing this offer's partner and token. Returns\n`Promise<`[`EconItem`](#econitem)`[]>`.\n\n### getUserDetails()\n\nPersona, contexts, escrow days, avatars, and partner probation for both sides — McKay-parity. Scrapes\nthe trade page once. Picks the URL based on the offer's state: `/tradeoffer/<id>/` for offers that\nhave already been sent, otherwise `/tradeoffer/new/?partner=…[&token=…]` for an unsent draft. Returns\n`Promise<`[`UserDetails`](#userdetails)`>`. **Steam applies `max(me.escrowDays, them.escrowDays)`** as\nthe actual hold — compute that yourself if you need a single number.\n\nUse this when you already have a `TradeOffer` in hand. If you only have a tradeUrl/SteamID, prefer\nthe top-level [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) — same scrape, no offer\nconstruction needed.\n\nThrows when called on an offer where Steam doesn't render trade info:\n\n- An offer **we sent** (`isOurOffer === true` with `this.id` set) — Steam shows our own offers from a\n  different view. Use [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) instead.\n- An offer **received** but no longer `Active` (already accepted, declined, expired, etc.).\n\n### containsItem(item)\n\n- `item` — `{ appid, contextid, assetid }`.\n\nReturns `true` if either side of the offer contains that exact asset.\n\n---\n\n## bot.community\n\nAccount- and profile-level helpers backed by `steamcommunity.com` and a couple of Web API calls.\n\n### getInventory(appid\\[, contextid]\\[, options])\n\n- `appid` — App id.\n- `contextid` — Optional context id (default `\"2\"`).\n- `options`\n  - `steamId` — Optional. Whose inventory to load (default: yourself).\n  - `tradableOnly` — Optional. If `true`, only tradable items are returned (default `false`). For your\n    own inventory this is applied server-side via the legacy endpoint's `trading=1` flag; for other\n    users it's applied in the parser.\n\nLoads your own inventory — or any **public** inventory — paginated automatically. Returns\n`Promise<`[`EconItem`](#econitem)`[]>`. Throws `PrivateInventoryError` on a private inventory (the\nlazy trade-page scrape also classifies trade-ban / target-cannot-trade / item-server-unavailable\nerrors into their typed counterparts).\n\nRoutes by target:\n\n- **Self** → the legacy `/profiles/<id>/inventory/json/<appid>/<contextid>` endpoint (cookie path,\n  near-nonexistent rate limits, surfaces trade-protected items). The modern\n  `IEconService/GetInventoryItemsWithDescriptions` is avoided here — Steam silently returns\n  `{response:{}}` on ≥2 calls/sec, making it unfit for repeated use.\n- **Other user** → the public `/inventory/` endpoint (cookie path).\n\nFor a trade **partner's** inventory, use\n[`bot.trade.getInventory`](#getinventorytarget-appid-contextid-options) instead — `/partnerinventory/`\nis the only path that reveals trade-protected items.\n\n### getTradeURL()\n\nReturns your current trade URL and token as `Promise<{ url: string; token: string }>`.\n\n### changeTradeURL()\n\nRegenerates (and thereby **invalidates**) your trade URL/token. Returns the new\n`Promise<{ url: string; token: string }>`.\n\n### getProfile(\\[steamId])\n\n- `steamId` — Optional. Whose profile to read (default: yourself).\n\nReads a profile summary from the community XML in a **single request**. Returns a\n`Promise<`[`SteamProfile`](#steamprofile)`>` with persona, avatar, account-creation date, trade-ban\nstate, VAC status, privacy state, and the `isLimited` flag. (Steam level isn't in the XML — use\n`getSteamLevel`.)\n\n### getSteamLevel(\\[steamId])\n\n- `steamId` — Optional (default: yourself).\n\nReturns the account's Steam level as a `Promise<number>` via `IPlayerService/GetSteamLevel` (which\naccepts the access token — no API key required).\n\n### community.ensureApiKey(\\[domain])\n\n- `domain` — Optional hostname for the key (default `\"assetpay.gg\"`).\n\nReturns the existing Web API key, or registers a new one (auto-accepting the mobile confirmation if\n`identitySecret` is set), as `Promise<string | null>`. Resolves to `null` when the account is\nineligible (unverified email, no authenticator, or limited). The registration flow handles Steam's\nPending → confirm → retry handshake for you.\n\n### acknowledgeTradeProtection()\n\nAcknowledges the 2025 trade-protection notice. Called automatically by [`send()`](#send); exposed here\nfor completeness. Returns `Promise<void>`.\n\n---\n\n## bot.session\n\nThe `SessionManager` owns the token lifecycle. You rarely call it directly — the namespaces refresh the\naccess token for you — but it's where session control lives.\n\n### getAccessToken()\n\nReturns a valid access token, minting or renewing it if needed, as `Promise<string>`. Concurrent calls\nshare one in-flight mint. Throws `SteamSessionExpiredError` if the session has been revoked or the\nrefresh token is dead.\n\n### listSessions()\n\nReturns the client ids of in-progress auth sessions (login-approval flows) as `Promise<bigint[]>`.\nNote: this is **not** a list of logged-in devices — it reflects pending `IAuthenticationService`\nsessions.\n\n### logout(\\[action])\n\n- `action` — Optional [`EAuthTokenRevokeAction`](#eauthtokenrevokeaction) (default `Logout`).\n\nRevokes this refresh token server-side. Afterward the session is dead and any further call throws\n`SteamSessionExpiredError`. Returns `Promise<void>`.\n\n### setRefreshToken(refreshToken)\n\n- `refreshToken` — A fresh MobileApp refresh token for the **same** account.\n\nSwaps in a new refresh token and re-mints in place — the mechanism behind\n[`bot.reauthenticate`](#reauthenticatecredentials). Re-emits [`refreshToken`](#event-refreshtoken).\nThrows if the token is malformed or belongs to a different account. Returns `Promise<void>`.\n\n### Properties & events\n\n- `refreshToken` / `accessToken` / `steamID` — Same values surfaced on [`SteamMobile`](#properties).\n- Events: `refreshToken`, `sessionExpired`, `debug` — re-emitted on the root client.\n\n---\n\n## bot.confirmations\n\nThe `ConfirmationManager` for mobile confirmations. **Prefer [`offer.confirm()`](#confirm)** for trades;\nthese cover other cases (market listings, API-key requests, manual flows). All require `identitySecret`.\n\nThe high-level helpers below derive the TOTP time and HMAC key for you — you normally only need these.\nThe lower-level `getConfirmations`/`respondToConfirmation` remain for when you want to compute keys yourself.\n\n### getPending()\n\nReturns all outstanding confirmations as `Promise<`[`Confirmation`](#confirmation)`[]>`, deriving the\nlist HMAC automatically.\n\n### acceptConfirmation(id, nonce) · rejectConfirmation(id, nonce)\n\nAccept or reject (cancel) a single confirmation by its `id` and `nonce` (the `key` field from\n[`getPending`](#getpending)). Each returns `Promise<void>` and handles the time-offset + per-request\nHMAC timestamp for you.\n\n### acceptAll()\n\nAccepts every pending confirmation and resolves to the [`Confirmation`](#confirmation)`[]` it acted on.\nFails fast on the first error.\n\n```ts\nfor (const c of await bot.confirmations.getPending()) console.log(c.title);\nawait bot.confirmations.acceptAll();\n```\n\n### getConfirmations(time, key)\n\n- `time` — A Unix timestamp (use the manager's server-time-corrected clock).\n- `key` — A confirmation key, or `{ tag, key }`.\n\nReturns the outstanding confirmations as `Promise<`[`Confirmation`](#confirmation)`[]>`.\n\n### respondToConfirmation(confID, confKey, time, key, accept)\n\n- `confID` — The confirmation id.\n- `confKey` — The confirmation's nonce.\n- `time` — A Unix timestamp.\n- `key` — A confirmation key, or `{ tag, key }`.\n- `accept` — `true` to allow, `false` to cancel.\n\nAccepts or rejects a single confirmation. Returns `Promise<void>`.\n\n### acceptConfirmationForObject(objectID)\n\n- `objectID` — The id of the object the confirmation is for (e.g. a trade offer id, or an API-key request id).\n\nFinds and accepts the confirmation tied to that object, handling the time-offset and per-request HMAC\ntimestamps for you. Returns `Promise<void>`. Throws a `ConfirmationError` if no matching confirmation\nexists.\n\n---\n\n## decodePreviewToken\n\nA standalone helper (not on the bot) that decodes a **CS2 masked preview token** — the inspect-data\nblob Steam ships in an item's `asset_properties` under `propertyid: 6` (the \"certificate\") — into a\nplain JSON object. The same masked format is used by `csgo_econ_action_preview` market inspect tokens.\nDecoding is fully offline (XOR-unmask → protobuf), so no game-coordinator call is needed.\n\n```ts\nimport { decodePreviewToken } from \"@assetpay/steam-mobile\";\n\nconst cert = item.asset_properties.find((p) => p.propertyid === 6)?.string_value;\nconst data = decodePreviewToken(cert);\n// {\n//   itemid: \"51663785755\", defindex: 4, paintindex: 230, rarity: 4, quality: 9,\n//   paintwear: 0.18685653805732727, paintseed: 34,\n//   stickers: [{ slot: 2, stickerId: 6618 }, …],\n//   inventory: 3221225475, origin: 8,\n//   keychains: [{ slot: 0, stickerId: 54, offsetX: -1.55…, pattern: 34792 }],\n// }\n```\n\n- `hex` — The raw certificate hex (or any `[xorKey][protobuf][crc32]` masked blob). Pass it as-is; no\n  trimming or tag-stripping needed.\n- Returns the decoded [`CEconItemPreviewDataBlock`](https://github.com/SteamDatabase/Protobufs/blob/master/csgo/cstrike15_gcmessages.proto#L917)\n  as a plain object (`Record<string, unknown>`), or `null` for non-hex / undecodable input.\n\nOutput notes: `itemid` (uint64) is a **string**, fields are **camelCase** (`sticker_id` → `stickerId`,\n`offset_x` → `offsetX`), only fields actually present on the wire are included, and `paintwear` is\nreturned as the **float wear** (`0..1`) rather than its raw uint32 bits.\n\n> S/A/D inspect links (the ones with a `D…` parameter) are **not** decodable this way — those carry no\n> embedded data and require a live game-coordinator lookup.\n\n---\n\n## encodePreviewToken\n\nThe inverse of [`decodePreviewToken`](#decodepreviewtoken) — takes the JSON shape that one returns and\npacks it back into a masked token. Useful for minting an inspect link for an item that doesn't exist\n(previewing a float/seed combination), or for re-emitting a token after tweaking a field.\n\n```ts\nimport { decodePreviewToken, encodePreviewToken } from \"@assetpay/steam-mobile\";\n\nconst data = decodePreviewToken(cert);\nencodePreviewToken(data, 0x2e) === cert; // true — exact round-trip, key included\n\n// Preview the same skin at a different float and pattern seed:\nconst hex = encodePreviewToken({ ...data, paintwear: 0.01, paintseed: 661 });\nconst link = `steam://rungame/730/76561202255233023/+csgo_econ_action_preview%20${hex}`;\n```\n\n- `data` — a [`CEconItemPreviewDataBlock`](https://github.com/SteamDatabase/Protobufs/blob/master/csgo/cstrike15_gcmessages.proto#L917)\n  as a plain object, in the same camelCase/string-uint64 form `decodePreviewToken` returns.\n- `xorKey` — the mask byte (default `0`). `0` leaves the payload readable; any other byte masks it the\n  way CS2's own links do. Purely cosmetic — the key travels in plaintext as byte 0.\n- Returns uppercase hex, or `null` if `data` isn't a valid preview block or `xorKey` isn't a byte.\n\n`paintwear` is read as the **float wear** (`0..1`) and re-packed into its uint32 bits. It is stored as a\n**float32**, so a value typed as a decimal comes back rounded to ~7 significant digits\n(`0.1` → `0.10000000149011612`); values already read out of a real token round-trip exactly.\n\n> The trailing checksum is an unkeyed CRC32 over `[xorKey, ...protobuf]` — an integrity code, **not** a\n> signature. Masked tokens are self-contained and unauthenticated: CS2 renders them client-side without\n> asking Valve whether the item exists. Nothing here forges ownership of anything.\n\n---\n\n## Data types\n\n### OfferTarget\n\nIdentifies a trade partner. It's a discriminated union — supply **one** of the two shapes:\n\n```ts\ntype OfferTarget =\n  | { tradeUrl: string }            // a full trade URL (partner + token parsed out)\n  | { steamId: string; token?: string };  // a SteamID64, optionally with a trade token\n```\n\n### TradeItem\n\nThe minimal shape needed to put an item into an offer:\n\n```ts\ninterface TradeItem {\n  appid: number;\n  contextid: string;\n  assetid: string;\n  amount?: number; // default 1\n}\n```\n\n### EconItem\n\nThe fully-typed item, with Steam's fields preserved 1:1 (snake_case, lossless). Returned by every\ninventory and offer read. Key fields:\n\n- `appid` (number), `contextid`, `assetid`, `classid`, `instanceid`, `amount`, `currencyid?`\n- `name`, `market_name`, `market_hash_name`, `type`, `name_color?`, `background_color?`\n- `icon_url`, `icon_url_large?`\n- `tradable`, `marketable`, `commodity` (booleans), `market_tradable_restriction`, `market_marketable_restriction` (numbers)\n- `descriptions`, `owner_descriptions` ([`SteamDescriptionLine`](#data-types)[]), `actions`, `market_actions` (`SteamAction[]`), `fraudwarnings` (string[]), `tags` (`SteamTag[]`)\n- `asset_properties` ([`AssetProperty`](#data-types)[]) — CS2 float/seed/sticker data when present; decode the `propertyid: 6` certificate with [`decodePreviewToken`](#decodepreviewtoken)\n- Any other field Steam returns is preserved (`[key: string]: unknown`).\n\n### ExchangeItem\n\nAn [`EconItem`](#econitem) plus where it landed after the trade settled:\n\n```ts\ninterface ExchangeItem extends EconItem {\n  new_assetid?: string;        // Rust populates these; CS2 usually omits them\n  new_contextid?: string;\n  rollback_new_assetid?: string;\n  rollback_new_contextid?: string;\n}\n```\n\n### ExchangeDetails\n\nReturned by [`getTradeStatus`](#gettradestatustradeid):\n\n```ts\ninterface ExchangeDetails {\n  status: ETradeStatus;\n  tradeInitTime: Date;\n  settlementTime: Date | null;   // when items were actually delivered\n  receivedItems: ExchangeItem[];\n  sentItems: ExchangeItem[];\n  usedInventoryFallback: boolean; // always false — surfacing new_assetid is enough; reconciling by inventory diff is the server's job\n}\n```\n\n### TradeHistory\n\nReturned by [`getTradeHistory`](#gettradehistoryoptions):\n\n```ts\ninterface TradeHistory {\n  trades: TradeHistoryEntry[];   // newest first\n  more: boolean;                 // are there more pages?\n  totalTrades: number | undefined; // only when includeTotal was set\n}\n\ninterface TradeHistoryEntry extends ExchangeDetails {\n  tradeId: string;\n  partnerSteamId: string | undefined;\n}\n```\n\n### TradeHistoryOptions\n\nSee [`getTradeHistory`](#gettradehistoryoptions) for field meanings: `maxTrades?`, `startAfterTime?`,\n`startAfterTradeId?`, `navigatingBack?`, `includeFailed?`, `includeTotal?`.\n\n### TradeOffersSummary\n\nReturned by [`getTradeOffersSummary`](#gettradeofferssummary) — all `number`:\n\n`pending_received_count`, `new_received_count`, `updated_received_count`, `historical_received_count`,\n`pending_sent_count`, `newly_accepted_sent_count`, `updated_sent_count`, `historical_sent_count`,\n`escrow_received_count`, `escrow_sent_count`.\n\n### SteamProfile\n\nReturned by [`getProfile`](#getprofilesteamid):\n\n```ts\ninterface SteamProfile {\n  steamId: string;\n  personaName: string;\n  avatar: string;\n  accountCreated: Date | null;\n  tradeBanState: string;   // e.g. \"None\"\n  isLimited: boolean;\n  vacBanned: boolean;\n  privacyState: string;    // e.g. \"public\"\n}\n```\n\n### UserDetails\n\nReturned by [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) and\n[`offer.getUserDetails()`](#getuserdetails) — mirrors steamcommunity's `TradeOffer#getUserDetails`:\n\n```ts\ninterface UserDetails {\n  me: UserSideDetails;\n  them: UserPartnerDetails;\n}\n\ninterface UserSideDetails {\n  personaName: string;\n  contexts: Record<string, unknown> | null;\n  escrowDays: number;             // your side; Steam holds for max(me, them) days\n  avatarIcon: string | undefined; // .jpg\n  avatarMedium: string | undefined; // _medium.jpg\n  avatarFull: string | undefined;   // _full.jpg\n}\n\ninterface UserPartnerDetails extends UserSideDetails {\n  probation: boolean; // partner is on Steam-trade probation (see Steam's warning banner)\n}\n```\n\n### EscrowHold\n\nReturned by [`bot.trade.getEscrow`](#getescrowtarget) — escrow-only, in **seconds**. Top-level keys\nrenamed from Steam's `my_escrow`/`their_escrow`/`both_escrow`; inner shape preserved 1:1:\n\n```ts\ninterface EscrowHold {\n  me: EscrowSide;\n  them: EscrowSide;\n  both: EscrowSide;\n}\n\ninterface EscrowSide {\n  escrow_end_duration_seconds: number;\n}\n```\n\n### PollData\n\nThe poll snapshot — persist it via the [`pollData` event](#event-polldata) and pass it back to\n[`startPolling`](#startpollingoptions):\n\n```ts\ninterface PollData {\n  offersSince: number;                          // Unix seconds of the newest processed update\n  sent: Record<string, ETradeOfferState>;       // offer id -> last seen state\n  received: Record<string, ETradeOfferState>;\n  timestamps: Record<string, number>;\n  lastFullUpdate?: number;                       // ms epoch of the last full sweep; persisted so the cadence survives stateless workers\n}\n```\n\n### PollOptions\n\nSee [`startPolling`](#startpollingoptions): `pollInterval?` (default 10000 ms), `pollFullUpdateInterval?`\n(default 300000 ms), `pollData?`, `store?` (a [`PollDataStore`](#polldatastore)), `maxAgeMs?` (default\n30d), `cancelTime?` (off by default).\n\n### PollDataStore\n\nPluggable persistence for the poll snapshot — implement it to keep `pollData` in Redis (or anywhere) so\nany worker can resume a bot's poll. You own one-tick-at-a-time mutual exclusion; the library owns the\ndiff. Use it with [`startPolling`](#startpollingoptions) (timer) or [`pollOnce`](#pollonceoptions) (cron).\n\n```ts\ninterface PollDataStore {\n  load(): Promise<PollData | undefined>;\n  save(pollData: PollData): Promise<void>;\n}\n```\n\n### PollChange\n\nOne offer transition returned by [`pollOnce`](#pollonceoptions) (the same set is also emitted as\n[trade events](#trade-events)):\n\n```ts\ntype PollChange =\n  | { type: \"newOffer\"; offer: TradeOffer }\n  | { type: \"sentOfferChanged\"; offer: TradeOffer; oldState: ETradeOfferState }\n  | { type: \"receivedOfferChanged\"; offer: TradeOffer; oldState: ETradeOfferState }\n  | { type: \"unknownOfferSent\"; offer: TradeOffer };\n```\n\n### TradeOfferUpdate\n\nThe payload of the [`offerUpdate`](#event-offerupdate) event — a unified view of any single offer\nchange. `previousState` is `undefined` the first time an offer is seen; direction is `offer.isOurOffer`:\n\n```ts\ninterface TradeOfferUpdate {\n  offer: TradeOffer;\n  previousState?: ETradeOfferState;\n}\n```\n\n### Confirmation\n\nReturned by [`getConfirmations`](#getconfirmationstime-key): `id`, `type`, `creator`, `key` (nonce),\n`title`, `receiving`, `sending`, `time` (ISO string), `timestamp` (`Date`), `icon`.\n\n---\n\n## Enums\n\nAll are exported and have the standard Steam numeric values.\n\n### ETradeOfferState\n\n`Invalid (1)`, `Active (2)`, `Accepted (3)`, `Countered (4)`, `Expired (5)`, `Canceled (6)`,\n`Declined (7)`, `InvalidItems (8)`, `CreatedNeedsConfirmation (9)`, `CanceledBySecondFactor (10)`,\n`InEscrow (11)`, `Reverted (12)`.\n\n### ETradeStatus\n\n`Init (0)`, `PreCommitted (1)`, `Committed (2)`, `Complete (3)`, `Failed (4)`,\n`PartialSupportRollback (5)`, `FullSupportRollback (6)`, `SupportRollback_Selective (7)`,\n`RollbackFailed (8)`, `RollbackAbandoned (9)`, `InEscrow (10)`, `EscrowRollback (11)`, `Reverted (12)`.\n\n### EConfirmationMethod\n\n`None (0)`, `Email (1)`, `MobileApp (2)`.\n\n### EConfirmationType\n\n`Invalid (0)`, `Generic (1)`, `Trade (2)`, `MarketListing (3)`, `FeatureOptOut (4)`,\n`PhoneNumberChange (5)`, `AccountRecovery (6)`.\n\n### EOfferFilter\n\n`ActiveOnly (1)`, `HistoricalOnly (2)`, `All (3)`.\n\n### EAuthSessionGuardType\n\n`Unknown (0)`, `None (1)`, `EmailCode (2)`, `DeviceCode (3)`, `DeviceConfirmation (4)`,\n`EmailConfirmation (5)`, `MachineToken (6)`, `LegacyMachineAuth (7)`.\n\n### EAuthTokenRevokeAction\n\n`Logout (0)`, `Permanent (1)`, `Replaced (2)`, `Support (3)`, `Consume (4)`, `NonRememberedLogout (5)`,\n`NonRememberedPermanent (6)`, `Automatic (7)`. Used by [`bot.session.logout`](#logoutaction).\n\n### Others\n\n`EAuthTokenPlatformType`, `ESessionPersistence`, `ETokenRenewalType`, and the full `EResult` table are\nalso exported.\n\n---\n\n## Errors\n\nEvery error extends **`SteamError`**, which carries an optional `eresult` (a Steam `EResult`) and `body`\n(the raw response). The library **classifies** errors but does not implement a cooldown policy — retry\nlogic stays in your code.\n\nSteam-supplied messages (the `strError` field on send/accept/cancel responses **and** the\n`<div id=\"error_msg\">` text scraped from the trade page when an inventory load fails) are run through\none shared classifier so the same `instanceof` check works everywhere.\n\n| Class | Extra fields | Meaning |\n| ----- | ------------ | ------- |\n| `SteamError` | `eresult?`, `body?` | Base class for everything. |\n| `HttpStatusError` | `statusCode` | A non-2xx HTTP response. |\n| `SteamSessionExpiredError` | — | The session/token is no longer valid; re-authenticate. |\n| `RateLimitError` | `statusCode?`, `retryAfterMs`, `unlockAt` | Rate limited (HTTP 429 or eresult 84). `unlockAt` is a millisecond epoch when you may retry — **always populated** (a conservative default when Steam gives no hint). |\n| `ProxyError` | `cause?` | A request through a configured proxy failed at the transport layer (unreachable / refused / timeout / auth). Only thrown when a `proxy` is set. |\n| `EscrowError` | `escrowDays` | The trade would be (or is) held in escrow. |\n| `TradeBanError` | — | The account is trade-banned. |\n| `OfferLimitError` | `eresult = 25` | Sent too many offers. |\n| `TargetCannotTradeError` | `eresult?` | Partner is not available to trade (limited, escrow-only, etc.). |\n| `NewDeviceError` | `eresult?` | Steam blocks the action because we logged in from a new device recently. |\n| `ItemServerUnavailableError` | `eresult = 102` | The game's item server (its Game Coordinator) is unreachable — transient, per-app, retry shortly. |\n| `PrivateInventoryError` | — | Partner's inventory is private (or friends-only and we're not friends). A trade URL/token does **not** bypass inventory privacy. |\n| `ConfirmationError` | — | A mobile-confirmation step failed (incl. missing `identitySecret` when calling [`offer.confirm()`](#confirm)). |\n| `FamilyViewError` | — | Family View is restricting the account. |\n| `LoginError` | `extendedErrorMessage?`, `isTransient` | Credential-login failure; `isTransient` is `true` for a retryable blip (timeout / service unavailable) rather than bad credentials. |\n| `NoMobileAuthenticatorError` | (extends `LoginError`) | `sharedSecret` was supplied but the account has no mobile authenticator attached, so TOTP can't answer the guard challenge. Use `steamGuardCode` / `onSteamGuardRequired` with an email code instead. |\n\n`unlockAt` / `retryAfterMs` on `RateLimitError` are never `undefined`, so a cooldown is just\n`cooldownUntil = err.unlockAt`:\n\n```ts\nimport { RateLimitError } from \"@assetpay/steam-mobile\";\n\ntry {\n  await bot.trade.getTradeOffers();\n} catch (err) {\n  if (err instanceof RateLimitError) {\n    await sleepUntil(err.unlockAt);\n  } else throw err;\n}\n```\n\n---\n\n## Mobile-app impersonation\n\nThe client presents as the Steam mobile app on the wire: per-host user agents, the `mobileClient`\ncookie, an `origin=SteamMobile` header on writes, and `mobileconf` confirmations. Pick a preset with\n`mobileProfile: \"ios\"` (default) or `\"android\"`, or override individual fields with an object.\n\nA **`MobileProfile`** has: `mobileClient` (`\"ios\"` | `\"android\"`), `mobileClientVersion`,\n`apiUserAgent`, `webUserAgent`, `deviceFriendlyName` (iOS sends the model id, e.g. `\"iPhone18,3\"`),\n`osType` (signed EOSType; iOS `-600`, Android `-500`), `gamingDeviceType`, and `appType?`. The\n`IOS_PROFILE` and `ANDROID_PROFILE` constants (captured from real apps) and `resolveMobileProfile` are\nexported if you want to build on them.\n\n```ts\nconst bot = new SteamMobile({\n  refreshToken,\n  mobileProfile: { mobileClient: \"ios\", deviceFriendlyName: \"iPhone17,2\" },\n});\n```\n\n---\n\n## Notes & limitations\n\n- **MobileApp-only.** No real-time CM push — offer detection is polling. No Game Coordinator (item data\n  comes from the web `asset_properties`). No chat/presence.\n- **Limited accounts** (haven't spent the $5 that lifts Steam's anti-spam limit) **can still trade** —\n  subject to the usual Steam Guard / escrow holds — but can't use the Community Market or generate a\n  Web API key. This library doesn't use a Web API key (it authenticates with the mobile access token),\n  so limited accounts can send and receive trades through it. `getProfile().isLimited` surfaces the\n  state; `ensureApiKey()` returns `null`.\n- **English only** (`l=english`) by design — no localization dependency.\n- **Settlement is read-only.** `getTradeStatus` surfaces Steam's `new_assetid`/`new_contextid`;\n  reconciling items by inventory diff (the race-prone part) is left to your server, which has the\n  concurrency context. `usedInventoryFallback` is therefore always `false`.\n- **No auto-retry.** Rate limits are thrown as `RateLimitError` — always with a concrete `unlockAt`, so\n  the caller's cooldown needs no guesswork (the poll loop is the one exception: it backs off and resumes).\n\n---\n\n## Development\n\n```bash\npnpm install\npnpm typecheck   # tsc --noEmit (strict NodeNext)\npnpm lint        # biome\npnpm test        # vitest\npnpm build       # tsdown → dist (ESM + d.ts)\npnpm proto       # regenerate src/protobufs from protobufs/*.proto (buf + protoc-gen-es)\n```\n\nLive debug scripts live in `debug/` and read a `.env` for credentials:\n\n```bash\npnpm bootstrap          # credential login → save ./bot.refreshtoken (run once)\npnpm smoke              # read-only health check across the whole API\npnpm watch              # live trade-event watcher — send the bot a trade and watch it fire\npnpm trade              # send → confirm → cancel lifecycle (gated: SEND=1 PARTNER_TRADE_URL=…; needs a non-limited bot)\npnpm partner-inventory  # load a partner's inventory via /partnerinventory/ (PARTNER_TRADE_URL=…; surfaces PrivateInventoryError etc.)\n```\n","readmeFilename":"README.md"}