0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SteamマーケットAPIをブックマークレットで叩いて取引履歴をCSV出力する

0
Posted at

はじめに

TBH(タスクバーヒーロー)やCS2のアイテムトレードが盛んになる中、確定申告のためにSteamの取引履歴をまとめたいというニーズが増えています。SteamにはCSVエクスポート機能がないため、ブックマークレットを使って非公式APIから直接データを取得するツールを作りました。

この記事では実装を通じて分かった SteamマーケットAPIの構造ブックマークレットの制約・実装パターン をまとめます。


完成物

👉 GitHub Gist(コード本体)

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 はロケール依存の文字列なので日付計算には使えない
  • アイテム情報・金額はここに入っていない。listingidpurchaseid で他のオブジェクトと紐付ける

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 は売り手の受取額(手数料引き後)
  • pricefee はこのエンドポイントでは常に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 で取得する必要がある。


通貨コードの仕組み

currencyid2000 + 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.comstore.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側の仕様変更で動かなくなる可能性もあるため、その際はコメントで教えてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?