You made it! You've deployed a smart contract! We will take one MASSIVE step forward and build an entire blockchain application. What are we going to be making? A decentralized, censorship-resistant news headline aggregator based on hacker news. This is commonly referred to as a dApp or Distributed Application
What are we building?
A decentralized, censorship-resistant news headline aggregator based on hacker news. That's a lot of buzzwords. Let's break it down. This application will use to call the hacker news API. It'll return the latest news story and store the title and link in a smart contract. This will give us several advantages.
Distributed back-end
We will be using a blockchain to store the data, which will provide a fault-tolerant distributed back-end. This can be compared to a database that is shared across independent providers.
Censorship Resistance
Due to the decentralized nature of the blockchain, the data will be available as long as at least one node in the network is up and running, making it resistant to censorship.
Immutable
Smart contracts are immutable, providing transparency and accountability as all data modifications are visible on the blockchain.
How will we build it?
Create The Smart Contract
Below is a starting point for a basic Chainlink Functions Consumer smart contract.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol";
contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner {
using FunctionsRequest for FunctionsRequest.Request;
bytes32 public s_lastRequestId;
bytes public s_lastResponse;
bytes public s_lastError;
error UnexpectedRequestID(bytes32 requestId);
event Response(bytes32 indexed requestId, bytes response, bytes err);
// CUSTOM PARAMS - START
//Sepolia Router address;
// Additional Routers can be found at
// https://docs.chain.link/chainlink-functions/supported-networks
address router = 0xb83E47C2bC239B3bf370bc41e1459A34b41238D0;
//Functions Subscription ID
uint64 subscriptionId = 9999; // Fill this in with your subscription ID
//Gas limit for callback tx do not change
uint32 gasLimit = 300000;
//DoN ID for Sepolia, from supported networks in the docs
// Additional DoN IDs can be found at
// https://docs.chain.link/chainlink-functions/supported-networks
bytes32 donId = 0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000;
// Source JavaScript to run
string source = "";
// CUSTOM PARAMS - END
constructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}
function sendRequest() external onlyOwner returns (bytes32 requestId) {
FunctionsRequest.Request memory req;
req.initializeRequestForInlineJavaScript(source);
s_lastRequestId = _sendRequest(
req.encodeCBOR(),
subscriptionId,
gasLimit,
donId
);
return s_lastRequestId;
}
function fulfillRequest(
bytes32 requestId,
bytes memory response,
bytes memory err
) internal override {
if (s_lastRequestId != requestId) {
revert UnexpectedRequestID(requestId);
}
s_lastResponse = response;
s_lastError = err;
emit Response(requestId, s_lastResponse, s_lastError);
}
}
Adding Our News Source
We'll modify this contract to house our news articles and pass the correct call to Chainlink Functions.
First, let's add a struct and array to hold our articles. A struct is a custom-defined type that allows us to group variables. After bytes public s_lastError; add
Once we've added the event, the next step is to add our source JavaScript.
This JavaScript will be need to:
Fetch latest news stories from HackerNews
Filter out the latest story
Fetch the details of that story and story the title and URL
Return the story title and URL
The code we'll need looks something like this
const url = `https://hacker-news.firebaseio.com/v0/newstories.json`;
const newRequest = Functions.makeHttpRequest({ url });
const newResponse = await newRequest;
if (newResponse.error) {
throw Error(`Error fetching news`);
}
// HN can sometimes return story objects without a `url` property. So retry.
let itemIdx = 0
let done = false
let storyUrl;
while (!done) {
const latestStory = newResponse.data[itemIdx];
const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`;
const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL });
const storyResponse = await storyRequest;
if (!storyResponse.data.url) {
console.log("\nReturned object missing URL property. Retrying...")
itemIdx += 1
continue
}
storyUrl = storyResponse.data.url
done = true;
}
return Functions.encodeString(storyUrl);
One thing to note is you MUST have a ; after each statement. This is because we will store this script as a single line in our contract.
This JavaScript will be stored in the source variable in our contract. Replace the source variable line in the smart contract with the following which includes the entire JavaScript compressed into a single line with all semi colons!
This single line script will be stored in the source variable in our contract.
string source =
// handles errors where HN returns an object without a URL property.
"const url = `https://hacker-news.firebaseio.com/v0/newstories.json`; const newRequest = Functions.makeHttpRequest({ url }); const newResponse = await newRequest; if (newResponse.error) { throw Error(`Error fetching news`);} let itemIdx = 0; let done = false; let storyUrl; while (!done) { const latestStory = newResponse.data[itemIdx];const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`; const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL }); const storyResponse = await storyRequest; if (!storyResponse.data.url) {console.log(`\nReturned object missing URL property. Retrying...`); itemIdx += 1; continue;} storyUrl = storyResponse.data.url;done = true;}return Functions.encodeString(storyUrl);";
We will need to update subscriptionId. We'll come back to that later.
Within fulfillRequest(), we need to add logic to decode the response from our JavaScript, push the article into our articles array, and emit the ArticleAdded event. We'll add this code after the if checking s_lastRequestId
The last addition to our smart contract will be adding getAllArticles(), allowing us to see all of the articles we have saved so far.
// Function to return all articles
function getAllArticles() public view returns (string[] memory) {
string[] memory allArticles = new string[](articles.length);
for (uint i = 0; i < articles.length; i++) {
allArticles[i] = string(articles[i].url);
}
return allArticles;
}
Creating The Functions Subscription
To process our request, we'll need to create a Functions Subscription.
Connect your wallet and make sure you are on the Ethereum Sepolia test network.
Click 'Create Subscription'
Fill in your email address and select the checkbox for the Privacy Policy
Approve subscription creation
Sign the message to accept the Terms of Service
Select add funds
Add funds to your subscription. I'd suggest 10 LINK
Confirm the transaction
Deploy your contract with the subscription changed, see below
Add a consumer
At this point you have your subscription Id you'll need to add that to the smart contract before deploying it. Replace 999 with your value here
uint64 subscriptionId = 999;
The final code should look like this with the subscriptionId changed.
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/dev/v1_0_0/libraries/FunctionsRequest.sol";
contract FunctionsConsumerExample is FunctionsClient, ConfirmedOwner {
using FunctionsRequest for FunctionsRequest.Request;
bytes32 public s_lastRequestId;
bytes public s_lastResponse;
bytes public s_lastError;
error UnexpectedRequestID(bytes32 requestId);
struct Article {
bytes url;
uint256 publishedDate;
}
Article[] private articles;
event Response(bytes32 indexed requestId, bytes response, bytes err);
event ArticleAdded(bytes url, uint256 publishedDate);
// CUSTOM PARAMS - START
//Sepolia Router address;
// Additional Routers can be found at
// https://docs.chain.link/chainlink-functions/supported-networks
address router = 0xb83E47C2bC239B3bf370bc41e1459A34b41238D0;
//Functions Subscription ID
uint64 subscriptionId = 1796; // Fill this in with your subscription ID
//Gas limit for callback tx do not change
uint32 gasLimit = 300000;
//DoN ID for Sepolia, from supported networks in the docs
// Additional DoN IDs can be found at
// https://docs.chain.link/chainlink-functions/supported-networks
bytes32 donId =
0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000;
// Source JavaScript to run
string source =
"const url = `https://hacker-news.firebaseio.com/v0/newstories.json`; const newRequest = Functions.makeHttpRequest({ url }); const newResponse = await newRequest; if (newResponse.error) { throw Error(`Error fetching news`);} let itemIdx = 0; let done = false; let storyUrl; while (!done) { const latestStory = newResponse.data[itemIdx];const latestStoryURL = `https://hacker-news.firebaseio.com/v0/item/${latestStory}.json`; const storyRequest = Functions.makeHttpRequest({ url: latestStoryURL }); const storyResponse = await storyRequest; if (!storyResponse.data.url) {console.log(`\nReturned object missing URL property. Retrying...`); itemIdx += 1; continue;} storyUrl = storyResponse.data.url;done = true;}return Functions.encodeString(storyUrl);";
// CUSTOM PARAMS - END
constructor() FunctionsClient(router) ConfirmedOwner(msg.sender) {}
function sendRequest() external onlyOwner returns (bytes32 requestId) {
FunctionsRequest.Request memory req;
req.initializeRequestForInlineJavaScript(source);
s_lastRequestId = _sendRequest(
req.encodeCBOR(),
subscriptionId,
gasLimit,
donId
);
return s_lastRequestId;
}
function fulfillRequest(
bytes32 requestId,
bytes memory response,
bytes memory err
) internal override {
if (s_lastRequestId != requestId) {
revert UnexpectedRequestID(requestId);
}
articles.push(Article(response, block.timestamp));
emit ArticleAdded(response, block.timestamp);
s_lastResponse = response;
s_lastError = err;
emit Response(requestId, s_lastResponse, s_lastError);
}
// Function to return all articles
function getAllArticles() public view returns (string[] memory) {
string[] memory allArticles = new string[](articles.length);
for (uint i = 0; i < articles.length; i++) {
allArticles[i] = string(articles[i].url);
}
return allArticles;
}
}
Deploy Time
Alright, we have our contract ready to go! Now you'll need to deploy it to Sepolia. This should follow the same process we used for deploying the Hello World contract.
All of the code for this project can be found on
Head to to create this smart contract. The interface should look familiar. You can copy and paste this code directly into a new file in remix. You may encounter a message about pasted code. It should be okay to accept it and continue.
If you want to try out this code, head to . There, you can test the code and see the results.
Head to
Once your contract is deployed, you'll need to head back to and add the deployed contract as a consumer.
You're now ready to call sendRequest() from remix to initiate a Function request.
Once the request is complete getAllArticles() should return the latest headline from HackerNews.
Congratulations! You're successfully using Chainlink Functions to store data from an API on-chain!