Skip to content

Instantly share code, notes, and snippets.

@joekiller
Last active October 17, 2023 12:25
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joekiller/9485eaecf8d984b8e366acc3d8af6765 to your computer and use it in GitHub Desktop.
Save joekiller/9485eaecf8d984b8e366acc3d8af6765 to your computer and use it in GitHub Desktop.
TF2 Trade History

This is a program to scan though your steam trade history for TF2 Trades. you have to update in scanHistory.json on line 6 the target to match your steam id. so for me it's 76561197966563795 but you need to replace it with your steamID64, look it on on steamrep if you are unsure. and then you need to make a file in the same directory called .env.json that contains an object with your Steam API Key: https://steamcommunity.com/dev/apikey

Also this requires nodejs 14+. I run the program by running the command: node scanHistory.js.

You can use VSCode or other IDEs like Webstorm that can interpret JSON files to make this easy. It's kinda a cludge but it works. Basically it makes two files for trade history. the files are a date.json and date-details.json like 1321088897.json and 1323108997-details.json image

The way this works is it runs through all your trade history and then looks up all the details of each asset. So given asset ids there are item descriptions that are registered through the combination of classid and instanceid of the item. Look for the assetid in the history and then search for classid_instanceid for the item. That should find the file that has the trade history. I didn't make this fancy so given assetsgiven, you need to look at the length of the array to see how many keys or whatever you traded. You can also use to the time_init to jump back to the trade history on steam to look stuff up if you have history bastard installed, etc.

To match trades to descriptions pull the assetid from the backpack.tf trade history (yeah I know busted but still kinda works)

So given the following: image

I would search for 11122658166 in the files and find the assetid match: image

then given the assetid I can look up the classid and instanceid that corresponds to the description which in this case is 11040732 and 92749198: image

now because sometimes I move stuff between inventories, this might not be a problem for you, I need to find the oldest instance of the combination of classid_instanceid, ie 11040732_92749198: image

So in the above I found the details in 1622031891-details.json so the trade will be in 1622031891.json I can just search for the instanceid or classid and find the trade: image

Then typically what I do is roll up the assets_given and put my cursor at the end of the ] and then unroll it so that I'm at the end of the vector (the vector is the stuff in between the [...] or sometimes call the array). image ^ cursor is next to the black dot, then click the plus on the left to expand the selection back out then press the left arrow on your keyboard to snap to where the cursor is such that it indicates that in this case I'm in the "assets_given" vector if the 27th trade. If I move the cursor so it's next to the last } I can see how many keys approximately I traded for it.

In this case for the trade I traded 44 items (it counts starting with zero) for the specs: image

I can also fold up the whole trade to get to the time_init which then I paste into my trade history to see what steam gives me: image

So the time_init is 1621687452. and I paste that into my trade history like: https://steamcommunity.com/id/joekiller/tradehistory/?after_time=1621687452 and I get to go straight to the trade: image

and I can see I paid 22 keys and 22 metal for all that stuff. 44 items in total, just like the file said it would be.

maybe one day I'll make this better but for now it's what it is.

{
"key": "ABC123"
}
const https = require('https');
const fs = require('fs');
const ids = {
target: '76561197966563795'
};
class AssetCache {
cache = new Map()
cacheName = 'assetCache.json';
async init() {
try {
const file = await readFile(this.cacheName);
// const output = JSON.parse(file);
const parsed = new Map(Object.entries(file));
this.cache = parsed;
} catch {}
}
async save() {
await saveFile(JSON.stringify(Object.fromEntries(this.cache.entries())), this.cacheName);
}
get(key) {
return this.cache.get(key);
}
set(key, value) {
this.cache.set(key, value);
}
}
async function getUrl(url) {
return new Promise((resolve, reject) => {
const callUrl = async (url) => {
https.get(url, (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
let data = ""
res.on('data', (d) => {
data += d;
});
res.on('end', () => {
if (200 !== res.statusCode) {
console.log(data);
setTimeout(() => callUrl(url), 50);
} else {
resolve(data);
}
})
}).on('error', (e) => {
if(e.code !== undefined && e.code === "ECONNRESET") {
setTimeout(() => callUrl(url), 50);
} else {
reject(e);
}
});
}
callUrl(url).catch((e) => reject(e));
});
}
let key = null;
async function apiKey() {
if (!key) key = JSON.parse((await fs.promises.readFile('.env.json')).toString());
return key;
}
async function getAssetClassInfo(queries, classCount) {
let url = `https://api.steampowered.com/ISteamEconomy/GetAssetClassInfo/v0001/?key=${key.key}&format=json&language=en&appid=440`;
url += `&class_count=${classCount}${queries}`;
const result = JSON.parse(await getUrl(url));
if (result.result === undefined || result.result.success === false) {
await new Promise(r => setTimeout(r, 20));
return await getAssetClassInfo(queries, classCount);
}
return result;
}
async function getAndCacheAssetClassInfo(queries, classCount, assetCache) {
const classesInfo = await getAssetClassInfo(queries, classCount);
for (const k in classesInfo.result) {
if('success' !== k) {
let key;
if(classesInfo.result[k].instanceid) {
key = `${k}_${classesInfo.result[k].instanceid}`
} else {
key = `${k}_0`
}
assetCache.set(key, classesInfo.result[k]);
}
}
return classesInfo.result;
}
async function getAssetClassInfoForTrades({trades, assetCache}) {
let results = [];
for await (const trade of trades) {
if(trade.assets_received !== undefined) {
let classCount = 0;
let pending = new Set();
let queries = '';
for await (const asset of trade.assets_received) {
const key = `${asset.classid}_${asset.instanceid}`;
const classInfo = assetCache.get(key);
if(classInfo !== undefined && !pending.has(key)) {
if(asset.appid === 440) {
pending.add(key);
queries += `&classid${classCount}=${asset.classid}&instanceid${classCount}=${asset.instanceid}`
classCount += 1;
if(classCount === 30) {
results = results.concat(await getAndCacheAssetClassInfo(queries, classCount, assetCache));
classCount = 0;
queries = '';
pending = new Set();
}
}
}
}
if(classCount > 0) {
results = results.concat(await getAndCacheAssetClassInfo(queries, classCount, assetCache));
}
}
if(trade.assets_given !== undefined) {
let classCount = 0;
let pending = new Set();
let queries = '';
for await (const asset of trade.assets_given) {
const key = `${asset.classid}_${asset.instanceid}`;
const classInfo = assetCache.get(key);
if(classInfo !== undefined && !pending.has(key)) {
if(asset.appid === 440) {
pending.add(key);
queries += `&classid${classCount}=${asset.classid}&instanceid${classCount}=${asset.instanceid}`
classCount += 1;
if(classCount === 30) {
results = results.concat(await getAndCacheAssetClassInfo(queries, classCount, assetCache));
classCount = 0;
queries = '';
pending = new Set();
}
}
}
}
if(classCount > 0) {
results = results.concat(await getAndCacheAssetClassInfo(queries, classCount, assetCache));
}
}
}
return results;
}
async function getTF2TradeHistory(options) {
if(undefined === options) options = {};
if(options.maxTrades === undefined || (1 > options.maxTrades && 30 < options.maxTrades)) options.maxTrades = 30;
let url = `https://api.steampowered.com/IEconService/GetTradeHistory/v1/?key=${key.key}&max_trades=${options.maxTrades}`;
if(options.startAfterTime !== undefined) url = `${url}&start_after_time=${options.startAfterTime}`;
const result = JSON.parse(await getUrl(url));
if (result.response === undefined || result.response.more === undefined) {
console.log(`failed to get trade history ${url}. sleeping`);
await new Promise(r => setTimeout(r, 20));
return getTF2TradeHistory(options);
}
return result;
}
async function saveFile(data, f) {
return new Promise((r, e) => {
fs.writeFile(f, data, (err) => {
if (err) e(err);
console.log('The file has been saved!');
r(data);
})
});
}
async function readDir(d) {
return new Promise((r, e) => {
fs.readdir(d, (err, files) => {
if (err) e(err);
r(files);
});
})
}
function tradeHistoryDir(steamID) {
return `tradehistory/${steamID}`;
}
async function writeHistory({history, targetDir, startAfterTime}) {
await fs.promises.mkdir(targetDir, { recursive: true })
await saveFile(JSON.stringify(history, null, 2), `${targetDir}/${startAfterTime}.json`, { encoding: 'utf-8' });
}
async function writeHistoryDetails({details, targetDir, startAfterTime}) {
await fs.promises.mkdir(targetDir, { recursive: true })
await saveFile(JSON.stringify(details, null, 2), `${targetDir}/${startAfterTime}-details.json`, { encoding: 'utf-8' });
}
async function readFile(filePath) {
return JSON.parse((await fs.promises.readFile(filePath, { encoding: 'utf-8' })));
}
async function dumpHistoryUntil({targetDir, startAfterTime, maxTimeInit, assetCache}) {
let result = await getTF2TradeHistory({ startAfterTime });
let trades = result.response.trades;
const firstResultTimeInit = trades[0].time_init;
const lastResultTimeInit = trades[trades.length - 1].time_init;
if(maxTimeInit) {
if(maxTimeInit <= firstResultTimeInit) return;
if(maxTimeInit <= lastResultTimeInit) {
trades = trades.slice(0, trades.findIndex((e) => e.time_init > maxTimeInit ) - 1);
}
}
if(trades.length > 0) {
let details = await getAssetClassInfoForTrades({trades, assetCache});
await writeHistory({history: trades, targetDir: targetDir, startAfterTime: firstResultTimeInit});
await writeHistoryDetails({details: details, targetDir: targetDir, startAfterTime: firstResultTimeInit});
if (result.response.more) {
return dumpHistoryUntil({targetDir: targetDir, startAfterTime: lastResultTimeInit, maxTimeInit: maxTimeInit, assetCache});
}
}
}
async function dumpAllHistory({targetDir, startAfterTime, assetCache}) {
let latestTimeInit, oldestTimeInit;
try {
const histories = (await readDir(targetDir)).map((fn) => Number(fn.substring(0, fn.length - 5)));
latestTimeInit = histories[histories.length - 1];
const oldestHistoryResults = await readFile(`${targetDir}/${histories[0]}.json`);
oldestTimeInit = oldestHistoryResults[oldestHistoryResults.length - 1].time_init;
} catch {}
// get everything new
await dumpHistoryUntil({targetDir: targetDir, startAfterTime: startAfterTime, maxTimeInit: latestTimeInit, assetCache});
// get everything old.. just in case
await dumpHistoryUntil({targetDir: targetDir, startAfterTime: oldestTimeInit, assetCache});
}
async function run() {
const assetCache = new AssetCache();
await assetCache.init();
await apiKey();
const result = await dumpAllHistory({targetDir: tradeHistoryDir(ids.target), assetCache});
await assetCache.save();
return result;
}
run().then((result) => console.log(result)).catch((e) => console.log(e));
{
"key": "ABC123"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment