curl --request GET \
--url https://api.webacy.com/rwa/hci \
--header 'x-api-key: <api-key>'import requests
url = "https://api.webacy.com/rwa/hci"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.webacy.com/rwa/hci', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.webacy.com/rwa/hci",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.webacy.com/rwa/hci"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.webacy.com/rwa/hci")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.webacy.com/rwa/hci")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"chain": "arb",
"address": "0x021289588cd81dc1ac87ea91e91607eef68303f5",
"symbol": "ustbl",
"name": "Spiko US T-Bills Money Market Fund",
"holderConcentration": {
"top10": {
"index": 0.3504,
"topSharePct": 40.06,
"rawTopSharePct": 80.37,
"source": "holders",
"riskBand": "medium"
},
"top30": {
"index": 0.3521,
"topSharePct": 50.29,
"rawTopSharePct": 92.68,
"source": "holders",
"riskBand": "medium"
},
"holderCount": 199
}
},
{
"chain": "eth",
"address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e",
"symbol": "crvusd",
"name": "crvUSD",
"holderConcentration": {
"top10": {
"index": 0.0012,
"topSharePct": 0.12,
"rawTopSharePct": 77.82,
"source": "holders",
"riskBand": "low"
},
"top30": null,
"top30NullReason": "organic_cohort_not_larger_than_top10",
"holderCount": 94962
}
}
],
"meta": {
"generatedAt": "2026-10-06T17:10:23.611640Z",
"stale": false,
"page": 1,
"pageSize": 50,
"total": 2,
"totalPages": 1,
"totalUnfiltered": 2,
"byRiskBand": {
"low": 1,
"medium": 1,
"high": 0,
"extreme": 0
},
"riskBandThresholds": {
"low": [
0,
0.2
],
"medium": [
0.2,
0.4
],
"high": [
0.4,
0.6
],
"extreme": [
0.6,
1
]
}
}
}{
"message": "Unauthorized"
}{
"x402Version": 1,
"error": "payment required"
}List all stablecoin and rwa holder data
Returns a paginated list of stablecoins and RWA tokens with daily-updated holder concentration data. Each record includes top-10 and top-30 cohort objects with an index, a topSharePct and a risk band. topSharePct is the share of supply (%) held by the top-N organic holders: wallets labelled as exchanges, bridges or pools and any contract holder (staking vaults, lending markets, bridges) are excluded. index (0–1) is that organic top-N share minus the fair-share baseline effective N / holderCount (effective N is N, or fewer when fewer organic holders were fetched; nothing is subtracted when holderCount is at most effective N), floored at 0, so it reads close to topSharePct / 100 for widely held tokens; on source: aggregate cohorts it is computed from the raw share instead, because contracts cannot be excluded without per-holder data. It is not a Herfindahl index. Every cohort also carries rawTopSharePct, the same share with every holder counted, and source (holders or aggregate). When every top-N holder is a protocol contract, note is all_top_n_are_protocol_contracts_organic_low, index and topSharePct are 0 and rawTopSharePct shows the protocol-held share. top30 is null when the pipeline has no value for it; top30NullReason says why (for example the organic top-30 would only repeat the organic top-10). meta.byRiskBand counts are computed before the riskBand filter so dashboards always see the full ecosystem distribution. Every riskBand (on each cohort, in the riskBand filter and in meta.byRiskBand) is derived from the cohort index with the thresholds published in meta.riskBandThresholds, so a caller can always re-derive and verify it.
curl --request GET \
--url https://api.webacy.com/rwa/hci \
--header 'x-api-key: <api-key>'import requests
url = "https://api.webacy.com/rwa/hci"
headers = {"x-api-key": "<api-key>"}
response = requests.get(url, headers=headers)
print(response.text)const options = {method: 'GET', headers: {'x-api-key': '<api-key>'}};
fetch('https://api.webacy.com/rwa/hci', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.webacy.com/rwa/hci",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"x-api-key: <api-key>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.webacy.com/rwa/hci"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("x-api-key", "<api-key>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.webacy.com/rwa/hci")
.header("x-api-key", "<api-key>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.webacy.com/rwa/hci")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["x-api-key"] = '<api-key>'
response = http.request(request)
puts response.read_body{
"data": [
{
"chain": "arb",
"address": "0x021289588cd81dc1ac87ea91e91607eef68303f5",
"symbol": "ustbl",
"name": "Spiko US T-Bills Money Market Fund",
"holderConcentration": {
"top10": {
"index": 0.3504,
"topSharePct": 40.06,
"rawTopSharePct": 80.37,
"source": "holders",
"riskBand": "medium"
},
"top30": {
"index": 0.3521,
"topSharePct": 50.29,
"rawTopSharePct": 92.68,
"source": "holders",
"riskBand": "medium"
},
"holderCount": 199
}
},
{
"chain": "eth",
"address": "0xf939e0a03fb07f59a73314e73794be0e57ac1b4e",
"symbol": "crvusd",
"name": "crvUSD",
"holderConcentration": {
"top10": {
"index": 0.0012,
"topSharePct": 0.12,
"rawTopSharePct": 77.82,
"source": "holders",
"riskBand": "low"
},
"top30": null,
"top30NullReason": "organic_cohort_not_larger_than_top10",
"holderCount": 94962
}
}
],
"meta": {
"generatedAt": "2026-10-06T17:10:23.611640Z",
"stale": false,
"page": 1,
"pageSize": 50,
"total": 2,
"totalPages": 1,
"totalUnfiltered": 2,
"byRiskBand": {
"low": 1,
"medium": 1,
"high": 0,
"extreme": 0
},
"riskBandThresholds": {
"low": [
0,
0.2
],
"medium": [
0.2,
0.4
],
"high": [
0.4,
0.6
],
"extreme": [
0.6,
1
]
}
}
}{
"message": "Unauthorized"
}{
"x402Version": 1,
"error": "payment required"
}Authorizations
Query Parameters
Filter by chain slug. Holder data covers eth, arb, base, opt, pol and bsc; other supported chains return an empty list, unrecognized values return 400.
Comma-separated token addresses to return (max 50). EVM addresses match in any casing; Solana addresses are case-sensitive. Without chain, every chain the address appears on is returned. Returns 400 for a malformed address or more than 50 entries.
Comma-separated tickers (max 50), exact case-insensitive match. Returns every deployment with that ticker, including unrelated tokens that share it; check meta.symbolCollisions and use address to pin a specific token. Combined with address, a row must match both (AND).
Filter by the top-10 HCI risk band. Case-sensitive lowercase. Returns 400 for unrecognized or incorrectly-cased values. meta.byRiskBand counts are computed after the chain / minTop10Index filters and before this one.
low, medium, high, extreme Minimum top-10 holder concentration index (0–1). E.g. 0.4 returns high and extreme tokens only. Returns 400 for non-numeric or out-of-range values.
0 <= x <= 1Field to sort by. Defaults to top10Index.
top10Index, top30Index, symbol, chain, holderCount Sort direction. Defaults to desc.
asc, desc 1-indexed page number. Defaults to 1. Values below 1 are treated as 1.
x >= 1Number of results per page. Defaults to 50, max 500. Clamped to 1–500.
1 <= x <= 500