What is Chainlink?
Chainlink is a decentralized network of nodes that provide data and information from off-blockchain sources to on-blockchain smart contracts via oracles1. In simpler terms, if we need to make a GET or POST request to an endpoint that doesn’t exist on the blockchain, we can use Chainlink services. The services they provide are quite useful and we will soon find out why.
What are Chainlink External Adapters (EA)?
Chainlink enables the easy integration of custom computations and specialized APIs using external adapters. EA’s are services which the core of the Chainlink node communicates via its API with a simple JSON specification2. Essentially, they act as a wrapper to our API that we can modify as we like. You can read more about them here. In this article, we will build an EA from scratch without using any Chainlink provided libraries. You’ll find that we do things a little differently compared to that described on Chainlink’s website. This makes it easier to understand the exact mechanism by which Chainlink helps us build an EA.
Why use them?
Executing transactions on blockchain costs real money, which can sometimes make it infeasible to run expensive computations on-chain. If you have a situation where a computation you are trying to execute is too expensive on-chain, you can use External Adapters to delegate that task off-chain. Apart from the expensive compute, blockchains are also public databases. Nothing is really hidden! When you need to make an API call that needs custom functionality or you need to secure the API Authentication headers, you can use External Adapters. The last case is exactly why we’re building an External Adapter.
What are we building?
We will use our EA’s to check prices for any equity or crypto using Alpaca APIs and then trade them using CLI. You can find the source code for the EA here. We will focus on checking prices and trading equities but we also leave in the code for checking crypto prices in the same file.
Building an External Adapter involves:
- Running a local express server
- Defining our External Adapters to check the price of an asset and place a trade.
- Sending curl requests to check prices and place trades on Alpaca
Let’s BUIDL!
Setting up our Project
Let’s start by setting up a package.json file. In the root directory of your project, run the following command in the terminal.
npm init
This will create a package.json file in the same directory. Now, remove all the contents of the file created and replace it with the following. Here, we are stating the dependencies we need to build our EA along with some predefined scripts that we will use to start our EA.
//package.json
{
"name": "EA-NodeJS-Template",
"version": "0.1.0",
"author": "",
"description": "",
"license": "MIT",
"scripts": {
"test": "./node_modules/.bin/_mocha --timeout 0",
"start": "node app.js"
},
"dependencies": {
"cross-fetch": "^3.1.5"
},
"devDependencies": {
"body-parser": "^1.19.0",
"chai": "^4.2.0",
"dotenv": "^16.0.0",
"eslint": "^7.0.0",
"eslint-config-standard": "^14.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.1",
"express": "^4.17.0",
"mocha": "^7.1.2"
}
}
Once the package.json file is created, you need to run npm install
to install the dependencies mentioned above.
Next, create an account with Alpaca to get your API keys. You can do this for free by signing up here. Once you have signed up and have your API keys ready, set up your .env file. This is where we hide our API secrets. In the repository mentioned above, we have included a .env.example file that shows how your .env file should look. It contains your Alpaca Key Id and Secret, along with some environment variables.
Now that we have our environment variables setup, let’s start writing the code for our EA. First, create an app.js file. This is where all our code necessary to create an EA is going to be.
Alpaca Secrets
Start by importing the cross-fetch and dotenv libraries. We need cross-fetch to make API calls to Alpaca and dotenv helps us access the environment variables we defined in our .env file. Then, we define the Alpaca relevant environment variables. These are needed to communicate with Alpaca.
// app.js
const fetch = require("cross-fetch");
require("dotenv").config();
const APCA_API_KEY_ID = process.env.APCA_API_KEY_ID;
const APCA_API_SECRET_KEY = process.env.APCA_API_SECRET_KEY;
const headers = {
"APCA-API-KEY-ID": APCA_API_KEY_ID,
"APCA-API-SECRET-KEY": APCA_API_SECRET_KEY,
};
Express server
Next, we define our local express server. This server will POST and GET requests for our External Adapter.
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = process.env.EA_PORT || 8080;
const host = process.env.EA_HOST || "172.17.0.1";
app.use(bodyParser.json());
app.listen(port, host, () => console.log(`Listening on port ${port}!`));
In the code above, we import express and body-parser dependencies. These were also defined in our package.json file and should have been installed when you ran npm install
. Then, we instantiate our express server and define the port and host address of the server. You can define your own port and host if you like. Then, we ask our newly instantiated express server to use bodyParser. bodyParser acts as a middleware for node.js and helps us parse the request body in JSON format. Finally, we ask our EA to listen on a port we defined earlier.
Defining Adapters
Now that our server is defined, let’s build our adapters to talk to Alpaca. This is where the bulk of the logic goes that wraps our API and helps us hide our secret keys. Let’s start with defining the adapter for checking Equity prices.
const getEquitiesPrice = async (input) => {
const jobRunId = typeof input.id === "undefined" ? 1 : input.id;
try {
const { symbol } = input.data;
if (!symbol) throw new Error("Symbol is required");
const url = `https://data.alpaca.markets/v2/stocks/${symbol}/quotes/latest`;
const response = await fetch(url, { headers });
const data = await response.json();
const price = data.quote.ap;
return {
status: response.status,
result: { jobRunId, price },
};
} catch (error) {
return {
status: 500,
result: {
jobRunId, status: "errored", error: "AdapterError", message: error.message, statusCode: 500,
}
}
}
};
We start by accepting an input object. This input object is going to be passed in an id and a data object when we test our adapter out. First, we initialize the jobRunId using the id that was passed in through our input. Then, using a try catch block we deconstruct the data object that was passed in. The value we are looking for when deconstructing is the symbol of equity. If no symbol is passed in we throw an error message. Then using the symbol as a template literal, we define our request url. This url is specific for checking the latest quotes on an equity. If you would like to check the latest quotes on crypto then you will need to use another url. You can find more information on the available market data endpoints here.
Now that our url and headers are defined, we can make a fetch request to the url and parse its response in JSON. We deconstruct our response data by following the path that returns the asking price. In the case above, the JSON object being returned follows path data->quote -> ap. `ap` is the asking price of the said equity. To better understand what your response might look like you might want to check out Alpaca’s Postman workspace. Finally, we return the response status code and result object. Our result object contains the jobRunId we defined above along with the asking price of the asset. If any of the above steps fail while sending the request, we return an error message in the catch block with status code 500.
Similarly, we can define EA’s for different API endpoints we want to request data from. The example you saw above requests Equity prices, but what if you wanted to place a trade?
Let’s go through another EA that places an order using Alpaca’s trading APIs. You can find their trading APIs documentation here.
const tradeAlpaca = async (input) => {
const jobRunId = typeof input.id === "undefined" ? 1 : input.id;
try {
const { symbol, qty , side} = input.data;
if (!symbol) throw new Error("Symbol is required");
if (!qty) throw new Error("Quantity is required");
if (!side) throw new Error("Buy/Sell Side is required");
const body = {symbol, qty, side, type:"market", time_in_force:"day"};
const url = `https://paper-api.alpaca.markets/v2/orders`;
const response = await fetch(url, { headers, body: JSON.stringify(body), method: 'POST' });
const data = await response.json();
const orderStatus = data.status;
return {
status: response.status,
result: { jobRunId, orderStatus },
};
} catch (error) {
return {
status: 500,
result: {
jobRunId, status: "errored", error: "AdapterError", message: error.message, statusCode: 500,
}
}
}
};
In the code block above, we are trying to place a trade given an input. We deconstruct our input to get the symbol, qty (quantity of the asset you want to trade) and the side (buy/sell) of the trade we want to execute. If either of them aren’t passed, we throw an error message.
Next, to send this as a post request, we need to define a body for the request. This is essentially an object containing required trade parameters. You can find out more about the required parameters for the orders endpoint here. Once we have the body of our request we are ready to send the request using fetch. This time our fetch request looks a little different, right? This is because we are making a POST request to submit an order to Alpaca.
Once we send this request and have the response object available, we turn it into JSON format to easily access the order attributes. In the adapter above, I am returning the jobRunId along with the order status. You can find details about other order attributes that are returned here.
As we saw earlier, if the above request fails at any time we return an error message along with a status code of 500.
Let’s take a look at what we have done so far. We have:
- Set up our Alpaca keys
- Started our Express server
- Defined 2 adapters (getEquityPrice and tradeAlpaca)
Now, let's ask our express server to route our requests to the right External Adapters.
Express Routing
app.post("/alpacatrade", async (req, res) => {
const { status, result } = await tradeAlpaca(req.body);
res.status(status).json(result);
});
app.post("/equitiesprice", async (req, res) => {
const { status, result } = await getEquitiesPrice(req.body);
res.status(status).json(result);
});
In the code above, we are creating two separate routes, one for trading equities and one for checking equity prices. Each of these routes call their respective EA’s separately and in an asynchronous way. To trade equities and crypto, we define the route /alpacatrade
while equitiesprice
lets us check the price for an equity. In our EA’s, we return a status and a result. We deconstruct the response from the EA’s and return that to CLI when running the request.
Hooray! We’ve successfully completed writing our EA and are ready to test it out on CLI.
Curl Requests using CLI
To send requests from CLI, we need two terminal windows (Terminal A and Terminal B). Terminal A will run our express server and Terminal B will send requests. We are ready to send requests directly from CLI.
First, let’s send a request to check the price of an Equity. We will try to check the price of $COIN (Coinbase stock).
curl -X POST -H "content-type:application/json" "http://localhost:8080/equitiesprice" --data '{ "id": 0, "data": { "symbol": "COIN"} }'
In the code above, we are trying to make a POST request for our JSON data using Curl. Once we specify the kind of request we’re making and the type of data we’re posting, we need to specify the route of the request. In our case, we have defined the host in our .env
file as localhost and our port as 8080. Since we are checking the price of an equity, we need to route our request to /equitiesprice
. Now that we know where we are sending the request, we need to pass in our input with a data flag. The input is fed in as a JSON object. As we saw earlier while defining our getEquityPrices
EA, we need the input to have an id and a symbol. That’s exactly what we are doing here. We’re feeding in a jobRunId
as id
and giving it an arbitrary value (0 here) along with the symbol of the asset we are checking the price for.
After running the curl command, a successful response should look like the one you see below. Here, jobRunId is the id
we passed in and the price is the asking price of $COIN.
{"jobRunId":0,"price":70.55}
Next, let’s try to run a command that executes a buy trade on Alpaca. We will try to buy one share of Coinbase stock $COIN.
curl -X POST -H "content-type:application/json" "http://localhost:8080/alpacatrade" --data '{ "id": 0, "data": { "symbol": "COIN", "qty":"1","side":"buy"} }'
This command is similar to the one we saw earlier. You’ll notice that we are routing our request to `/alpacatrade` instead. This was the route we defined earlier that called the `tradeAlpaca` EA. We then pass it the data object that contains an id and a data JSON object containing the necessary trade parameters. Once called, a successful response should look like this.
{"jobRunId":0,"orderStatus":"accepted"}
orderStatus is the status of the trade order we just placed. In this case, the order was accepted and we successfully bought 1 share of $COIN.
Takeaways
Congrats! You now know how to build a Chainlink External Adapter from scratch. Although we didn’t use any Chainlink library to build it, it can still be hosted on a Chainlink node. You learned how to make a GET request to get price from Alpaca endpoints and even sent a POST request to submit an order. The EA’s can be modified to suit any of Alpaca’s endpoints. They can be written in any language, and even run on separate machines, to include serverless functions3.
In later articles, we’ll see how to host this EA on an AWS-like service and connect it to our own node.
Sources
- https://www.gemini.com/cryptopedia/what-is-chainlink-and-how-does-it-work
- https://docs.chain.link/docs/developers/#requesting-data
- https://docs.chain.link/docs/external-adapters/
Please note that this article is for general informational purposes only. All examples are for illustrative purposes only. Alpaca does not recommend any specific securities, cryptocurrencies or investment strategies.
All investments involve risk and the past performance of a security, or financial product does not guarantee future results or returns. Keep in mind that while diversification may help spread risk it does not assure a profit, or protect against loss. There is always the potential of losing money when you invest in securities, or other financial products. Investors should consider their investment objectives and risks carefully before investing.
Brokerage services are provided by Alpaca Securities LLC ("Alpaca Securities"), member FINRA/SIPC, a wholly-owned subsidiary of AlpacaDB, Inc. Technology and services are offered by AlpacaDB, Inc.
Cryptocurrency is highly speculative in nature, involves a high degree of risks, such as volatile market price swings, market manipulation, flash crashes, and cybersecurity risks. Cryptocurrency is not regulated or is lightly regulated in most countries. Cryptocurrency trading can lead to large, immediate and permanent loss of financial value. You should have appropriate knowledge and experience before engaging in cryptocurrency trading. For additional information please click here.
Cryptocurrency services are made available by Alpaca Crypto LLC ("Alpaca Crypto"), a FinCEN registered money services business (NMLS # 2160858), and a wholly-owned subsidiary of AlpacaDB, Inc. Alpaca Crypto is not a member of SIPC or FINRA. Cryptocurrencies are not stocks and your cryptocurrency investments are not protected by either FDIC or SIPC. Please see the Disclosure Library for more information.
This is not an offer, solicitation of an offer, or advice to buy or sell cryptocurrencies, or open a cryptocurrency account in any jurisdiction where Alpaca Crypto is not registered or licensed, as applicable.