![Absolute Labs](https://s3.amazonaws.com/assets.heroku.com/addons.heroku.com/icons/9616/original.png)
This add-on is operated by Absolute Labs Inc.
Leverage augmented, standardized blockchain data across 5+ chains, using SQL
Absolute Labs
Last updated July 26, 2024
The Absolute Labs add-on is currently in beta.
Table of Contents
Absolute Labs is an add-on for providing SQL access to augmented, standardized blockchain data across several blockchains.
Using SQL, you can augment your app with up-to-date stats about any wallet, digital asset, or decentralized application (dapp). You can build advanced Web3 products without having to set up the whole infrastructure yourself.
By using REST calls in several programming languages, you can access the Absolute Labs data APIs to post SQL query jobs and fetch results.
Provisioning the Add-on
Attach Absolute Labs to a Heroku application via the CLI:
Reference the Absolute Labs Name Elements Page for a list of available plans and regions.
$ heroku addons:create absolutelabs
Creating absolutelabs on example-app... free
Your add-on has been provisioned successfully
After provisioning Absolute Labs, the ABSOLUTE_LABS_API_KEY
config var is available in the attached app’s configuration. It contains your unique API key used to post and retrieve your SQL jobs. You can see the config var via the heroku config:get
command:
$ heroku config:get ABSOLUTE_LABS_API_KEY
ABCDE-12345-FGHIJ
After installing Absolute Labs, the application is fully configured to integrate with the add-on.
Local Setup
Environment Setup
After provisioning the add-on, replicate its config vars locally for development environments that need the service.
Use the local
Heroku CLI command to configure, run, and manage process types specified in your app’s Procfile. Heroku Local reads configuration variables from a .env
file. Use heroku config
to view an app’s configuration variables in a terminal. Use the following command to add a configuration variable to a local .env
file:
$ heroku config:get ABSOLUTE_LABS_API_KEY -s >> .env
Don’t commit credentials and other sensitive configuration variables to source control. In Git exclude the .env
file with: echo .env >> .gitignore
.
For more information, see the Heroku Local article.
General Usage
The Absolute Labs add-on allows you to execute queries against our data center of blockchain data by posting query jobs via REST calls. After executing the queries, you can fetch the status or results of them asynchronously.
To interact with the APIs, use your ABSOLUTE_LABS_API_KEY
in an HTTP header x-api-key
when interacting with the endpoints provided in the Endpoints section. Endpoints receive their parameters in a JSON body.
Each job scans a given amount of data measured in GB. Your plan gives you a monthly quota in GB, measured based on 30-day periods starting from the time of add-on provisioning. Jobs consumption is measured by increments of 1 MB scanned.
Endpoints
Job Creation
Endpoint
POST https://heroku.absolutelabs.app/api/jobs/
Parameters
Key | Required | Type | Description |
---|---|---|---|
query | Required | String | The SQL query to execute |
Return Values
Key | Type | Description |
---|---|---|
job_id | String | The ID to retrieve status and results for this job |
Discussion
The endpoint doesn’t verify the validity of the query and tries to run it directly. It returns 200
as long as it can post the provided query, which must be present.
Job Status
Endpoint
GET https://heroku.absolutelabs.app/api/jobs/<job_id>/
Parameters
Key | Required | Type | Description |
---|---|---|---|
job_id | Required | String | ID of a job obtained when posting it |
Return Values
Key | Type | Description |
---|---|---|
job_id | String | The ID to retrieve status and results for this job |
status | String | One of Success, Failed, Pending |
bytes_scanned | Integer | Quota consumption in bytes - only returned if the job succeeded |
Discussion
A failed job doesn’t count against your quota.
Job Results
Endpoint
GET https://heroku.absolutelabs.app/api/jobs/<job_id>/results/
Parameters
Key | Required | Type | Description |
---|---|---|---|
per_page | Optional | Integer | Defaults to 100 |
page_number | Optional | Integer | Defaults to 1 |
Return Values
Key | Type | Description |
---|---|---|
rows | array | Rows of your SQL request |
Example Queries
The main two tables you can query are fungible tokens and non-fungible tokens (NFTs).
Fungible Tokens
Table
al-production-blockchain-etl.main.ft_tokens_wl
Description
Lists all fungible tokens referenced in the Absolute Labs dataset, with both their market level stats and its holder stats.
Example Query
SELECT name, holder_count, relevant_holder_count, monthly_active_addresses
FROM `main.ft_tokens_wl`
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'
Non-Fungible Tokens (NFTs)
Table
al-production-blockchain-etl.main.nft_collections_wl
Description
Lists all NFT collections referenced in the Absolute Labs dataset, with both their market level stats and its holder stats.
Example Query
SELECT name, holder_count, holder_count, monthly_active_addresses, current_stats
FROM `main.nft_collections_wl`
WHERE contract_address = '0x78d61c684a992b0289bbfe58aaa2659f667907f8' and blockchain = 'ethereum'
Language-Specific Integrations
There are no custom packages available at this time. See the code examples for how to post a job depending on the language.
Ruby
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
require 'net/http'
require 'json'
require 'uri'
url = 'https://heroku.absolutelabs.app/api/jobs/'
ABSOLUTE_LABS_API_KEY = ENV['ABSOLUTE_LABS_API_KEY']
payload = {
query: "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses \
FROM `main.ft_tokens_wl` \
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"
}.to_json
headers = {
'Content-Type' => 'application/json',
'X-API-KEY' => ABSOLUTE_LABS_API_KEY
}
uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.path, headers)
request.body = payload
response = http.request(request)
job_id = JSON.parse(response.body)['job_id']
puts "Job ID: #{job_id}"
puts "Waiting 5 seconds for the job to finish..."
# Sleep for 5 seconds
sleep(5)
# Query the results
results_url = "https://heroku.absolutelabs.app/api/jobs/#{job_id}/results"
results_uri = URI(results_url)
request = Net::HTTP::Get.new(results_uri, headers)
results_response = http.request(request)
results = JSON.parse(results_response.body)
puts "Results: #{results}"
Python
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
import os
import time
import requests
import json
url = "https://heroku.absolutelabs.app/api/jobs/"
ABSOLUTE_LABS_API_KEY = os.environ.get("ABSOLUTE_LABS_API_KEY")
payload = {
"query": "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses \
FROM `main.ft_tokens_wl` \
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"
}
headers = {
"Content-Type": "application/json",
"X-API-KEY": ABSOLUTE_LABS_API_KEY
}
response = requests.post(url, json=payload, headers=headers)
job_id = json.loads(response.text)['job_id']
print("Job ID:", job_id)
print("Waiting 5 seconds for the job to finish...")
# Sleep for 5 seconds
time.sleep(5)
# Query the results
results_url = f"https://heroku.absolutelabs.app/api/jobs/{job_id}/results"
results_response = requests.get(results_url, headers=headers)
# Print the results
results = json.loads(results_response.text)
print("Results:", results)
Java
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import org.json.JSONObject;
public class ApiJob {
private static final String API_URL = "https://heroku.absolutelabs.app/api/jobs/";
private static final String ABSOLUTE_LABS_API_KEY = System.getenv("ABSOLUTE_LABS_API_KEY");
public static void main(String[] args) throws Exception {
// Prepare the payload
JSONObject payload = new JSONObject();
payload.put("query", "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses "
+ "FROM `main.ft_tokens_wl` "
+ "WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'");
// Prepare the headers
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
headers.put("X-API-KEY", ABSOLUTE_LABS_API_KEY);
// Send the POST request
String response = sendPostRequest(API_URL, payload.toString(), headers);
JSONObject jsonResponse = new JSONObject(response);
String jobId = jsonResponse.getString("job_id");
System.out.println("Job ID: " + jobId);
System.out.println("Waiting 5 seconds for the job to finish...");
// Sleep for 5 seconds
Thread.sleep(5000);
// Query the results
String resultsUrl = API_URL + jobId + "/results";
String resultsResponse = sendGetRequest(resultsUrl, headers);
// Print the results
JSONObject results = new JSONObject(resultsResponse);
System.out.println("Results: " + results.toString(2));
}
private static String sendPostRequest(String urlString, String payload, Map<String, String> headers) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
for (Map.Entry<String, String> header : headers.entrySet()) {
conn.setRequestProperty(header.getKey(), header.getValue());
}
conn.setDoOutput(true);
try (OutputStream os = conn.getOutputStream()) {
byte[] input = payload.getBytes("utf-8");
os.write(input, 0, input.length);
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
return response.toString();
}
}
private static String sendGetRequest(String urlString, Map<String, String> headers) throws IOException {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
for (Map.Entry<String, String> header : headers.entrySet()) {
conn.setRequestProperty(header.getKey(), header.getValue());
}
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "utf-8"))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
return response.toString();
}
}
}
This Java code uses the HttpURLConnection
class to send HTTP requests and handle responses. It also uses the org.json
library to handle JSON objects, which you must include in your project. You can add it to your project using Maven:
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20210307</version>
</dependency>
Or you can download the JSON library from the Maven repository and include it in your project’s classpath.
NodeJS
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
const https = require('https');
const { URL } = require('url');
const url = 'https://heroku.absolutelabs.app/api/jobs/';
const ABSOLUTE_LABS_API_KEY = process.env.ABSOLUTE_LABS_API_KEY;
const payload = JSON.stringify({
query: "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses \
FROM `main.ft_tokens_wl` \
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"
});
const headers = {
'Content-Type': 'application/json',
'X-API-KEY': ABSOLUTE_LABS_API_KEY
};
const postRequestOptions = {
method: 'POST',
headers: headers
};
const postReq = https.request(url, postRequestOptions, (postRes) => {
let data = '';
postRes.on('data', (chunk) => {
data += chunk;
});
postRes.on('end', () => {
const response = JSON.parse(data);
const job_id = response.job_id;
console.log('Job ID:', job_id);
console.log('Waiting 5 seconds for the job to finish...');
setTimeout(() => {
const results_url = new URL(`https://heroku.absolutelabs.app/api/jobs/${job_id}/results`);
const getRequestOptions = {
headers: headers
};
https.get(results_url, getRequestOptions, (getRes) => {
let resultsData = '';
getRes.on('data', (chunk) => {
resultsData += chunk;
});
getRes.on('end', () => {
const results = JSON.parse(resultsData);
console.log('Results:', results);
});
}).on('error', (err) => {
console.error('Error:', err.message);
});
}, 5000);
});
});
postReq.on('error', (err) => {
console.error('Error:', err.message);
});
postReq.write(payload);
postReq.end();
PHP
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
<?php
$url = 'https://heroku.absolutelabs.app/api/jobs/';
$ABSOLUTE_LABS_API_KEY = getenv('ABSOLUTE_LABS_API_KEY');
$payload = json_encode(array(
'query' => "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses \
FROM `main.ft_tokens_wl` \
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"
));
$headers = array(
'Content-Type: application/json',
'X-API-KEY: ' . $ABSOLUTE_LABS_API_KEY
);
$options = array(
'http' => array(
'header' => $headers,
'method' => 'POST',
'content' => $payload
)
);
$context = stream_context_create($options);
$response = file_get_contents($url, false, $context);
$responseData = json_decode($response, true);
$job_id = $responseData['job_id'];
echo "Job ID: " . $job_id . "\n";
echo "Waiting 5 seconds for the job to finish...\n";
// Sleep for 5 seconds
sleep(5);
// Query the results
$results_url = 'https://heroku.absolutelabs.app/api/jobs/' . $job_id . '/results';
$options = array(
'http' => array(
'header' => $headers,
'method' => 'GET'
)
);
$context = stream_context_create($options);
$results_response = file_get_contents($results_url, false, $context);
$results = json_decode($results_response, true);
echo "Results: ";
print_r($results);
?>
Go
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
)
const url = "https://heroku.absolutelabs.app/api/jobs/"
type Payload struct {
Query string `json:"query"`
}
type JobResponse struct {
JobID string `json:"job_id"`
}
func main() {
ABSOLUTE_LABS_API_KEY := os.Getenv("ABSOLUTE_LABS_API_KEY")
payload := Payload{
Query: "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses " +
"FROM `main.ft_tokens_wl` " +
"WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'",
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
fmt.Println("Error marshalling payload:", err)
return
}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-KEY", ABSOLUTE_LABS_API_KEY)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
var jobResponse JobResponse
err = json.Unmarshal(body, &jobResponse)
if err != nil {
fmt.Println("Error unmarshalling response:", err)
return
}
fmt.Println("Job ID:", jobResponse.JobID)
fmt.Println("Waiting 5 seconds for the job to finish...")
time.Sleep(5 * time.Second)
resultsURL := url + jobResponse.JobID + "/results"
req, err = http.NewRequest("GET", resultsURL, nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
req.Header.Set("X-API-KEY", ABSOLUTE_LABS_API_KEY)
resp, err = client.Do(req)
if err != nil {
fmt.Println("Error sending request:", err)
return
}
defer resp.Body.Close()
body, err = ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("Error reading response body:", err)
return
}
var results map[string]interface{}
err = json.Unmarshal(body, &results)
if err != nil {
fmt.Println("Error unmarshalling response:", err)
return
}
resultsJSON, err := json.MarshalIndent(results, "", " ")
if err != nil {
fmt.Println("Error marshalling results:", err)
return
}
fmt.Println("Results:", string(resultsJSON))
}
Clojure
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
(ns api-job
(:require [clj-http.client :as client]
[cheshire.core :as json]
[clojure.java.io :as io]
[clojure.string :as str]))
(def api-url "https://heroku.absolutelabs.app/api/jobs/")
(def api-key (System/getenv "ABSOLUTE_LABS_API_KEY"))
(defn get-job-id [response]
(let [body (json/parse-string (:body response) true)]
(:job_id body)))
(defn main []
(let [payload {:query "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses \
FROM `main.ft_tokens_wl` \
WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"}
headers {"Content-Type" "application/json"
"X-API-KEY" api-key}
response (client/post api-url {:body (json/generate-string payload)
:headers headers})
job-id (get-job-id response)]
(println "Job ID:" job-id)
(println "Waiting 5 seconds for the job to finish...")
(Thread/sleep 5000)
(let [results-url (str api-url job-id "/results")
results-response (client/get results-url {:headers headers})
results (json/parse-string (:body results-response) true)]
(println "Results:" (json/generate-string results {:pretty true})))))
(main)
To run this Clojure code, you must add the dependencies for clj-http
and cheshire
to your project. If you’re using Leiningen, add the following to your project.clj
:
(defproject api-job "0.1.0-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.3"]
[clj-http "3.12.3"]
[cheshire "5.10.0"]])
Scala
Use the following sample code to post a job. Don’t forget to replace the API key placeholder with your actual API key.
import java.net.{HttpURLConnection, URL}
import java.io.{BufferedReader, InputStreamReader, OutputStream}
import scala.io.Source
import scala.util.parsing.json.JSON
object ApiJob {
val apiUrl = "https://heroku.absolutelabs.app/api/jobs/"
val apiKey = sys.env("ABSOLUTE_LABS_API_KEY")
def main(args: Array[String]): Unit = {
val payload =
"""
|{
| "query": "SELECT name, holder_count, relevant_holder_count, monthly_active_addresses FROM `main.ft_tokens_wl` WHERE contract_address = '0x4d224452801aced8b2f0aebe155379bb5d594381' and blockchain = 'ethereum'"
|}
""".stripMargin
val jobId = createJob(payload)
println(s"Job ID: $jobId")
println("Waiting 5 seconds for the job to finish...")
Thread.sleep(5000)
val results = queryResults(jobId)
println(s"Results: $results")
}
def createJob(payload: String): String = {
val url = new URL(apiUrl)
val connection = url.openConnection().asInstanceOf[HttpURLConnection]
connection.setRequestMethod("POST")
connection.setRequestProperty("Content-Type", "application/json")
connection.setRequestProperty("X-API-KEY", apiKey)
connection.setDoOutput(true)
val outputStream: OutputStream = connection.getOutputStream
outputStream.write(payload.getBytes("UTF-8"))
outputStream.flush()
outputStream.close()
val response = getResponse(connection)
val jsonResponse = JSON.parseFull(response).get.asInstanceOf[Map[String, Any]]
jsonResponse("job_id").toString
}
def queryResults(jobId: String): String = {
val resultsUrl = s"$apiUrl$jobId/results"
val url = new URL(resultsUrl)
val connection = url.openConnection().asInstanceOf[HttpURLConnection]
connection.setRequestMethod("GET")
connection.setRequestProperty("X-API-KEY", apiKey)
getResponse(connection)
}
def getResponse(connection: HttpURLConnection): String = {
val inputStream = connection.getInputStream
val response = Source.fromInputStream(inputStream).mkString
inputStream.close()
response
}
}
To run this Scala code, you must include the necessary dependencies for JSON parsing. If you’re using SBT, add the following to your build.sbt
:
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
Monitoring and Logging
Display stats and the current state of Absolute Labs via the CLI:
$ heroku absolutelabs:command
example output
Dashboard
The Absolute Labs add-on dashboard provides a view on the jobs you created, along with your current quota consumption over the current 30-day period.
Access the dashboard via the CLI:
$ heroku addons:open absolutelabs
Opening absolutelabs for sharp-mountain-4005
or by visiting the Heroku Dashboard and selecting the application in question. Select Absolute Labs
from the Add-ons
menu.
Troubleshooting
If your jobs aren’t executing, try running a simple query using one of the sample codes to confirm your API key is functioning. If you continue experiencing issues, contact us.
Migrating Between Plans
Application owners must carefully manage the migration timing to ensure proper application function during the migration process.
Use the heroku addons:upgrade
command to migrate to a new plan.
$ heroku addons:upgrade absolutelabs:newplan
-----> Upgrading absolutelabs:newplan to example-app.. done, v18 ($49/mo)
Your plan has been updated to: absolutelabs:newplan
Removing the Add-on
Remove Absolute Labs via the CLI:
This action destroys all associated data and you can’t undo it!
$ heroku addons:destroy absolutelabs
-----> Removing absolutelabs from example-app... done, v20 (free)
You can’t export your history of jobs posted before removing the add-on. To export your history, contact us via email.
Support
Submit all Absolute Labs support and runtime issues via one of the Heroku Support channels. Any non-support-related issues or product feedback is welcome at support@absolutelabs.io.