Getting Started With OPSkins’s ExpressTrade API By Writing a Trade Site From Scratch

All code from this article and more is available on the trading site’s GitHub repository.

OPSkins’s ExpressTrade API was made public in June, 2018, giving web developers immense power and opportunities when working with virtual items. If you are unaware of what OPSkins and ExpressTrade are; OPSkins is the biggest online marketplace for virtual items such as CS:GO skins and more recently VGO skins. ExpressTrade briefly described is OPSkins’s solution to Steam’s measures to prevent scamming. These changes unfortunately had a very negative impact on trading on Steam, the platform where trading with virtual items was most prevalent. ExpressTrade solves this by creating an abstraction, an independent platform, where users can trade virtual items back and forth without the former restrictions. In this article we’re going to integrate the API to a website, a trading site, to demonstrate its vast usefulness. If you want more details on the technology behind ExpressTrade, read OPSkins’s official blog post!

What we’re writing

I could write a short article briefly summarizing the features the ExpressTrade API exposes, instead we’ll go through a thorough example of integrating the API to a trade site written from scratch. Not only discussing the API usage, but showing how virtual trading can easily be incorporated to any site despite its size and stack.

To those unaware of what “trade sites” are; they are essentially a way of users to exchange their virtual items for other virtual items where the owner generally earns revenue by increasing the valuation of their items. Trading sites operate by allowing the user to construct a trade by choosing a number of virtual items they desire, and then by specifying a number of items from themselves in regard to how they wish to “pay” for these desired items. Once satisfied the user initiates trading. The site then automatically sends a trade offer to the user with the aforementioned items. Trading sites were widely used trading CS:GO skins before Steam’s new regulations.

This article targets all developers from beginner to advanced. Every line of code of the example website is available on GitHub and I do recommend going through the source code by itself so you’ll get a sense of how everything ties together. Just below this text you can see a picture of the trading site we’re writing today. Ignore the simplistic design, as this article will focus on integrating the ExpressTrade API with basic trading functionality commonly seen on trading sites.

Selecting items from both inventories, then sending trade request. The backend API will then delegate the trade request to the ExpressTrade API to create the trade offer


I’m going to write the trading site integrated with ExpressTrade in NodeJS with the help of the web framework, Express. The frontend portion of the project will be built upon jQuery, simple Javascript, and the Axios module for HTTP requests. What you need to get started:

  • An OPSkins’s account, or create one now
  • A Steam account, needed to authenticate with Steam
  • NodeJS installed on your computer

The principles and use cases showcased can obviously be applied to any language and framework of your preference. I chose the previously mentioned stack due to its simplicity and lack of overhead. Furthermore, the styling could be completely omitted if you so desire. I intend to keep the styling as it’s adds very much without much of an expense.

Creating the project

Head over to your workspace directory and create a new project folder, I call mine “Trade Site”. Open your terminal in this folder, and create the following files and folders:

touch app.js config.json
mkdir controllers
mkdir helpers
mkdir public
mkdir public/images
mkdir public/js
mkdir public/css
mkdir services
mkdir views
mkdir views/layout

Next up, create a package.json file by calling npm init in your terminal and following the steps. Everything set to default is fine. A package.json file should’ve now been generated in your project directory. We can now start adding dependencies:

npm install axios body-parser bootstrap express express-handlebars jquery --save

We’re now ready to get started!

Configuring the configuration

Before diving into the codebase let’s write a configuration file which is supposed to hold all API keys, secrets, and important dynamic properties:

touch config.json
  • OPSkins API key; to access the ExpressTrade API.
  • Steam API key; allowing users to authenticate themselves with Steam on our third-party site.
  • Two factor secret; to access certain endpoints of the ExpressTrade API. More on this later.
  • Profit factor; an easily mutable constant that represents how high of a profit the owner is earning. 1.10 = 10% profit.
  • App id; the unique id of what type of items you want to accept when trading. 1 = VGO items, 730 = CS:GO items.
  • The session secret; used to hash your session cookie. We’re going to use sessions later when working with Steam authentication.

Retrieving your OPSkins’s API key

  • Go to
  • Click on the profile dropdown, then Account.
  • On the lefthand side, head over to the Advanced Options tab.
  • Scroll down until you see the API Key field. Click on the Show Key button.
  • You most likely have to enter your 2FA code. Once entered your API key should be visible. Grab the API key and enter it in your config.json file, that way it’s going to be used for all requests to endpoints where its needed.

Retrieving your Steam API key

  • Visit the Steam API Web Documentation page.
  • Fill out the form. Once finished, put the API key in your config.json file. The API key will later be used when authenticating a user with Steam.

“With great power comes great responsibility”

When an API is powerful enough, like the ExpressTrade API, you need additional security measures. For example, you want to prevent users from constructing trade offers to themselves, robbing you of all your items, by maliciously getting their hands on your OPSkins API key. Fortunately OPSkins had this in mind when designing the API and introduced the requirement of a 2FA code for these “dangerous” endpoints. Although, how about us who are currently writing automated trading systems? We don’t want to manually input a 2FA code for each outgoing trade. No worries, as there are numerous uncomplicated approaches to circumvent this problem.

Generating an OPSkins 2FA code

Start by creating a new service that has the responsibility of generating 2FA codes:

touch services/two-factor-service.js

Install the ‘node-2fa’ module. It has an extremely friendly API which makes it easy to generate 2FA tokens(codes).

npm install node-2fa --save

Fetching the OPSkins’s 2FA secret code

Notice all config.twoFactorSecret references in the two factor service? Well, in order to generate 2FA codes automatically you need your OPSkins 2FA secret code:

  • Visit
  • Click on the profile dropdown, then Account.
  • On the lefthand side, head over to the Account Security tab.
  • If you haven’t activated 2FA before, great! Follow the steps until you come to the step where a QR code is visible. If you have activated 2FA previously, you need to deactivated and then re-activate it.
  • Wait with scanning the QR code, and write down the displayed secret code. Put this in your config.json file under twoFactorSecret.
  • Finish the 2FA process by scanning the QR code or manually inputting the secret code in your preferred authenticator.

Starting the Express server

Let’s get the Express server up and running. The server will when we’re finished serve all clientside code, expose an API for our clientside to consume, and provide the clientside with Steam authentication. But first off, let’s get the server up and running, enter the code below in app.js.


Try running ‘node app’ in your project directory, the server should be initialized and display “Example app is running → PORT 5000”.

Great! Let’s continue by integrating Handlebars which is a templating extension that allows you to create dynamic pages depending on for example, if the user is logged in or logged out, etc. Certain directories are also made static. The reason for folders in node_modules set to be static is due to its ease of access on the frontend, otherwise the frontend wouldn’t be able to access these modules.


I set the ‘helpers’ property of the options object to include a number of helpers used throughout the templates. Helpers are functions that can be called from your Handlebars templates to transform data. To write your own helpers you need to create a new module:

touch helpers/handlebars.js

Afterwards add the following code:


The only helper worth mentioning is stringifyJSON, it’s essential when attaching data with the ‘data-*’ attribute to the item div elements when generating them with Handlebars. If you try to assign a ‘data-*’ attribute with a standard Javascript object you’re going to end up with a string with ‘Object object’ or similar. Therefore it’s important that you stringify your data before assigning it, the format doesn’t matter, as long as the formatted data is a string.

Upon registering Handlebars as the view engine I also defined a property called ‘defaultLayout’. This property simply refers to a template layout that all views should be a part of. It’s therefore a perfect place to put specific elements that is supposed to be available in all pages, such as the navbar, loading stylesheets, and including global scripts:


See that {{{body}}} tag? That’s a handlebars expression. Because this is our default layout the page template will be inserted in that expression. Everything besides that expression will be present throughout all templates. Due to this fact I include the CSS stylesheets in this file, furthermore, include scripts related to styling along with modules such as Axios which will be used when sending HTTP requests later on.

Logging in

Third-party websites either need access to the logged in user’s id(from OPSkins) or the user’s SteamId. The OPSkins’s userId is unobtainable by third-party sites(to my knowledge) but letting the user login with Steam works just as well. For Steam authentication you require a module called ‘steam-login’. To store user details we’re going to use sessions by utilizing the ‘express-session’ module. Both can be installed through npm:

npm install steam-login express-session --save

First things first, register the ‘steam-login’ module’s middleware in app.js:


Remember to import the ‘steam-login’ module to app.js as well. Otherwise it won’t work.

Create a new controller, authentication-controller.js, in your controllers folder. Add the following code:


We start by defining a router dedicated to the authentication routes.

  • ‘/’; the route that initiates the Steam authentication process, hence the ‘steam.authenticate()’ call of the Steam login module.
  • ‘/verify’; the route the user is redirected to once the authentication succeeds.
  • ‘/logout’; the route that clears the user’s session cookie, effectively logging the user out.

Finally, export the router, so it can be registered by our main router in app.js:


In order for the user data to be saved upon successful authentication we need to store that data somehow. That’s how sessions comes in, specifically the ‘express-session’ module. Head back to app.js. Start with importing the ‘express-session’ module. Then register it as middleware for the Express server:


Do not worry about manually setting the session data, the ‘steam-login’ module does this automatically.

Run the server! You should now be able to go to http://localhost:5000/authentication and authenticate yourself with Steam.

Creating the trade page

The trade page will be a simple representation of two parties, two inventories, and the ability to select items from each inventory. Between these inventories there will be trade data indicating the value of the items selected from each party and how much the user is underpaying/overpaying.

Let’s get down to it! Start by defining a route for ‘/’(the route hit when simply entering the URL) on the “global” router defined in app.js:


If you want, visit the base URL(http://localhost:5000), and you should see the message “Hello from the other side!”.

Having an insignificant message pop up when we want to trade some virtual items ain’t going to cut it. What we need to do next is fetch both the user inventory and the owner’s inventory. Then with the power of Handlebars we can render the page template and pass along these inventories. But first off, let’s load our own inventory(we’re the “owner” in this example).

Loading your own ExpressTrade inventory

Create a new service that will have the responsibility of fetching ExpressTrade inventories.

touch services/inventory-service.js

How are we going to fetch our own ExpressTrade inventory? By taking a peek at the ExpressTrade API documentation we have three apparent choices to make:

In my opinion the /IUser/GetInventory endpoint is the obvious choice as it only requires an API key and an app id. The endpoint is intelligent enough to connect your OPSkins account to the API key you sent along which makes it perfect to retrieve the “owner’s” inventory with. The endpoint’s expected input and output looks like the following:

/IUser/GetInventory inputs
/IUser/GetInventory outputs

For our purposes the only input we need to send with the request is the required app_id. Furthermore, the only output parameter we’re interested in is the array of items.


That’s quite a lot of code. Let’s take a look at the actual request towards the API endpoint:


This is the first we see of Axios. Axios is a HTTP module which is promise-based. Which means that the requests sent and the imminent responses can be handled as promises, which is a great alternative to callbacks regarding asynchronous code. But I prefer the ES7 async/await syntax which can be seen when making the request above.

The ExpressTrade API, for GET endpoints, expects the input formatted as query parameters. App id is the only query parameter in this case, and it’s required by the endpoint in order to know which type of items to retrieve. The trade site written in this article will focus on VGO virtual items which has an app id of 1. But you could just as well retrieve CS:GO inventories with app id 730 and trade CS:GO items. In the example I use string concatenation to set the app_id query parameter. It would look like this once processed:

Before returning the array of items present in the inventory we need to inflate the items’s valuation with the profit factor which we assigned previously in the configuration file. This is a fundamental step to ensure that the owner of the trade site yields a profit.

Profiting by inflating the owner’s inventory’s items valuation

Create a pricing service with the responsibility of assigning appropriate prices in regards to the profit factor:

touch services/pricing-service.js

Add the following:


Your inventory service for gathering your inventory(the owner) should now be fully functional.

Sending a request with authentication to the API

Something I haven’t explained is how requests are authenticated. Why is authentication needed with the ExpressTrade API? Because without it, the endpoint wouldn’t know who makes what requests. A problem that could occur is that thousands of requests could be sent each second which isn’t viable for an API. Moreover, an authenticated requests obviously lets the endpoint know who is asking for a resource, who is sending a request for a trade, etc. As demonstrated above, the /IUser/GetInventory route is able to return the right inventory solely by relying on the authenticated request.

OPSkins prefers that the API key is sent as your username in the HTTP Basic authorization. Which can be seen in the snippet above. Axios exposes an ‘auth’ parameter dedicated to Basic Authorization, simply set its username to your OPSkins’s API key and refrain from setting the password property.

Loading the user’s ExpressTrade inventory

The owner’s inventory is now retrieved, next up is requesting the user’s inventory which is a tad bit trickier. To my knowledge the a user’s OPSkins id is unobtainable by third-party site, that was the reason to incorporate Steam authentication in the first place. Fortunately for us the ExpressTrade API exposes an endpoint called /ITrade/GetUserInventoryFromSteamId to retrieve an inventory based on only a SteamID with the following expected input and output:

/ITrade/GetUserInventoryFromSteamId inputs
/ITrade/GetUserInventoryFromSteamId outputs

By glancing at the inputs and outputs we can figure out that the necessary inputs are both steam_id and app_id for our intentions. The only interesting output once again is the items array. So let’s write a new function for inventory service!


The differences between requesting the owner’s inventory with /IUser/GetInventory are a few. Firstly, we define a parameter for which inventory to retrieve by passing in a SteamID. Similar to before, the request parameters are appended as query parameters, which is exactly what we do with the SteamID. Just like before the app id is specified.

Updating the trade page route

With the inventory data at our fingertips, all that’s needed is to send that data to our Handlebars template for display! Import the inventory service. The app.js route should now look something like this:


We only want to retrieve the user’s inventory when a user is logged in. A simply conditional check is therefore written by checking the user session of the request object.

Notice the ‘.render()’ call on the response object? The first parameter refers to which Handlebars template to render. The second parameter is the data to send along with the template. The trade page needs access to the user’s inventory, the owner’s inventory, and user details. So the aforementioned is passed down to the template with keys making them easy to access; userItems, ownerItems, and user.

The trade page template

It wouldn’t be fair to leave you without the template code. The markup and styling is beyond the scope of this article, consequently there will be no explanation for the code. But it should be fairly self-explanatory. The full source is also available on GitHub.


Frontend logic: Item selection. Trade data. Trade validity.

A requirement for our trading site to operate as expected is to allow the user to select a number of items from their inventory and a number of items of your inventory(owner’s inventory).

Items selected from both inventories, ready to be traded

Start with creating a new clientside Javascript file that is intended to run only on the trading page. Ensure that the script is included in the trade page template, index.hbs:

touch public/js/trading.js

To store state regarding which items are selected and which are not; two dictionaries are defined. With the key of an index value and the value being the ‘Standard Item Object’. Here you have two choices; either store an index based on the Handlebars ‘#each’ iterator. Or keep the item id as index as the item ids are unique for VGO items. Both are excellent choices. But when writing loosely coupled code you have to consider: What happens if we some day change the type of items our trading site accepts? What if the new items doesn’t have an unique id property? With these questions in mind I recommend the former approach of generating indices on the frontend. Write the following in trading.js:


Trade data and trade validity are additional features I could go over, but they are not crucial. And to be honest they’re beyond the scope of this article. But the full trading.js script is available on GitHub.

Sending a trade offer when the user requests it

Let’s tie the click on the trade button to actually sending a HTTP request to our backend. Define a new function within trading.js with the responsibility of sending a POST request regarding a new trade:


Remember to call the function from the click event handler:


Catching the request on the backend

Our frontend is now working like intended! Although not much happens when we click the trade button after selecting a number of items. For that reason we need to define a route on our backend which is designated to handle API requests. When I use the word “API” in this context I’m referring to our backend, not the ExpressTrade API. But as you’ve might already guessed our API endpoint just delegates the request further to the ExpressTrade API if the trade fulfills the requirements, such as valuation of items in regards to the defined profit factor, etc.

Let’s start writing the API controller. Create a new controller:

touch controllers/api-controller.js

Next up is defining a function to run when a POST request is sent to the /api/request-trade route:


When working with POST requests you need to access the body data of the request. In this case this body data contains the items the user selected from both parties; itemsToExchange and itemsToReceive. To extract that data from the request you need the ‘body-parser’ middleware. Inside your app.js file, include the body-parser middleware like so:


The POST request’s body data can now be accessed with:


Delegating the request to the ExpressTrade API

To begin with, create a new service with the responsibility of constructing trade offers:

touch services/trade-service.js

Let’s define the requestTrade() function from our route function:


The requestTrade() function despite it’s worrying appearence does two simple things; verifies the trade and delegates the responsibility of sending the trade to another function, sendTrade().

The sendTrade() function has the responsibility of actually sending a request to the ExpressTrade API with the intention of constructing a trade offer. But which API endpoint fits our requirements? The two discernible options are:

Just as before /ITrade/SendOfferToSteamId stands out since it only requires the user’s SteamID in comparison to the user’s id or their trade URL.

/ITrade/SendOfferToSteamId inputs
/ITrade/SendOfferToSteamId outputs

An implementation of sendTrade() in the same file could look something like this:


Most of the function is code you’ve seen elsewhere in this codebase. Same authorization strategy, same Axios call when sending a POST request, etc. The biggest difference is the endpoint’s Content-type.

The ITrade/SendOfferToSteamId endpoint accepts either a content type of application/json or application/x-www-form-urlencoded. I chose the latter as I think key value pairs are cleaner if your data isn’t complex enough to warrant a dedicated JSON document. But in order to use this content type you need to encode your data properly just as the content type expects.

Registrering a new API router

A new type of route(API route) needs a router. It would be fitting to define a new router for all API routes in the app.js file:



The trading site’s core functionality is finally complete. If you spin up that server of yours you should now be able to log in, select a few items, and send a trade offer. Remember, you cannot save trade offers to yourself. Ideally you want to try testing the functionality with an other account unrelated to your OPSkins account.

Noticed any misspellings in the article? Something doesn’t make sense? Feel free to contact me about it, I’ll highly appreciate it!

The full source code is available on GitHub. Found something which doesn’t work? Want to improve the site? Send me a PR and I’ll most likely accept it!

Questions, want to reach out, or just follow me? Here’s my Twitter.