はじめに
TBH(タスクバーヒーロー)やCS2のアイテムトレードが盛んになる中、確定申告のためにSteamの取引履歴をまとめたいというニーズが増えています。SteamにはCSVエクスポート機能がないため、ブックマークレットを使って非公式APIから直接データを取得するツールを作りました。
この記事では実装を通じて分かった SteamマーケットAPIの構造 と ブックマークレットの制約・実装パターン をまとめます。
完成物
steamcommunity.com/market/ で実行すると、指定年の取引履歴を全件取得してCSVをダウンロードします。
出力列:日時 / 種別(購入・売却)/ ゲーム名 / アイテム名 / 数量 / 金額 / 通貨
SteamマーケットAPIの構造
エンドポイント
GET /market/myhistory/render/?start=0&count=500&norender=1
ログイン済みセッションのCookieをそのまま使う。norender=1 を付けるとHTMLではなくJSONが返る。count の最大値は500。
レスポンス構造
{
"success": true,
"pagesize": 100,
"total_count": 2600,
"start": 0,
"assets": { ... },
"listings": { ... },
"purchases": { ... },
"events": [ ... ]
}
4つのオブジェクトを組み合わせて1件の取引を再構成する設計になっている。
events
取引イベントの一覧。1件あたりの構造:
{
"listingid": "553525557067507977",
"purchaseid": "553525557067507978",
"event_type": 4,
"time_event": 1780982631,
"time_event_fraction": 350000000,
"steamid_actor": "76561198311770783",
"date_event": "6月9日"
}
注意点:
-
event_type: 3= 売却、event_type: 4= 購入(直感と逆) -
time_eventはUNIXタイムスタンプ(秒) -
date_eventはロケール依存の文字列なので日付計算には使えない - アイテム情報・金額はここに入っていない。
listingidとpurchaseidで他のオブジェクトと紐付ける
listings
出品情報。キーは listingid。
{
"553525557067507977": {
"listingid": "553525557067507977",
"price": 0,
"fee": 0,
"publisher_fee_app": 3678970,
"publisher_fee_percent": "0.10",
"currencyid": 2008,
"asset": {
"currency": 0,
"appid": 3678970,
"contextid": "2",
"id": "497230749269761484",
"amount": "0"
},
"original_price": 400
}
}
注意点:
-
original_priceは売り手の受取額(手数料引き後) -
priceとfeeはこのエンドポイントでは常に0 -
currencyidは出品者の通貨(後述)
purchases
購入取引の詳細。キーが listingid_purchaseid という複合キーになっている点に注意。
{
"553525557067507977_553525557067507978": {
"listingid": "553525557067507977",
"purchaseid": "553525557067507978",
"time_sold": 1780982631,
"steamid_purchaser": "76561198311770783",
"asset": {
"appid": 3678970,
"contextid": "2",
"id": "497230749270005493",
"classid": "8507091145",
"instanceid": "0",
"amount": "1",
"new_id": "506237948545372872"
},
"paid_amount": 400,
"paid_fee": 400,
"currencyid": "2008",
"steam_fee": 200,
"publisher_fee": 200,
"received_amount": 400,
"received_currencyid": "2008"
}
}
購入者の実際の支払額 = paid_amount + paid_fee
paid_amount だけでは手数料が含まれない。Steam手数料5%+パブリッシャー手数料(通常10%)が paid_fee に含まれる。
数量は asset.amount に入っている。「3 Iron Ingot」のようなセット出品の場合ここが "3" になる。
assets
アイテムのメタ情報。キー構造が assets[appid][contextid][assetid] という3階層。
{
"3678970": {
"2": {
"497230749269761484": {
"appid": 3678970,
"contextid": "2",
"id": "497230749269761484",
"classid": "8507091145",
"name": "Sapphire",
"market_name": "Sapphire",
"market_hash_name": "Sapphire",
"type": "Decoration Material",
"app_icon": "https://cdn.fastly.steamstatic.com/...",
...
}
}
}
}
listings側の asset.id と purchases側の asset.id(または new_id)でlookupする。ゲーム名は app_icon のURLや appid から別途 store.steampowered.com/api/appdetails で取得する必要がある。
通貨コードの仕組み
currencyid は 2000 + ECurrencyCode の形式。
| currencyid | 通貨 |
|---|---|
| 2001 | USD |
| 2008 | JPY |
| 2016 | KRW |
| 2003 | EUR |
重要:currencyid は出品者の通貨。日本円アカウントでKRW出品のアイテムを買っても、listings.currencyid はKRWで返ってくる。購入者が実際に支払った通貨は purchases.currencyid で確認できる(こちらは購入者の通貨)。
金額の単位は各通貨とも ÷100(KWDのみ ÷1000)。JPYも ÷100 なので 400 = ¥4。
ゲーム名の取得
assets にゲーム名フィールドはない。appid から以下のAPIで取得する:
GET https://store.steampowered.com/api/appdetails?appids=3678970&filters=basic
steamcommunity.com から store.steampowered.com へのリクエストはCORSでブロックされるが、逆(steamcommunity.com → store.steampowered.com)は同一オリジン扱いで通る。
ブックマークレット実装のポイント
CORSとCSPの壁
steamcommunity.com/market/ 上で動かす前提にすることでCORS問題を回避できる。fetchのURLを相対パス(/market/myhistory/render/...)にすれば同一オリジンになる。
CSPの制約から外部スクリプトの動的読み込み(SheetJSなど)はブロックされる。外部ライブラリは一切使えない前提で実装する必要がある。XLSXの代わりにCSVで出力するのが現実的。
prompt() が動かない
モダンブラウザでは javascript: URLからの prompt() をブロックするケースがある。年指定などのパラメータはコード内に直書きするか、URLパラメータ方式にするのが安全。
ページング
function fetchPage(start) {
fetch(`/market/myhistory/render/?start=${start}&count=500&norender=1`, {
credentials: 'include'
})
.then(r => r.json())
.then(d => {
// 処理...
if (!stopped && fetched < d.total_count) {
setTimeout(() => fetchPage(fetched), 1100); // レート制限対策
} else {
finish();
}
});
}
total_count を超えるまで再帰的に叩く。リクエスト間隔は1秒以上空けないとレート制限(429)を受ける。
年度フィルタ
time_event がUNIXタイムスタンプなので、年の開始・終了をタイムスタンプに変換して比較する。古い順ではなく新しい順で返ってくるため、yStart を下回ったらそこで打ち切れる。
const yStart = new Date(year, 0, 1).getTime() / 1000;
const yEnd = new Date(year, 11, 31, 23, 59, 59).getTime() / 1000;
for (const ev of events) {
const ts = ev.time_event;
if (ts > 0 && ts < yStart) { stopped = true; break; }
if (ts > yEnd) continue;
// 処理
}
ハマりポイントまとめ
| 問題 | 原因 | 対処 |
|---|---|---|
| assetsからアイテムが取れない | キー構造が [appid][contextid][assetid] の3階層 |
全階層を総当たりでフォールバック検索 |
| purchasesが空に見える | キーが listingid ではなく listingid_purchaseid
|
ev.listingid + '_' + ev.purchaseid で引く |
| 購入金額がずれる |
paid_amount だけでは手数料抜き |
paid_amount + paid_fee を使う |
| 通貨が出品者のものになる | APIの仕様。購入者通貨は purchases.currencyid
|
通貨列を別途出力して後で換算 |
| ゲーム名が取れない | assetsにゲーム名フィールドがない |
store.steampowered.com/api/appdetails で取得 |
| 売却/購入が逆 |
event_type: 3 = 売却、4 = 購入 |
直感と逆なので注意 |
おわりに
SteamマーケットAPIは公式ドキュメントが存在しないため、実際にレスポンスを観察しながらリバースエンジニアリングする形になります。特に purchases のキー形式や assets の3階層構造は初見では気づきにくい部分です。
同じような実装をする方の参考になれば幸いです。Steam側の仕様変更で動かなくなる可能性もあるため、その際はコメントで教えてください。