@assetpay/steam-mobile 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1188 -0
- package/dist/index.d.mts +1632 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2984 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +80 -0
package/README.md
ADDED
|
@@ -0,0 +1,1188 @@
|
|
|
1
|
+
# Steam Mobile
|
|
2
|
+
|
|
3
|
+
A headless, fully-typed **ESM TypeScript** client for the **Steam mobile app**, built for trading. It
|
|
4
|
+
unifies the *web* logic of [`steamcommunity`](https://github.com/DoctorMcKay/node-steamcommunity) and
|
|
5
|
+
[`steam-tradeoffer-manager`](https://github.com/DoctorMcKay/node-steam-tradeoffer-manager) into one
|
|
6
|
+
cohesive, Promise-native package — with **no `steam-user` and no Steam CM binary protocol**.
|
|
7
|
+
|
|
8
|
+
Everything the mobile app needs is in-package: credential login, TOTP, mobile confirmations
|
|
9
|
+
(`mobileconf`), and the `IAuthenticationService` protobufs. The only runtime dependencies are
|
|
10
|
+
[`got`](https://github.com/sindresorhus/got), [`proxy-agent`](https://github.com/TooTallNate/proxy-agents),
|
|
11
|
+
[`tough-cookie`](https://github.com/salesforce/tough-cookie), [`steamid`](https://github.com/DoctorMcKay/node-steamid),
|
|
12
|
+
and [`@bufbuild/protobuf`](https://github.com/bufbuild/protobuf-es).
|
|
13
|
+
|
|
14
|
+
The client impersonates the Steam mobile app on the wire (a MobileApp-platform access token, per-host
|
|
15
|
+
user agents, mobile cookies, and `mobileconf` confirmations). MobileApp is the only platform whose
|
|
16
|
+
access token can self-renew over plain HTTP, which is what lets this library run headless without a
|
|
17
|
+
persistent CM connection.
|
|
18
|
+
|
|
19
|
+
## Table of contents
|
|
20
|
+
|
|
21
|
+
- [Installation](#installation)
|
|
22
|
+
- [The basics](#the-basics)
|
|
23
|
+
- [Logging in](#logging-in)
|
|
24
|
+
- [SteamMobile](#steammobile) — the root client
|
|
25
|
+
- [bot.trade](#bottrade) — sending, reading, and polling trade offers
|
|
26
|
+
- [TradeOffer](#tradeoffer) — a single offer
|
|
27
|
+
- [bot.community](#botcommunity) — inventories, profiles, trade URLs, API keys
|
|
28
|
+
- [bot.session](#botsession) — token lifecycle and sessions
|
|
29
|
+
- [bot.confirmations](#botconfirmations) — low-level mobile confirmations
|
|
30
|
+
- [decodePreviewToken](#decodepreviewtoken) — decode CS2 item inspect/certificate data
|
|
31
|
+
- [Data types](#data-types)
|
|
32
|
+
- [Enums](#enums)
|
|
33
|
+
- [Errors](#errors)
|
|
34
|
+
- [Mobile-app impersonation](#mobile-app-impersonation)
|
|
35
|
+
- [Notes & limitations](#notes--limitations)
|
|
36
|
+
- [Development](#development)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm add @assetpay/steam-mobile # Node 22+, ESM only
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This package is ESM-only and targets Node 22+. Import it with `import { SteamMobile } from "@assetpay/steam-mobile"`.
|
|
47
|
+
|
|
48
|
+
## The basics
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { SteamMobile } from "@assetpay/steam-mobile";
|
|
52
|
+
|
|
53
|
+
const bot = await new SteamMobile({
|
|
54
|
+
refreshToken: process.env.STEAM_REFRESH_TOKEN!,
|
|
55
|
+
identitySecret: process.env.STEAM_IDENTITY_SECRET, // only needed to confirm trades
|
|
56
|
+
}).login();
|
|
57
|
+
|
|
58
|
+
console.log("Logged in as", bot.steamID.getSteamID64());
|
|
59
|
+
|
|
60
|
+
const { sent, received } = await bot.trade.getTradeOffers();
|
|
61
|
+
console.log(`${sent.length} sent, ${received.length} received`);
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Construction is **synchronous** and does no network I/O — your `steamID` is known immediately because
|
|
65
|
+
it is encoded in the refresh-token JWT. The single network step is `await bot.login()`, which mints the
|
|
66
|
+
access token and applies the login cookie. Every method awaits the token lazily, so you *can* call
|
|
67
|
+
methods before `login()`, but calling `login()` up front gives you fail-fast behavior and lets you
|
|
68
|
+
auto-start polling.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Logging in
|
|
73
|
+
|
|
74
|
+
There are two ways to get a session. In production you almost always want the **refresh-token** path:
|
|
75
|
+
log in once with credentials, store the long-lived refresh token, and reuse it.
|
|
76
|
+
|
|
77
|
+
### From a refresh token (the normal path)
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const bot = await new SteamMobile({ refreshToken }).login();
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
A MobileApp refresh token lives ~211 days. The library renews the ~24-hour access token lazily and
|
|
84
|
+
rotates the refresh token automatically as it nears end-of-life — emitting a [`refreshToken`](#event-refreshtoken)
|
|
85
|
+
event each time so you can persist the new one.
|
|
86
|
+
|
|
87
|
+
### From credentials
|
|
88
|
+
|
|
89
|
+
When you don't yet have a refresh token, use `loginWithCredentials`. This drives the full
|
|
90
|
+
`IAuthenticationService` credential flow (RSA password encryption → begin session → answer Steam Guard
|
|
91
|
+
→ poll) entirely in-package — no `steam-session` needed.
|
|
92
|
+
|
|
93
|
+
#### loginWithCredentials(options)
|
|
94
|
+
|
|
95
|
+
- `options`
|
|
96
|
+
- `username` — Your Steam account name (login name, not persona).
|
|
97
|
+
- `password` — Your account password.
|
|
98
|
+
- `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.
|
|
99
|
+
- `steamGuardCode` — Optional. A Steam Guard code you supply yourself (email or device).
|
|
100
|
+
- `proxy` — Optional. A proxy URL; routes the entire login flow.
|
|
101
|
+
- `mobileProfile` — Optional. `"ios"` (default), `"android"`, or an object overriding individual [profile fields](#mobile-app-impersonation).
|
|
102
|
+
- `signal` — Optional. An `AbortSignal`; aborting stops polling and rejects with a `LoginError`.
|
|
103
|
+
- `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.
|
|
104
|
+
|
|
105
|
+
Returns a `Promise` that resolves to a **`LoginResult`**:
|
|
106
|
+
|
|
107
|
+
- `refreshToken` — The MobileApp refresh token. **Store this** and feed it to `new SteamMobile({ refreshToken })`.
|
|
108
|
+
- `accessToken` — The freshly-minted access token, or `undefined` if Steam didn't return one.
|
|
109
|
+
- `steamId` — The account's SteamID64, as a string.
|
|
110
|
+
- `username` — The account name Steam echoed back.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { loginWithCredentials, SteamMobile } from "@assetpay/steam-mobile";
|
|
114
|
+
|
|
115
|
+
const { refreshToken } = await loginWithCredentials({
|
|
116
|
+
username: "myaccount",
|
|
117
|
+
password: "…",
|
|
118
|
+
sharedSecret: "…", // answers the device-code 2FA automatically
|
|
119
|
+
// onSteamGuardRequired: async ({ message }) => await promptUser(message),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const bot = await new SteamMobile({ refreshToken }).login();
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Credential login is rate-limited by Steam, so do it once per device and reuse the refresh token. For
|
|
126
|
+
finer-grained control over the state machine (events for each step, manual code submission), use the
|
|
127
|
+
[`CredentialSession`](#credentialsession-low-level) class directly.
|
|
128
|
+
|
|
129
|
+
#### CredentialSession (low-level)
|
|
130
|
+
|
|
131
|
+
`loginWithCredentials` is a thin one-shot wrapper around this `EventEmitter`. Use it directly when you
|
|
132
|
+
need to react to each step.
|
|
133
|
+
|
|
134
|
+
- `new CredentialSession(http, profile[, pollTimeoutMs])` — `http` is an `HttpClient`, `profile` a resolved [`MobileProfile`](#mobile-app-impersonation), `pollTimeoutMs` defaults to 180000.
|
|
135
|
+
- `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>`.
|
|
136
|
+
- `submitSteamGuardCode(code)` — Supplies a code in response to a `steamGuardRequired` event. Returns `Promise<void>`.
|
|
137
|
+
- `stop()` — Aborts the flow and clears the poll timer.
|
|
138
|
+
- Properties (populated as the flow progresses): `steamID?`, `username`, `accessToken?`, `refreshToken?`.
|
|
139
|
+
- Events:
|
|
140
|
+
- `authenticated` — A refresh token is available; read it off the instance.
|
|
141
|
+
- `steamGuardRequired` — `{ type, message }`. No code could be supplied automatically; call `submitSteamGuardCode()`.
|
|
142
|
+
- `remoteInteraction` — Steam is waiting for the user to approve on their phone (device/email confirmation).
|
|
143
|
+
- `timeout` — The confirmation window elapsed.
|
|
144
|
+
- `error` — `error`. The flow failed.
|
|
145
|
+
- `debug` — `message`. Verbose step logging.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## SteamMobile
|
|
150
|
+
|
|
151
|
+
The root client. It owns the HTTP layer, the session, and the namespaces, and it re-emits every
|
|
152
|
+
[trade event](#trade-events) so you can listen on `bot` directly instead of `bot.trade`.
|
|
153
|
+
|
|
154
|
+
### Constructor: new SteamMobile(options)
|
|
155
|
+
|
|
156
|
+
- `options`
|
|
157
|
+
- `refreshToken` — **Required.** A MobileApp refresh token (from `loginWithCredentials` or a prior session).
|
|
158
|
+
- `identitySecret` — Optional. Your `identity_secret`, required to **confirm** trades and other mobile confirmations. Read-only operations and sending don't need it.
|
|
159
|
+
- `proxy` — Optional. A proxy URL (`http://user:pass@host:port`, `socks5://…`, etc.). All traffic — API, community, and confirmations — routes through it.
|
|
160
|
+
- `mobileProfile` — Optional. `"ios"` (default), `"android"`, or a `Partial<MobileProfile>` to override individual fields. See [Mobile-app impersonation](#mobile-app-impersonation).
|
|
161
|
+
- `polling` — Optional. `true` to start polling with default cadence after `login()`, or a [`PollOptions`](#polloptions) object to tune it / resume from saved `pollData`.
|
|
162
|
+
|
|
163
|
+
Throws `SteamSessionExpiredError` synchronously if the refresh token is malformed (no SteamID in its
|
|
164
|
+
payload). Does **not** make any network request.
|
|
165
|
+
|
|
166
|
+
### Properties
|
|
167
|
+
|
|
168
|
+
- `steamID` — A [`SteamID`](https://github.com/DoctorMcKay/node-steamid) object for the logged-in account. Available immediately after construction.
|
|
169
|
+
- `accessToken` — The current access token (`string`), or `undefined` before the first mint.
|
|
170
|
+
- `refreshToken` — The current refresh token (`string`). This changes when Steam rotates it; listen for the [`refreshToken`](#event-refreshtoken) event to persist updates.
|
|
171
|
+
- `identitySecret` — The `identity_secret` you passed, or `undefined`.
|
|
172
|
+
- `trade` — The [TradeNamespace](#bottrade).
|
|
173
|
+
- `community` — The [CommunityNamespace](#botcommunity).
|
|
174
|
+
- `session` — The [SessionManager](#botsession).
|
|
175
|
+
- `confirmations` — The [ConfirmationManager](#botconfirmations).
|
|
176
|
+
- `http`, `api` — The low-level `HttpClient` and `WebApiClient`. You normally won't need these.
|
|
177
|
+
|
|
178
|
+
### login()
|
|
179
|
+
|
|
180
|
+
Mints the access token (failing fast if the refresh token is dead), applies the `steamLoginSecure`
|
|
181
|
+
login cookie, and — if you passed `polling` — starts the poll loop. Returns a `Promise` that resolves
|
|
182
|
+
to the `SteamMobile` instance itself, so you can write `const bot = await new SteamMobile(opts).login()`.
|
|
183
|
+
|
|
184
|
+
### reauthenticate(credentials)
|
|
185
|
+
|
|
186
|
+
- `credentials` — A `ReauthenticateOptions`: the same fields as [`loginWithCredentials`](#loginwithcredentialsoptions) **except** `proxy` / `mobileProfile`, which are reused from this instance.
|
|
187
|
+
|
|
188
|
+
Recovers a **dead or revoked** refresh token — one the library can no longer renew (past its ~211-day
|
|
189
|
+
life, after a password change, or a Steam revocation). Runs a fresh credential login, swaps the new
|
|
190
|
+
refresh token in place, re-mints the access token, and re-emits [`refreshToken`](#event-refreshtoken) so
|
|
191
|
+
you can persist it. **Rejects if the credentials are for a different account** than this instance.
|
|
192
|
+
Returns `Promise<void>`. Pair it with the [`sessionExpired`](#event-sessionexpired) event.
|
|
193
|
+
|
|
194
|
+
### ensureApiKey(\[domain])
|
|
195
|
+
|
|
196
|
+
- `domain` — Optional. The domain to register the key under (default `"assetpay.gg"`). Steam's API-key `domain` is a hostname label, not a URL.
|
|
197
|
+
|
|
198
|
+
Returns the account's existing Web API key, or registers a new one (auto-accepting the mobile
|
|
199
|
+
confirmation prompt if `identitySecret` is set), as a `Promise<string | null>`. Resolves to `null` when
|
|
200
|
+
the account is **ineligible** for a key (unverified email, no mobile authenticator, or a
|
|
201
|
+
[limited account](#notes--limitations)). This is a convenience passthrough to
|
|
202
|
+
[`bot.community.ensureApiKey`](#communityensureapikeydomain).
|
|
203
|
+
|
|
204
|
+
### request(method, url\[, opts]) · get(url\[, opts]) · post(url\[, opts])
|
|
205
|
+
|
|
206
|
+
Escape hatch for endpoints the typed namespaces don't cover (like `node-steamcommunity`'s
|
|
207
|
+
`httpRequest`). Each ensures the session is live — minting/renewing the access token and applying the
|
|
208
|
+
`steamLoginSecure` cookie — *before* sending, so the request carries your logged-in session, then
|
|
209
|
+
delegates to [`bot.http`](#properties). Throws `SteamSessionExpiredError` if the session can't be
|
|
210
|
+
renewed.
|
|
211
|
+
|
|
212
|
+
- `method` — `"GET"` or `"POST"`.
|
|
213
|
+
- `url` — Absolute URL.
|
|
214
|
+
- `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`).
|
|
215
|
+
|
|
216
|
+
Returns `Promise<HttpResponse<T>>` (`{ statusCode, headers, body }`). Pass a type parameter and
|
|
217
|
+
`responseType: "json"` to get a parsed body.
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
const { body } = await bot.get<{ success: boolean }>(
|
|
221
|
+
"https://steamcommunity.com/market/priceoverview/",
|
|
222
|
+
{ searchParams: { appid: 730, market_hash_name: "Mag-7 | Heat (Field-Tested)", currency: 1 }, responseType: "json" },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
await bot.post("https://steamcommunity.com/some/endpoint", {
|
|
226
|
+
json: { sessionid: await bot.http.getSessionId(), foo: "bar" },
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
For requests where you *don't* want the session ensured first, use `bot.http` directly.
|
|
231
|
+
|
|
232
|
+
### shutdown()
|
|
233
|
+
|
|
234
|
+
Stops the poll loop and removes all event listeners (on both `bot` and `bot.trade`). Call this when
|
|
235
|
+
you're done with the client so it doesn't keep timers alive. Returns a `Promise<void>`.
|
|
236
|
+
|
|
237
|
+
### Events
|
|
238
|
+
|
|
239
|
+
`SteamMobile` re-emits all [trade events](#trade-events) (`newOffer`, `sentOfferChanged`,
|
|
240
|
+
`receivedOfferChanged`, `unknownOfferSent`, `pollData`, `pollSuccess`, `pollFailure`, `debug`) plus two
|
|
241
|
+
of its own:
|
|
242
|
+
|
|
243
|
+
#### Event: refreshToken
|
|
244
|
+
|
|
245
|
+
- `token` — The new refresh token (`string`).
|
|
246
|
+
|
|
247
|
+
Emitted when Steam rotates your refresh token (which happens automatically near end-of-life). **Persist
|
|
248
|
+
the new value** — the old one stops working.
|
|
249
|
+
|
|
250
|
+
#### Event: sessionExpired
|
|
251
|
+
|
|
252
|
+
- `error` — A `SteamSessionExpiredError`.
|
|
253
|
+
|
|
254
|
+
Emitted when the refresh token is rejected or expired and the session can no longer be renewed. It fires
|
|
255
|
+
only on a **confirmed-terminal** auth failure (not a transient blip), so it's safe to trigger re-auth
|
|
256
|
+
on: call [`bot.reauthenticate(credentials)`](#reauthenticatecredentials).
|
|
257
|
+
|
|
258
|
+
---
|
|
259
|
+
|
|
260
|
+
## bot.trade
|
|
261
|
+
|
|
262
|
+
The trade namespace. It's an `EventEmitter` (see [Trade events](#trade-events)) and holds every method for
|
|
263
|
+
reading, sending, and watching trade offers.
|
|
264
|
+
|
|
265
|
+
### createOffer(target)
|
|
266
|
+
|
|
267
|
+
- `target` — An [`OfferTarget`](#offertarget): either `{ tradeUrl }` or `{ steamId, token? }`.
|
|
268
|
+
|
|
269
|
+
Creates a new, empty [TradeOffer](#tradeoffer) addressed to the partner. **Local only** — nothing is
|
|
270
|
+
sent to Steam until you call [`offer.send()`](#send). Chain `.give()` / `.receive()` / `.setMessage()`
|
|
271
|
+
to build it.
|
|
272
|
+
|
|
273
|
+
### getTradeOffer(id)
|
|
274
|
+
|
|
275
|
+
- `id` — The trade offer's numeric id, as a string.
|
|
276
|
+
|
|
277
|
+
Fetches a single offer and returns it as a [TradeOffer](#tradeoffer). Item descriptions are inlined
|
|
278
|
+
(`get_descriptions=1`), so the offer's items are full [`EconItem`](#econitem)s. Rejects with a
|
|
279
|
+
`SteamError` if the offer doesn't exist.
|
|
280
|
+
|
|
281
|
+
### getTradeOffers(\[filter]\[, historicalCutoff])
|
|
282
|
+
|
|
283
|
+
- `filter` — Optional. A value from [`EOfferFilter`](#eofferfilter) (default `ActiveOnly`).
|
|
284
|
+
- `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").
|
|
285
|
+
|
|
286
|
+
Returns `Promise<{ sent: TradeOffer[]; received: TradeOffer[] }>`. This cursor-paginates **all** pages
|
|
287
|
+
internally and inlines descriptions into every offer's items. If Steam returns the wholly-malformed
|
|
288
|
+
"data temporarily unavailable" glitch (all offers missing partner/items), this rejects with a
|
|
289
|
+
`SteamError` rather than handing back garbage.
|
|
290
|
+
|
|
291
|
+
### getTradeStatus({tradeId})
|
|
292
|
+
|
|
293
|
+
- `tradeId` — The **trade id** (not the offer id). This is set on a [TradeOffer](#tradeoffer) once it's accepted (`offer.tradeID`).
|
|
294
|
+
|
|
295
|
+
Returns settlement details for a completed or escrowed trade as a `Promise<`[`ExchangeDetails`](#exchangedetails)`>`,
|
|
296
|
+
including where each item *landed* after the trade (`new_assetid` / `new_contextid`, when Steam provides
|
|
297
|
+
them). See [`offer.getTradeStatus()`](#gettradestatus) for the offer-level shortcut.
|
|
298
|
+
|
|
299
|
+
### getUserDetails(target)
|
|
300
|
+
|
|
301
|
+
- `target` — An [`OfferTarget`](#offertarget) (typically `{ tradeUrl }`).
|
|
302
|
+
|
|
303
|
+
Persona, contexts, escrow days, avatars, and partner probation for both sides — same scrape as
|
|
304
|
+
[`offer.getUserDetails()`](#getuserdetails) but addressed by target, so you don't need an offer in
|
|
305
|
+
hand. Returns `Promise<`[`UserDetails`](#userdetails)`>`. **Steam applies
|
|
306
|
+
`max(me.escrowDays, them.escrowDays)`** as the actual hold — compute that yourself if you need a
|
|
307
|
+
single number.
|
|
308
|
+
|
|
309
|
+
### getEscrow(target)
|
|
310
|
+
|
|
311
|
+
- `target` — An [`OfferTarget`](#offertarget) (typically `{ tradeUrl }`).
|
|
312
|
+
|
|
313
|
+
Escrow hold (in **seconds**) via `IEconService/GetTradeHoldDurations`. Returns
|
|
314
|
+
`Promise<`[`EscrowHold`](#escrowhold)`>` shaped `{ me, them, both }` (top-level renamed from Steam's
|
|
315
|
+
`my_escrow` / `their_escrow` / `both_escrow`; inner shape preserved 1:1). Lightweight — just the
|
|
316
|
+
WebAPI, no scrape, no other user data. For everything together (escrow + persona + avatars +
|
|
317
|
+
contexts), use [`getUserDetails`](#getuserdetailstarget) above (or [`offer.getUserDetails()`](#getuserdetails))
|
|
318
|
+
instead.
|
|
319
|
+
|
|
320
|
+
### getTradeHistory(\[options])
|
|
321
|
+
|
|
322
|
+
- `options` — Optional [`TradeHistoryOptions`](#tradehistoryoptions):
|
|
323
|
+
- `maxTrades` — Max trades to return (default 100).
|
|
324
|
+
- `startAfterTime` / `startAfterTradeId` — Cursor: the time and id of the last trade from the previous page. Pass both to fetch the next page.
|
|
325
|
+
- `navigatingBack` — Page backwards (toward newer trades) instead of forward.
|
|
326
|
+
- `includeFailed` — Include failed/rolled-back trades.
|
|
327
|
+
- `includeTotal` — Ask Steam for the total trade count (populates `totalTrades`).
|
|
328
|
+
|
|
329
|
+
Returns past trades, newest first, as a `Promise<`[`TradeHistory`](#tradehistory)`>`. Each entry is a
|
|
330
|
+
fully-parsed [`ExchangeDetails`](#exchangedetails) plus its `tradeId` and `partnerSteamId`. To page,
|
|
331
|
+
take the last entry's `tradeInitTime`/`tradeId` and pass them as `startAfterTime`/`startAfterTradeId`
|
|
332
|
+
while `more` is `true`.
|
|
333
|
+
|
|
334
|
+
### getTradeOffersSummary()
|
|
335
|
+
|
|
336
|
+
Returns counts of pending/new/historical sent & received offers (and escrow counts) as a
|
|
337
|
+
`Promise<`[`TradeOffersSummary`](#tradeofferssummary)`>`. Cheap; useful as a "do I have anything to
|
|
338
|
+
look at?" check before a full `getOffers`.
|
|
339
|
+
|
|
340
|
+
### reconcile(ids)
|
|
341
|
+
|
|
342
|
+
- `ids` — An array of trade offer id strings.
|
|
343
|
+
|
|
344
|
+
Re-reads each id in parallel and returns a `Promise<Map<string, TradeOffer>>` keyed by id. **Missing or
|
|
345
|
+
errored ids are skipped**, so one bad id can't fail the whole batch. This is the authoritative,
|
|
346
|
+
poll-free way to track offers you've sent: persist the id from `send()`, then reconcile it on demand.
|
|
347
|
+
|
|
348
|
+
### getOffersContainingItems(items\[]\[, includeInactive])
|
|
349
|
+
|
|
350
|
+
- `items` — An array of `{ appid, contextid, assetid }` item references.
|
|
351
|
+
- `includeInactive` — Optional. If `true`, searches all offers; otherwise active only (default `false`).
|
|
352
|
+
|
|
353
|
+
Returns the offers that contain **all** of the given items, as a `Promise<TradeOffer[]>`.
|
|
354
|
+
|
|
355
|
+
### getInventory(target, appid\[, contextid]\[, options])
|
|
356
|
+
|
|
357
|
+
- `target` — An [`OfferTarget`](#offertarget) identifying whose inventory to load (typically a partner via `{ tradeUrl }`).
|
|
358
|
+
- `appid` — The app id (e.g. `730` for CS2, `252490` for Rust).
|
|
359
|
+
- `contextid` — Optional. The context id (default `"2"`).
|
|
360
|
+
- `options`
|
|
361
|
+
- `tradableOnly` — Optional. If `true`, only tradable items are returned (default `false`).
|
|
362
|
+
|
|
363
|
+
Loads a **partner's** inventory through the trade-offer page (`/tradeoffer/new/partnerinventory/`),
|
|
364
|
+
which is the reliable way to see a partner's items — including trade-protected ones that the public
|
|
365
|
+
`/inventory/` endpoint hides. Returns `Promise<`[`EconItem`](#econitem)`[]>`, paginated automatically.
|
|
366
|
+
For your *own* inventory, prefer [`bot.community.getInventory`](#getinventoryappid-contextid-options).
|
|
367
|
+
|
|
368
|
+
On failure, the partner's trade page is lazily scraped for Steam's `<div id="error_msg">` and the
|
|
369
|
+
message is classified into the right typed [error](#errors) (`PrivateInventoryError`,
|
|
370
|
+
`TradeBanError`, `TargetCannotTradeError`, `ItemServerUnavailableError`, …).
|
|
371
|
+
|
|
372
|
+
### startPolling(\[options])
|
|
373
|
+
|
|
374
|
+
- `options` — Optional [`PollOptions`](#polloptions):
|
|
375
|
+
- `pollInterval` — Active-poll cadence in ms (default `10000`).
|
|
376
|
+
- `pollFullUpdateInterval` — Full-sweep cadence in ms (default `300000`).
|
|
377
|
+
- `pollData` — A saved [`PollData`](#polldata) snapshot to resume from (so a restart doesn't re-emit known offers).
|
|
378
|
+
- `store` — A [`PollDataStore`](#polldatastore) loaded on start and saved after each changed tick (e.g. Redis-backed). Default: in-memory only.
|
|
379
|
+
- `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.
|
|
380
|
+
- `cancelTime` — If set, auto-cancels a sent offer still `Active` this many ms after its last update, emitting [`sentOfferCanceled`](#event-sentoffercanceled). Off by default.
|
|
381
|
+
|
|
382
|
+
Starts the poll loop. The library does an **active poll** (recently-changed offers only) on the short
|
|
383
|
+
interval and a periodic **full sweep** (offers updated within the retention window, to catch backdated
|
|
384
|
+
state changes) on the long interval. State changes are diffed against the last snapshot and surfaced as
|
|
385
|
+
[events](#trade-events). The loop never dies on a rate limit — it backs off and resumes. Calling this
|
|
386
|
+
again replaces the running loop.
|
|
387
|
+
|
|
388
|
+
### stopPolling()
|
|
389
|
+
|
|
390
|
+
Stops the poll loop.
|
|
391
|
+
|
|
392
|
+
### pollOnce(\[options])
|
|
393
|
+
|
|
394
|
+
- `options` — Optional. A [`PollOptions`](#polloptions) (used to configure the lazily-created poller on first call) plus `forceFull?` to force a full sweep this cycle.
|
|
395
|
+
|
|
396
|
+
Runs a **single, timer-less** poll cycle, for driving cadence from an external scheduler (e.g. a BullMQ
|
|
397
|
+
cron) instead of the built-in timer. Returns `Promise<{ changes:`[`PollChange`](#pollchange)`[]; pollData: PollData }>`.
|
|
398
|
+
With a [`store`](#polldatastore) it loads the snapshot, diffs, emits the usual [events](#trade-events),
|
|
399
|
+
saves, and returns the diff. `lastFullUpdate` is persisted inside `pollData`, so a fresh instance per
|
|
400
|
+
job still keeps the full-sweep cadence (it won't degrade to a full sweep every call). The poller is
|
|
401
|
+
created lazily and reused, so pass config once. Unlike the timer loop, this **throws** on a fetch
|
|
402
|
+
failure so your scheduler can handle it.
|
|
403
|
+
|
|
404
|
+
### pollData (getter)
|
|
405
|
+
|
|
406
|
+
A getter returning the current [`PollData`](#polldata) snapshot, or `undefined` if polling isn't
|
|
407
|
+
running. Usually you persist this via the [`pollData` event](#event-polldata) rather than reading it
|
|
408
|
+
directly.
|
|
409
|
+
|
|
410
|
+
### Trade events
|
|
411
|
+
|
|
412
|
+
`bot.trade` (and `bot`, which re-emits them) emits:
|
|
413
|
+
|
|
414
|
+
#### Event: newOffer
|
|
415
|
+
|
|
416
|
+
- `offer` — A [TradeOffer](#tradeoffer) for the newly-received offer.
|
|
417
|
+
|
|
418
|
+
Emitted when someone sends you a new trade offer.
|
|
419
|
+
|
|
420
|
+
#### Event: receivedOfferChanged
|
|
421
|
+
|
|
422
|
+
- `offer` — The [TradeOffer](#tradeoffer) in its new state.
|
|
423
|
+
- `oldState` — The previous [`ETradeOfferState`](#etradeofferstate).
|
|
424
|
+
|
|
425
|
+
Emitted when an offer you received changes state (e.g. you accepted it elsewhere, or it expired).
|
|
426
|
+
|
|
427
|
+
#### Event: sentOfferChanged
|
|
428
|
+
|
|
429
|
+
- `offer` — The [TradeOffer](#tradeoffer) in its new state.
|
|
430
|
+
- `oldState` — The previous [`ETradeOfferState`](#etradeofferstate).
|
|
431
|
+
|
|
432
|
+
Emitted when an offer you sent changes state (e.g. the partner accepted or declined it).
|
|
433
|
+
|
|
434
|
+
#### Event: unknownOfferSent
|
|
435
|
+
|
|
436
|
+
- `offer` — The [TradeOffer](#tradeoffer).
|
|
437
|
+
|
|
438
|
+
Emitted when polling sees a sent offer it has no record of — either sent out-of-band (not via this
|
|
439
|
+
client) or seen on the first cold poll.
|
|
440
|
+
|
|
441
|
+
#### Event: sentOfferCanceled
|
|
442
|
+
|
|
443
|
+
- `offer` — The [TradeOffer](#tradeoffer) that was canceled.
|
|
444
|
+
- `reason` — Why it was canceled; currently always `"cancelTime"`.
|
|
445
|
+
|
|
446
|
+
Emitted when the poll loop auto-cancels a sent offer because it stayed `Active` past
|
|
447
|
+
[`cancelTime`](#startpollingoptions). The resulting `Active → Canceled` transition also surfaces as a
|
|
448
|
+
normal [`sentOfferChanged`](#event-sentofferchanged) on a later poll.
|
|
449
|
+
|
|
450
|
+
#### Event: offerUpdate
|
|
451
|
+
|
|
452
|
+
- `update` — A [`TradeOfferUpdate`](#tradeofferupdate) `{ offer, previousState? }`.
|
|
453
|
+
|
|
454
|
+
Fires once for **every** offer change, alongside the specific named event above. `previousState` is
|
|
455
|
+
`undefined` the first time an offer is observed; `offer.isOurOffer` distinguishes sent vs received. Use
|
|
456
|
+
this when you want a single handler for all changes (e.g. forwarding to a queue); use the named events
|
|
457
|
+
when you want to react to one kind.
|
|
458
|
+
|
|
459
|
+
#### Event: pollData
|
|
460
|
+
|
|
461
|
+
- `data` — The new [`PollData`](#polldata) snapshot.
|
|
462
|
+
|
|
463
|
+
Emitted whenever the poll snapshot changes. **Persist this** and pass it back as `pollData` on the next
|
|
464
|
+
`startPolling` to resume without re-emitting offers you've already handled.
|
|
465
|
+
|
|
466
|
+
#### Event: pollSuccess
|
|
467
|
+
|
|
468
|
+
Emitted after each successful poll cycle.
|
|
469
|
+
|
|
470
|
+
#### Event: pollFailure
|
|
471
|
+
|
|
472
|
+
- `error` — The `Error` that caused the failed poll.
|
|
473
|
+
|
|
474
|
+
Emitted when a poll cycle fails. The loop keeps running; this is informational.
|
|
475
|
+
|
|
476
|
+
#### Event: debug
|
|
477
|
+
|
|
478
|
+
- `message` — A human-readable debug string.
|
|
479
|
+
|
|
480
|
+
Verbose internal logging (cursor pages, mint/renewal, backoff). Listen on this while developing.
|
|
481
|
+
|
|
482
|
+
---
|
|
483
|
+
|
|
484
|
+
## TradeOffer
|
|
485
|
+
|
|
486
|
+
Represents a single trade offer — either one you're building to send, or one fetched from Steam. Build
|
|
487
|
+
offers with [`bot.trade.createOffer`](#createoffertarget); fetch them with
|
|
488
|
+
[`bot.trade.getTradeOffer`](#gettradeofferid) / `getOffers` / `getTradeOffers`.
|
|
489
|
+
|
|
490
|
+
### Offer properties
|
|
491
|
+
|
|
492
|
+
- `id` — The offer id (`string`), or `undefined` before it's been sent.
|
|
493
|
+
- `partner` — A [`SteamID`](https://github.com/DoctorMcKay/node-steamid) for the other party.
|
|
494
|
+
- `token` — The partner's trade-URL token (`string`), if known.
|
|
495
|
+
- `message` — The offer message (≤128 chars).
|
|
496
|
+
- `state` — The current [`ETradeOfferState`](#etradeofferstate).
|
|
497
|
+
- `itemsToGive` — Array of [`TradeItem`](#tradeitem) (full [`EconItem`](#econitem)s on fetched offers) you'd give up.
|
|
498
|
+
- `itemsToReceive` — Array of items you'd receive.
|
|
499
|
+
- `isOurOffer` — `true` if we sent it, `false` if we received it.
|
|
500
|
+
- `tradeID` — The trade id once accepted (`string`), or `undefined`. Needed for [`getTradeStatus()`](#gettradestatus).
|
|
501
|
+
- `confirmationMethod` — An [`EConfirmationMethod`](#econfirmationmethod) (`MobileApp`, `Email`, or `None`).
|
|
502
|
+
- `escrowEnds` — A `Date` when the Steam Guard escrow hold ends, or `undefined`.
|
|
503
|
+
- `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`.
|
|
504
|
+
- `delaySettlement` — `true` when the trade's items are subject to the trade-protection settlement delay.
|
|
505
|
+
- `created` / `updated` / `expires` — `Date`s, or `undefined`.
|
|
506
|
+
- `fromRealTimeTrade` — `true` if this came from a real-time trade session.
|
|
507
|
+
- `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.
|
|
508
|
+
|
|
509
|
+
### give(items)
|
|
510
|
+
|
|
511
|
+
- `items` — An array of [`TradeItem`](#tradeitem) (`{ appid, contextid, assetid, amount? }`).
|
|
512
|
+
|
|
513
|
+
Adds items **you** will give. Chainable (returns the offer). Array-only.
|
|
514
|
+
|
|
515
|
+
### receive(items)
|
|
516
|
+
|
|
517
|
+
- `items` — An array of [`TradeItem`](#tradeitem).
|
|
518
|
+
|
|
519
|
+
Adds items you'll receive from the partner. Chainable.
|
|
520
|
+
|
|
521
|
+
### setMessage(message)
|
|
522
|
+
|
|
523
|
+
- `message` — The offer message (truncated to 128 characters).
|
|
524
|
+
|
|
525
|
+
Sets the message. Chainable. Throws if the offer has already been sent.
|
|
526
|
+
|
|
527
|
+
### send()
|
|
528
|
+
|
|
529
|
+
Sends the offer. Before sending it best-effort acknowledges the 2025 trade-protection notice (which
|
|
530
|
+
Steam otherwise blocks new offers on). Returns a `Promise` resolving to a **`SendResult`**:
|
|
531
|
+
|
|
532
|
+
- `"sent"` — The offer is live; `offer.id` and `offer.state` (`Active`) are now set.
|
|
533
|
+
- `"needs_confirmation"` — The offer was created but needs a mobile/email confirmation. Call [`confirm()`](#confirm).
|
|
534
|
+
|
|
535
|
+
Throws a typed [error](#errors) on failure (e.g. `OfferLimitError`, `TradeBanError`,
|
|
536
|
+
`SteamSessionExpiredError`). Throws if the offer is empty or already sent.
|
|
537
|
+
|
|
538
|
+
### accept()
|
|
539
|
+
|
|
540
|
+
Accepts an offer **you received**. Returns a `Promise` resolving to an **`AcceptResult`**:
|
|
541
|
+
|
|
542
|
+
- `"accepted"` — Done; items have transferred.
|
|
543
|
+
- `"escrow"` — Accepted but held in escrow (`offer.escrowEnds` tells you until when).
|
|
544
|
+
- `"needs_confirmation"` — Needs a mobile/email confirmation; call [`confirm()`](#confirm).
|
|
545
|
+
|
|
546
|
+
Because the accept response doesn't itself flag escrow, this re-reads the offer afterward to tell a
|
|
547
|
+
held trade from a settled one (best-effort; falls back to `"accepted"`). Throws if the offer isn't
|
|
548
|
+
active, is our own, or on a Steam error.
|
|
549
|
+
|
|
550
|
+
### cancel()
|
|
551
|
+
|
|
552
|
+
Cancels an offer you sent (or, for a received offer, declines it) via the community endpoint — the Web
|
|
553
|
+
API rejects the mobile token for this. Returns `Promise<void>`. Throws if the offer isn't active /
|
|
554
|
+
pending-confirmation.
|
|
555
|
+
|
|
556
|
+
### decline()
|
|
557
|
+
|
|
558
|
+
Alias for [`cancel()`](#cancel); use it for readability on received offers.
|
|
559
|
+
|
|
560
|
+
### confirm()
|
|
561
|
+
|
|
562
|
+
Accepts the pending mobile confirmation for this offer (after `send()`/`accept()` returned
|
|
563
|
+
`"needs_confirmation"`). Returns `Promise<void>`. Throws a `ConfirmationError` immediately if the
|
|
564
|
+
offer is unsent, if `identitySecret` wasn't supplied to the `SteamMobile` constructor (named-fix
|
|
565
|
+
message: *"identitySecret is required to confirm trade offers — construct SteamMobile with
|
|
566
|
+
{ identitySecret }"*), or if no matching confirmation is found.
|
|
567
|
+
|
|
568
|
+
### counter()
|
|
569
|
+
|
|
570
|
+
Returns a **new** unsent [TradeOffer](#tradeoffer) to the same partner, pre-filled with this offer's
|
|
571
|
+
items and message (deep-copied, so editing the counter won't mutate the original). Edit it and call
|
|
572
|
+
`send()`; sending marks the original as countered. Throws if the original isn't active.
|
|
573
|
+
|
|
574
|
+
### getTradeStatus()
|
|
575
|
+
|
|
576
|
+
Shortcut for [`bot.trade.getTradeStatus({ tradeId: this.tradeID })`](#gettradestatustradeid). Returns
|
|
577
|
+
`Promise<`[`ExchangeDetails`](#exchangedetails)`>`. Throws if the offer has no `tradeID` (i.e. it hasn't
|
|
578
|
+
been accepted).
|
|
579
|
+
|
|
580
|
+
### getPartnerInventory(appid\[, contextid]\[, tradableOnly])
|
|
581
|
+
|
|
582
|
+
- `appid` — App id.
|
|
583
|
+
- `contextid` — Optional context id (default `"2"`).
|
|
584
|
+
- `tradableOnly` — Optional. Tradable items only.
|
|
585
|
+
|
|
586
|
+
Loads the **partner's** inventory, reusing this offer's partner and token. Returns
|
|
587
|
+
`Promise<`[`EconItem`](#econitem)`[]>`.
|
|
588
|
+
|
|
589
|
+
### getUserDetails()
|
|
590
|
+
|
|
591
|
+
Persona, contexts, escrow days, avatars, and partner probation for both sides — McKay-parity. Scrapes
|
|
592
|
+
the trade page once. Picks the URL based on the offer's state: `/tradeoffer/<id>/` for offers that
|
|
593
|
+
have already been sent, otherwise `/tradeoffer/new/?partner=…[&token=…]` for an unsent draft. Returns
|
|
594
|
+
`Promise<`[`UserDetails`](#userdetails)`>`. **Steam applies `max(me.escrowDays, them.escrowDays)`** as
|
|
595
|
+
the actual hold — compute that yourself if you need a single number.
|
|
596
|
+
|
|
597
|
+
Use this when you already have a `TradeOffer` in hand. If you only have a tradeUrl/SteamID, prefer
|
|
598
|
+
the top-level [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) — same scrape, no offer
|
|
599
|
+
construction needed.
|
|
600
|
+
|
|
601
|
+
Throws when called on an offer where Steam doesn't render trade info:
|
|
602
|
+
|
|
603
|
+
- An offer **we sent** (`isOurOffer === true` with `this.id` set) — Steam shows our own offers from a
|
|
604
|
+
different view. Use [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) instead.
|
|
605
|
+
- An offer **received** but no longer `Active` (already accepted, declined, expired, etc.).
|
|
606
|
+
|
|
607
|
+
### containsItem(item)
|
|
608
|
+
|
|
609
|
+
- `item` — `{ appid, contextid, assetid }`.
|
|
610
|
+
|
|
611
|
+
Returns `true` if either side of the offer contains that exact asset.
|
|
612
|
+
|
|
613
|
+
---
|
|
614
|
+
|
|
615
|
+
## bot.community
|
|
616
|
+
|
|
617
|
+
Account- and profile-level helpers backed by `steamcommunity.com` and a couple of Web API calls.
|
|
618
|
+
|
|
619
|
+
### getInventory(appid\[, contextid]\[, options])
|
|
620
|
+
|
|
621
|
+
- `appid` — App id.
|
|
622
|
+
- `contextid` — Optional context id (default `"2"`).
|
|
623
|
+
- `options`
|
|
624
|
+
- `steamId` — Optional. Whose inventory to load (default: yourself).
|
|
625
|
+
- `tradableOnly` — Optional. If `true`, only tradable items are returned (default `false`). For your
|
|
626
|
+
own inventory this is applied server-side via the legacy endpoint's `trading=1` flag; for other
|
|
627
|
+
users it's applied in the parser.
|
|
628
|
+
|
|
629
|
+
Loads your own inventory — or any **public** inventory — paginated automatically. Returns
|
|
630
|
+
`Promise<`[`EconItem`](#econitem)`[]>`. Throws `PrivateInventoryError` on a private inventory (the
|
|
631
|
+
lazy trade-page scrape also classifies trade-ban / target-cannot-trade / item-server-unavailable
|
|
632
|
+
errors into their typed counterparts).
|
|
633
|
+
|
|
634
|
+
Routes by target:
|
|
635
|
+
|
|
636
|
+
- **Self** → the legacy `/profiles/<id>/inventory/json/<appid>/<contextid>` endpoint (cookie path,
|
|
637
|
+
near-nonexistent rate limits, surfaces trade-protected items). The modern
|
|
638
|
+
`IEconService/GetInventoryItemsWithDescriptions` is avoided here — Steam silently returns
|
|
639
|
+
`{response:{}}` on ≥2 calls/sec, making it unfit for repeated use.
|
|
640
|
+
- **Other user** → the public `/inventory/` endpoint (cookie path).
|
|
641
|
+
|
|
642
|
+
For a trade **partner's** inventory, use
|
|
643
|
+
[`bot.trade.getInventory`](#getinventorytarget-appid-contextid-options) instead — `/partnerinventory/`
|
|
644
|
+
is the only path that reveals trade-protected items.
|
|
645
|
+
|
|
646
|
+
### getTradeURL()
|
|
647
|
+
|
|
648
|
+
Returns your current trade URL and token as `Promise<{ url: string; token: string }>`.
|
|
649
|
+
|
|
650
|
+
### changeTradeURL()
|
|
651
|
+
|
|
652
|
+
Regenerates (and thereby **invalidates**) your trade URL/token. Returns the new
|
|
653
|
+
`Promise<{ url: string; token: string }>`.
|
|
654
|
+
|
|
655
|
+
### getProfile(\[steamId])
|
|
656
|
+
|
|
657
|
+
- `steamId` — Optional. Whose profile to read (default: yourself).
|
|
658
|
+
|
|
659
|
+
Reads a profile summary from the community XML in a **single request**. Returns a
|
|
660
|
+
`Promise<`[`SteamProfile`](#steamprofile)`>` with persona, avatar, account-creation date, trade-ban
|
|
661
|
+
state, VAC status, privacy state, and the `isLimited` flag. (Steam level isn't in the XML — use
|
|
662
|
+
`getSteamLevel`.)
|
|
663
|
+
|
|
664
|
+
### getSteamLevel(\[steamId])
|
|
665
|
+
|
|
666
|
+
- `steamId` — Optional (default: yourself).
|
|
667
|
+
|
|
668
|
+
Returns the account's Steam level as a `Promise<number>` via `IPlayerService/GetSteamLevel` (which
|
|
669
|
+
accepts the access token — no API key required).
|
|
670
|
+
|
|
671
|
+
### community.ensureApiKey(\[domain])
|
|
672
|
+
|
|
673
|
+
- `domain` — Optional hostname for the key (default `"assetpay.gg"`).
|
|
674
|
+
|
|
675
|
+
Returns the existing Web API key, or registers a new one (auto-accepting the mobile confirmation if
|
|
676
|
+
`identitySecret` is set), as `Promise<string | null>`. Resolves to `null` when the account is
|
|
677
|
+
ineligible (unverified email, no authenticator, or limited). The registration flow handles Steam's
|
|
678
|
+
Pending → confirm → retry handshake for you.
|
|
679
|
+
|
|
680
|
+
### acknowledgeTradeProtection()
|
|
681
|
+
|
|
682
|
+
Acknowledges the 2025 trade-protection notice. Called automatically by [`send()`](#send); exposed here
|
|
683
|
+
for completeness. Returns `Promise<void>`.
|
|
684
|
+
|
|
685
|
+
---
|
|
686
|
+
|
|
687
|
+
## bot.session
|
|
688
|
+
|
|
689
|
+
The `SessionManager` owns the token lifecycle. You rarely call it directly — the namespaces refresh the
|
|
690
|
+
access token for you — but it's where session control lives.
|
|
691
|
+
|
|
692
|
+
### getAccessToken()
|
|
693
|
+
|
|
694
|
+
Returns a valid access token, minting or renewing it if needed, as `Promise<string>`. Concurrent calls
|
|
695
|
+
share one in-flight mint. Throws `SteamSessionExpiredError` if the session has been revoked or the
|
|
696
|
+
refresh token is dead.
|
|
697
|
+
|
|
698
|
+
### listSessions()
|
|
699
|
+
|
|
700
|
+
Returns the client ids of in-progress auth sessions (login-approval flows) as `Promise<bigint[]>`.
|
|
701
|
+
Note: this is **not** a list of logged-in devices — it reflects pending `IAuthenticationService`
|
|
702
|
+
sessions.
|
|
703
|
+
|
|
704
|
+
### logout(\[action])
|
|
705
|
+
|
|
706
|
+
- `action` — Optional [`EAuthTokenRevokeAction`](#eauthtokenrevokeaction) (default `Logout`).
|
|
707
|
+
|
|
708
|
+
Revokes this refresh token server-side. Afterward the session is dead and any further call throws
|
|
709
|
+
`SteamSessionExpiredError`. Returns `Promise<void>`.
|
|
710
|
+
|
|
711
|
+
### setRefreshToken(refreshToken)
|
|
712
|
+
|
|
713
|
+
- `refreshToken` — A fresh MobileApp refresh token for the **same** account.
|
|
714
|
+
|
|
715
|
+
Swaps in a new refresh token and re-mints in place — the mechanism behind
|
|
716
|
+
[`bot.reauthenticate`](#reauthenticatecredentials). Re-emits [`refreshToken`](#event-refreshtoken).
|
|
717
|
+
Throws if the token is malformed or belongs to a different account. Returns `Promise<void>`.
|
|
718
|
+
|
|
719
|
+
### Properties & events
|
|
720
|
+
|
|
721
|
+
- `refreshToken` / `accessToken` / `steamID` — Same values surfaced on [`SteamMobile`](#properties).
|
|
722
|
+
- Events: `refreshToken`, `sessionExpired`, `debug` — re-emitted on the root client.
|
|
723
|
+
|
|
724
|
+
---
|
|
725
|
+
|
|
726
|
+
## bot.confirmations
|
|
727
|
+
|
|
728
|
+
The `ConfirmationManager` for mobile confirmations. **Prefer [`offer.confirm()`](#confirm)** for trades;
|
|
729
|
+
these cover other cases (market listings, API-key requests, manual flows). All require `identitySecret`.
|
|
730
|
+
|
|
731
|
+
The high-level helpers below derive the TOTP time and HMAC key for you — you normally only need these.
|
|
732
|
+
The lower-level `getConfirmations`/`respondToConfirmation` remain for when you want to compute keys yourself.
|
|
733
|
+
|
|
734
|
+
### getPending()
|
|
735
|
+
|
|
736
|
+
Returns all outstanding confirmations as `Promise<`[`Confirmation`](#confirmation)`[]>`, deriving the
|
|
737
|
+
list HMAC automatically.
|
|
738
|
+
|
|
739
|
+
### acceptConfirmation(id, nonce) · rejectConfirmation(id, nonce)
|
|
740
|
+
|
|
741
|
+
Accept or reject (cancel) a single confirmation by its `id` and `nonce` (the `key` field from
|
|
742
|
+
[`getPending`](#getpending)). Each returns `Promise<void>` and handles the time-offset + per-request
|
|
743
|
+
HMAC timestamp for you.
|
|
744
|
+
|
|
745
|
+
### acceptAll()
|
|
746
|
+
|
|
747
|
+
Accepts every pending confirmation and resolves to the [`Confirmation`](#confirmation)`[]` it acted on.
|
|
748
|
+
Fails fast on the first error.
|
|
749
|
+
|
|
750
|
+
```ts
|
|
751
|
+
for (const c of await bot.confirmations.getPending()) console.log(c.title);
|
|
752
|
+
await bot.confirmations.acceptAll();
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### getConfirmations(time, key)
|
|
756
|
+
|
|
757
|
+
- `time` — A Unix timestamp (use the manager's server-time-corrected clock).
|
|
758
|
+
- `key` — A confirmation key, or `{ tag, key }`.
|
|
759
|
+
|
|
760
|
+
Returns the outstanding confirmations as `Promise<`[`Confirmation`](#confirmation)`[]>`.
|
|
761
|
+
|
|
762
|
+
### respondToConfirmation(confID, confKey, time, key, accept)
|
|
763
|
+
|
|
764
|
+
- `confID` — The confirmation id.
|
|
765
|
+
- `confKey` — The confirmation's nonce.
|
|
766
|
+
- `time` — A Unix timestamp.
|
|
767
|
+
- `key` — A confirmation key, or `{ tag, key }`.
|
|
768
|
+
- `accept` — `true` to allow, `false` to cancel.
|
|
769
|
+
|
|
770
|
+
Accepts or rejects a single confirmation. Returns `Promise<void>`.
|
|
771
|
+
|
|
772
|
+
### acceptConfirmationForObject(objectID)
|
|
773
|
+
|
|
774
|
+
- `objectID` — The id of the object the confirmation is for (e.g. a trade offer id, or an API-key request id).
|
|
775
|
+
|
|
776
|
+
Finds and accepts the confirmation tied to that object, handling the time-offset and per-request HMAC
|
|
777
|
+
timestamps for you. Returns `Promise<void>`. Throws a `ConfirmationError` if no matching confirmation
|
|
778
|
+
exists.
|
|
779
|
+
|
|
780
|
+
---
|
|
781
|
+
|
|
782
|
+
## decodePreviewToken
|
|
783
|
+
|
|
784
|
+
A standalone helper (not on the bot) that decodes a **CS2 masked preview token** — the inspect-data
|
|
785
|
+
blob Steam ships in an item's `asset_properties` under `propertyid: 6` (the "certificate") — into a
|
|
786
|
+
plain JSON object. The same masked format is used by `csgo_econ_action_preview` market inspect tokens.
|
|
787
|
+
Decoding is fully offline (XOR-unmask → protobuf), so no game-coordinator call is needed.
|
|
788
|
+
|
|
789
|
+
```ts
|
|
790
|
+
import { decodePreviewToken } from "@assetpay/steam-mobile";
|
|
791
|
+
|
|
792
|
+
const cert = item.asset_properties.find((p) => p.propertyid === 6)?.string_value;
|
|
793
|
+
const data = decodePreviewToken(cert);
|
|
794
|
+
// {
|
|
795
|
+
// itemid: "51663785755", defindex: 4, paintindex: 230, rarity: 4, quality: 9,
|
|
796
|
+
// paintwear: 0.18685653805732727, paintseed: 34,
|
|
797
|
+
// stickers: [{ slot: 2, stickerId: 6618 }, …],
|
|
798
|
+
// inventory: 3221225475, origin: 8,
|
|
799
|
+
// keychains: [{ slot: 0, stickerId: 54, offsetX: -1.55…, pattern: 34792 }],
|
|
800
|
+
// }
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
- `hex` — The raw certificate hex (or any `[xorKey][protobuf][crc32]` masked blob). Pass it as-is; no
|
|
804
|
+
trimming or tag-stripping needed.
|
|
805
|
+
- Returns the decoded [`CEconItemPreviewDataBlock`](https://github.com/SteamDatabase/Protobufs/blob/master/csgo/cstrike15_gcmessages.proto#L917)
|
|
806
|
+
as a plain object (`Record<string, unknown>`), or `null` for non-hex / undecodable input.
|
|
807
|
+
|
|
808
|
+
Output notes: `itemid` (uint64) is a **string**, fields are **camelCase** (`sticker_id` → `stickerId`,
|
|
809
|
+
`offset_x` → `offsetX`), only fields actually present on the wire are included, and `paintwear` is
|
|
810
|
+
returned as the **float wear** (`0..1`) rather than its raw uint32 bits.
|
|
811
|
+
|
|
812
|
+
> S/A/D inspect links (the ones with a `D…` parameter) are **not** decodable this way — those carry no
|
|
813
|
+
> embedded data and require a live game-coordinator lookup.
|
|
814
|
+
|
|
815
|
+
---
|
|
816
|
+
|
|
817
|
+
## Data types
|
|
818
|
+
|
|
819
|
+
### OfferTarget
|
|
820
|
+
|
|
821
|
+
Identifies a trade partner. It's a discriminated union — supply **one** of the two shapes:
|
|
822
|
+
|
|
823
|
+
```ts
|
|
824
|
+
type OfferTarget =
|
|
825
|
+
| { tradeUrl: string } // a full trade URL (partner + token parsed out)
|
|
826
|
+
| { steamId: string; token?: string }; // a SteamID64, optionally with a trade token
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### TradeItem
|
|
830
|
+
|
|
831
|
+
The minimal shape needed to put an item into an offer:
|
|
832
|
+
|
|
833
|
+
```ts
|
|
834
|
+
interface TradeItem {
|
|
835
|
+
appid: number;
|
|
836
|
+
contextid: string;
|
|
837
|
+
assetid: string;
|
|
838
|
+
amount?: number; // default 1
|
|
839
|
+
}
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
### EconItem
|
|
843
|
+
|
|
844
|
+
The fully-typed item, with Steam's fields preserved 1:1 (snake_case, lossless). Returned by every
|
|
845
|
+
inventory and offer read. Key fields:
|
|
846
|
+
|
|
847
|
+
- `appid` (number), `contextid`, `assetid`, `classid`, `instanceid`, `amount`, `currencyid?`
|
|
848
|
+
- `name`, `market_name`, `market_hash_name`, `type`, `name_color?`, `background_color?`
|
|
849
|
+
- `icon_url`, `icon_url_large?`
|
|
850
|
+
- `tradable`, `marketable`, `commodity` (booleans), `market_tradable_restriction`, `market_marketable_restriction` (numbers)
|
|
851
|
+
- `descriptions`, `owner_descriptions` ([`SteamDescriptionLine`](#data-types)[]), `actions`, `market_actions` (`SteamAction[]`), `fraudwarnings` (string[]), `tags` (`SteamTag[]`)
|
|
852
|
+
- `asset_properties` ([`AssetProperty`](#data-types)[]) — CS2 float/seed/sticker data when present; decode the `propertyid: 6` certificate with [`decodePreviewToken`](#decodepreviewtoken)
|
|
853
|
+
- Any other field Steam returns is preserved (`[key: string]: unknown`).
|
|
854
|
+
|
|
855
|
+
### ExchangeItem
|
|
856
|
+
|
|
857
|
+
An [`EconItem`](#econitem) plus where it landed after the trade settled:
|
|
858
|
+
|
|
859
|
+
```ts
|
|
860
|
+
interface ExchangeItem extends EconItem {
|
|
861
|
+
new_assetid?: string; // Rust populates these; CS2 usually omits them
|
|
862
|
+
new_contextid?: string;
|
|
863
|
+
rollback_new_assetid?: string;
|
|
864
|
+
rollback_new_contextid?: string;
|
|
865
|
+
}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### ExchangeDetails
|
|
869
|
+
|
|
870
|
+
Returned by [`getTradeStatus`](#gettradestatustradeid):
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
interface ExchangeDetails {
|
|
874
|
+
status: ETradeStatus;
|
|
875
|
+
tradeInitTime: Date;
|
|
876
|
+
settlementTime: Date | null; // when items were actually delivered
|
|
877
|
+
receivedItems: ExchangeItem[];
|
|
878
|
+
sentItems: ExchangeItem[];
|
|
879
|
+
usedInventoryFallback: boolean; // always false — surfacing new_assetid is enough; reconciling by inventory diff is the server's job
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
### TradeHistory
|
|
884
|
+
|
|
885
|
+
Returned by [`getTradeHistory`](#gettradehistoryoptions):
|
|
886
|
+
|
|
887
|
+
```ts
|
|
888
|
+
interface TradeHistory {
|
|
889
|
+
trades: TradeHistoryEntry[]; // newest first
|
|
890
|
+
more: boolean; // are there more pages?
|
|
891
|
+
totalTrades: number | undefined; // only when includeTotal was set
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
interface TradeHistoryEntry extends ExchangeDetails {
|
|
895
|
+
tradeId: string;
|
|
896
|
+
partnerSteamId: string | undefined;
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### TradeHistoryOptions
|
|
901
|
+
|
|
902
|
+
See [`getTradeHistory`](#gettradehistoryoptions) for field meanings: `maxTrades?`, `startAfterTime?`,
|
|
903
|
+
`startAfterTradeId?`, `navigatingBack?`, `includeFailed?`, `includeTotal?`.
|
|
904
|
+
|
|
905
|
+
### TradeOffersSummary
|
|
906
|
+
|
|
907
|
+
Returned by [`getTradeOffersSummary`](#gettradeofferssummary) — all `number`:
|
|
908
|
+
|
|
909
|
+
`pending_received_count`, `new_received_count`, `updated_received_count`, `historical_received_count`,
|
|
910
|
+
`pending_sent_count`, `newly_accepted_sent_count`, `updated_sent_count`, `historical_sent_count`,
|
|
911
|
+
`escrow_received_count`, `escrow_sent_count`.
|
|
912
|
+
|
|
913
|
+
### SteamProfile
|
|
914
|
+
|
|
915
|
+
Returned by [`getProfile`](#getprofilesteamid):
|
|
916
|
+
|
|
917
|
+
```ts
|
|
918
|
+
interface SteamProfile {
|
|
919
|
+
steamId: string;
|
|
920
|
+
personaName: string;
|
|
921
|
+
avatar: string;
|
|
922
|
+
accountCreated: Date | null;
|
|
923
|
+
tradeBanState: string; // e.g. "None"
|
|
924
|
+
isLimited: boolean;
|
|
925
|
+
vacBanned: boolean;
|
|
926
|
+
privacyState: string; // e.g. "public"
|
|
927
|
+
}
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
### UserDetails
|
|
931
|
+
|
|
932
|
+
Returned by [`bot.trade.getUserDetails(target)`](#getuserdetailstarget) and
|
|
933
|
+
[`offer.getUserDetails()`](#getuserdetails) — mirrors steamcommunity's `TradeOffer#getUserDetails`:
|
|
934
|
+
|
|
935
|
+
```ts
|
|
936
|
+
interface UserDetails {
|
|
937
|
+
me: UserSideDetails;
|
|
938
|
+
them: UserPartnerDetails;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
interface UserSideDetails {
|
|
942
|
+
personaName: string;
|
|
943
|
+
contexts: Record<string, unknown> | null;
|
|
944
|
+
escrowDays: number; // your side; Steam holds for max(me, them) days
|
|
945
|
+
avatarIcon: string | undefined; // .jpg
|
|
946
|
+
avatarMedium: string | undefined; // _medium.jpg
|
|
947
|
+
avatarFull: string | undefined; // _full.jpg
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
interface UserPartnerDetails extends UserSideDetails {
|
|
951
|
+
probation: boolean; // partner is on Steam-trade probation (see Steam's warning banner)
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### EscrowHold
|
|
956
|
+
|
|
957
|
+
Returned by [`bot.trade.getEscrow`](#getescrowtarget) — escrow-only, in **seconds**. Top-level keys
|
|
958
|
+
renamed from Steam's `my_escrow`/`their_escrow`/`both_escrow`; inner shape preserved 1:1:
|
|
959
|
+
|
|
960
|
+
```ts
|
|
961
|
+
interface EscrowHold {
|
|
962
|
+
me: EscrowSide;
|
|
963
|
+
them: EscrowSide;
|
|
964
|
+
both: EscrowSide;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
interface EscrowSide {
|
|
968
|
+
escrow_end_duration_seconds: number;
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### PollData
|
|
973
|
+
|
|
974
|
+
The poll snapshot — persist it via the [`pollData` event](#event-polldata) and pass it back to
|
|
975
|
+
[`startPolling`](#startpollingoptions):
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
interface PollData {
|
|
979
|
+
offersSince: number; // Unix seconds of the newest processed update
|
|
980
|
+
sent: Record<string, ETradeOfferState>; // offer id -> last seen state
|
|
981
|
+
received: Record<string, ETradeOfferState>;
|
|
982
|
+
timestamps: Record<string, number>;
|
|
983
|
+
lastFullUpdate?: number; // ms epoch of the last full sweep; persisted so the cadence survives stateless workers
|
|
984
|
+
}
|
|
985
|
+
```
|
|
986
|
+
|
|
987
|
+
### PollOptions
|
|
988
|
+
|
|
989
|
+
See [`startPolling`](#startpollingoptions): `pollInterval?` (default 10000 ms), `pollFullUpdateInterval?`
|
|
990
|
+
(default 300000 ms), `pollData?`, `store?` (a [`PollDataStore`](#polldatastore)), `maxAgeMs?` (default
|
|
991
|
+
30d), `cancelTime?` (off by default).
|
|
992
|
+
|
|
993
|
+
### PollDataStore
|
|
994
|
+
|
|
995
|
+
Pluggable persistence for the poll snapshot — implement it to keep `pollData` in Redis (or anywhere) so
|
|
996
|
+
any worker can resume a bot's poll. You own one-tick-at-a-time mutual exclusion; the library owns the
|
|
997
|
+
diff. Use it with [`startPolling`](#startpollingoptions) (timer) or [`pollOnce`](#pollonceoptions) (cron).
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
interface PollDataStore {
|
|
1001
|
+
load(): Promise<PollData | undefined>;
|
|
1002
|
+
save(pollData: PollData): Promise<void>;
|
|
1003
|
+
}
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
### PollChange
|
|
1007
|
+
|
|
1008
|
+
One offer transition returned by [`pollOnce`](#pollonceoptions) (the same set is also emitted as
|
|
1009
|
+
[trade events](#trade-events)):
|
|
1010
|
+
|
|
1011
|
+
```ts
|
|
1012
|
+
type PollChange =
|
|
1013
|
+
| { type: "newOffer"; offer: TradeOffer }
|
|
1014
|
+
| { type: "sentOfferChanged"; offer: TradeOffer; oldState: ETradeOfferState }
|
|
1015
|
+
| { type: "receivedOfferChanged"; offer: TradeOffer; oldState: ETradeOfferState }
|
|
1016
|
+
| { type: "unknownOfferSent"; offer: TradeOffer };
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
### TradeOfferUpdate
|
|
1020
|
+
|
|
1021
|
+
The payload of the [`offerUpdate`](#event-offerupdate) event — a unified view of any single offer
|
|
1022
|
+
change. `previousState` is `undefined` the first time an offer is seen; direction is `offer.isOurOffer`:
|
|
1023
|
+
|
|
1024
|
+
```ts
|
|
1025
|
+
interface TradeOfferUpdate {
|
|
1026
|
+
offer: TradeOffer;
|
|
1027
|
+
previousState?: ETradeOfferState;
|
|
1028
|
+
}
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### Confirmation
|
|
1032
|
+
|
|
1033
|
+
Returned by [`getConfirmations`](#getconfirmationstime-key): `id`, `type`, `creator`, `key` (nonce),
|
|
1034
|
+
`title`, `receiving`, `sending`, `time` (ISO string), `timestamp` (`Date`), `icon`.
|
|
1035
|
+
|
|
1036
|
+
---
|
|
1037
|
+
|
|
1038
|
+
## Enums
|
|
1039
|
+
|
|
1040
|
+
All are exported and have the standard Steam numeric values.
|
|
1041
|
+
|
|
1042
|
+
### ETradeOfferState
|
|
1043
|
+
|
|
1044
|
+
`Invalid (1)`, `Active (2)`, `Accepted (3)`, `Countered (4)`, `Expired (5)`, `Canceled (6)`,
|
|
1045
|
+
`Declined (7)`, `InvalidItems (8)`, `CreatedNeedsConfirmation (9)`, `CanceledBySecondFactor (10)`,
|
|
1046
|
+
`InEscrow (11)`, `Reverted (12)`.
|
|
1047
|
+
|
|
1048
|
+
### ETradeStatus
|
|
1049
|
+
|
|
1050
|
+
`Init (0)`, `PreCommitted (1)`, `Committed (2)`, `Complete (3)`, `Failed (4)`,
|
|
1051
|
+
`PartialSupportRollback (5)`, `FullSupportRollback (6)`, `SupportRollback_Selective (7)`,
|
|
1052
|
+
`RollbackFailed (8)`, `RollbackAbandoned (9)`, `InEscrow (10)`, `EscrowRollback (11)`, `Reverted (12)`.
|
|
1053
|
+
|
|
1054
|
+
### EConfirmationMethod
|
|
1055
|
+
|
|
1056
|
+
`None (0)`, `Email (1)`, `MobileApp (2)`.
|
|
1057
|
+
|
|
1058
|
+
### EConfirmationType
|
|
1059
|
+
|
|
1060
|
+
`Invalid (0)`, `Generic (1)`, `Trade (2)`, `MarketListing (3)`, `FeatureOptOut (4)`,
|
|
1061
|
+
`PhoneNumberChange (5)`, `AccountRecovery (6)`.
|
|
1062
|
+
|
|
1063
|
+
### EOfferFilter
|
|
1064
|
+
|
|
1065
|
+
`ActiveOnly (1)`, `HistoricalOnly (2)`, `All (3)`.
|
|
1066
|
+
|
|
1067
|
+
### EAuthSessionGuardType
|
|
1068
|
+
|
|
1069
|
+
`Unknown (0)`, `None (1)`, `EmailCode (2)`, `DeviceCode (3)`, `DeviceConfirmation (4)`,
|
|
1070
|
+
`EmailConfirmation (5)`, `MachineToken (6)`, `LegacyMachineAuth (7)`.
|
|
1071
|
+
|
|
1072
|
+
### EAuthTokenRevokeAction
|
|
1073
|
+
|
|
1074
|
+
`Logout (0)`, `Permanent (1)`, `Replaced (2)`, `Support (3)`, `Consume (4)`, `NonRememberedLogout (5)`,
|
|
1075
|
+
`NonRememberedPermanent (6)`, `Automatic (7)`. Used by [`bot.session.logout`](#logoutaction).
|
|
1076
|
+
|
|
1077
|
+
### Others
|
|
1078
|
+
|
|
1079
|
+
`EAuthTokenPlatformType`, `ESessionPersistence`, `ETokenRenewalType`, and the full `EResult` table are
|
|
1080
|
+
also exported.
|
|
1081
|
+
|
|
1082
|
+
---
|
|
1083
|
+
|
|
1084
|
+
## Errors
|
|
1085
|
+
|
|
1086
|
+
Every error extends **`SteamError`**, which carries an optional `eresult` (a Steam `EResult`) and `body`
|
|
1087
|
+
(the raw response). The library **classifies** errors but does not implement a cooldown policy — retry
|
|
1088
|
+
logic stays in your code.
|
|
1089
|
+
|
|
1090
|
+
Steam-supplied messages (the `strError` field on send/accept/cancel responses **and** the
|
|
1091
|
+
`<div id="error_msg">` text scraped from the trade page when an inventory load fails) are run through
|
|
1092
|
+
one shared classifier so the same `instanceof` check works everywhere.
|
|
1093
|
+
|
|
1094
|
+
| Class | Extra fields | Meaning |
|
|
1095
|
+
| ----- | ------------ | ------- |
|
|
1096
|
+
| `SteamError` | `eresult?`, `body?` | Base class for everything. |
|
|
1097
|
+
| `HttpStatusError` | `statusCode` | A non-2xx HTTP response. |
|
|
1098
|
+
| `SteamSessionExpiredError` | — | The session/token is no longer valid; re-authenticate. |
|
|
1099
|
+
| `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). |
|
|
1100
|
+
| `ProxyError` | `cause?` | A request through a configured proxy failed at the transport layer (unreachable / refused / timeout / auth). Only thrown when a `proxy` is set. |
|
|
1101
|
+
| `EscrowError` | `escrowDays` | The trade would be (or is) held in escrow. |
|
|
1102
|
+
| `TradeBanError` | — | The account is trade-banned. |
|
|
1103
|
+
| `OfferLimitError` | `eresult = 25` | Sent too many offers. |
|
|
1104
|
+
| `TargetCannotTradeError` | `eresult?` | Partner is not available to trade (limited, escrow-only, etc.). |
|
|
1105
|
+
| `NewDeviceError` | `eresult?` | Steam blocks the action because we logged in from a new device recently. |
|
|
1106
|
+
| `ItemServerUnavailableError` | `eresult = 102` | The game's item server (its Game Coordinator) is unreachable — transient, per-app, retry shortly. |
|
|
1107
|
+
| `PrivateInventoryError` | — | Partner's inventory is private (or friends-only and we're not friends). A trade URL/token does **not** bypass inventory privacy. |
|
|
1108
|
+
| `ConfirmationError` | — | A mobile-confirmation step failed (incl. missing `identitySecret` when calling [`offer.confirm()`](#confirm)). |
|
|
1109
|
+
| `FamilyViewError` | — | Family View is restricting the account. |
|
|
1110
|
+
| `LoginError` | `extendedErrorMessage?`, `isTransient` | Credential-login failure; `isTransient` is `true` for a retryable blip (timeout / service unavailable) rather than bad credentials. |
|
|
1111
|
+
| `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. |
|
|
1112
|
+
|
|
1113
|
+
`unlockAt` / `retryAfterMs` on `RateLimitError` are never `undefined`, so a cooldown is just
|
|
1114
|
+
`cooldownUntil = err.unlockAt`:
|
|
1115
|
+
|
|
1116
|
+
```ts
|
|
1117
|
+
import { RateLimitError } from "@assetpay/steam-mobile";
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
await bot.trade.getTradeOffers();
|
|
1121
|
+
} catch (err) {
|
|
1122
|
+
if (err instanceof RateLimitError) {
|
|
1123
|
+
await sleepUntil(err.unlockAt);
|
|
1124
|
+
} else throw err;
|
|
1125
|
+
}
|
|
1126
|
+
```
|
|
1127
|
+
|
|
1128
|
+
---
|
|
1129
|
+
|
|
1130
|
+
## Mobile-app impersonation
|
|
1131
|
+
|
|
1132
|
+
The client presents as the Steam mobile app on the wire: per-host user agents, the `mobileClient`
|
|
1133
|
+
cookie, an `origin=SteamMobile` header on writes, and `mobileconf` confirmations. Pick a preset with
|
|
1134
|
+
`mobileProfile: "ios"` (default) or `"android"`, or override individual fields with an object.
|
|
1135
|
+
|
|
1136
|
+
A **`MobileProfile`** has: `mobileClient` (`"ios"` | `"android"`), `mobileClientVersion`,
|
|
1137
|
+
`apiUserAgent`, `webUserAgent`, `deviceFriendlyName` (iOS sends the model id, e.g. `"iPhone18,3"`),
|
|
1138
|
+
`osType` (signed EOSType; iOS `-600`, Android `-500`), `gamingDeviceType`, and `appType?`. The
|
|
1139
|
+
`IOS_PROFILE` and `ANDROID_PROFILE` constants (captured from real apps) and `resolveMobileProfile` are
|
|
1140
|
+
exported if you want to build on them.
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
const bot = new SteamMobile({
|
|
1144
|
+
refreshToken,
|
|
1145
|
+
mobileProfile: { mobileClient: "ios", deviceFriendlyName: "iPhone17,2" },
|
|
1146
|
+
});
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
---
|
|
1150
|
+
|
|
1151
|
+
## Notes & limitations
|
|
1152
|
+
|
|
1153
|
+
- **MobileApp-only.** No real-time CM push — offer detection is polling. No Game Coordinator (item data
|
|
1154
|
+
comes from the web `asset_properties`). No chat/presence.
|
|
1155
|
+
- **Limited accounts** (haven't spent the $5 that lifts Steam's anti-spam limit) **can still trade** —
|
|
1156
|
+
subject to the usual Steam Guard / escrow holds — but can't use the Community Market or generate a
|
|
1157
|
+
Web API key. This library doesn't use a Web API key (it authenticates with the mobile access token),
|
|
1158
|
+
so limited accounts can send and receive trades through it. `getProfile().isLimited` surfaces the
|
|
1159
|
+
state; `ensureApiKey()` returns `null`.
|
|
1160
|
+
- **English only** (`l=english`) by design — no localization dependency.
|
|
1161
|
+
- **Settlement is read-only.** `getTradeStatus` surfaces Steam's `new_assetid`/`new_contextid`;
|
|
1162
|
+
reconciling items by inventory diff (the race-prone part) is left to your server, which has the
|
|
1163
|
+
concurrency context. `usedInventoryFallback` is therefore always `false`.
|
|
1164
|
+
- **No auto-retry.** Rate limits are thrown as `RateLimitError` — always with a concrete `unlockAt`, so
|
|
1165
|
+
the caller's cooldown needs no guesswork (the poll loop is the one exception: it backs off and resumes).
|
|
1166
|
+
|
|
1167
|
+
---
|
|
1168
|
+
|
|
1169
|
+
## Development
|
|
1170
|
+
|
|
1171
|
+
```bash
|
|
1172
|
+
pnpm install
|
|
1173
|
+
pnpm typecheck # tsc --noEmit (strict NodeNext)
|
|
1174
|
+
pnpm lint # biome
|
|
1175
|
+
pnpm test # vitest
|
|
1176
|
+
pnpm build # tsdown → dist (ESM + d.ts)
|
|
1177
|
+
pnpm proto # regenerate src/protobufs from protobufs/*.proto (buf + protoc-gen-es)
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
Live debug scripts live in `debug/` and read a `.env` for credentials:
|
|
1181
|
+
|
|
1182
|
+
```bash
|
|
1183
|
+
pnpm bootstrap # credential login → save ./bot.refreshtoken (run once)
|
|
1184
|
+
pnpm smoke # read-only health check across the whole API
|
|
1185
|
+
pnpm watch # live trade-event watcher — send the bot a trade and watch it fire
|
|
1186
|
+
pnpm trade # send → confirm → cancel lifecycle (gated: SEND=1 PARTNER_TRADE_URL=…; needs a non-limited bot)
|
|
1187
|
+
pnpm partner-inventory # load a partner's inventory via /partnerinventory/ (PARTNER_TRADE_URL=…; surfaces PrivateInventoryError etc.)
|
|
1188
|
+
```
|