Overview
Exchanges, trading desks, and portfolio managers need structured intelligence on listed assets. Shoal’s entity model lets you map tokens and projects to organizations and monitor them for regulatory, security, and development events — all through a single API.
You’ll need an API key to follow along. See the Quickstart guide to create one.
Endpoints Used
| Endpoint | Purpose | Reference |
|---|
/v1/organizations/byOrganizationName | Map token names to org IDs | Docs |
/v1/brief/batch | Monitor up to 25 assets in one call | Docs |
/v1/radar/byCategory | Regulatory & security events | Docs |
/v1/signal/byOrganizationId | Per-asset signal feed | Docs |
Workflow
Step 1: Map Your Portfolio
Resolve each asset to a Shoal org ID via name lookup and store the mapping.
curl "https://api.shoal.xyz/v1/organizations/byOrganizationName?name=Ethereum" \
-H "Authorization: Bearer YOUR_API_KEY"
import os, requests
API_KEY = os.environ.get("SHOAL_API_KEY", "YOUR_API_KEY")
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Map exchange-listed assets to Shoal org IDs
assets = ["Ethereum", "Solana", "Chainlink", "Aave", "Uniswap", "Arbitrum"]
asset_map = {} # asset_name -> org_id
for asset in assets:
r = requests.get(
"https://api.shoal.xyz/v1/organizations/byOrganizationName",
headers=HEADERS,
params={"name": asset},
)
data = r.json()["data"]
if data:
asset_map[asset] = str(data[0]["id"])
print(f"{asset} -> ID {data[0]['id']}")
else:
print(f"{asset} -> not found")
print(f"Mapped {len(asset_map)}/{len(assets)} assets")
const API_KEY = process.env.SHOAL_API_KEY || 'YOUR_API_KEY';
const HEADERS = { Authorization: `Bearer ${API_KEY}` };
const assets = ['Ethereum', 'Solana', 'Chainlink', 'Aave', 'Uniswap', 'Arbitrum'];
const assetMap = {};
for (const asset of assets) {
const res = await fetch(
`https://api.shoal.xyz/v1/organizations/byOrganizationName?name=${encodeURIComponent(asset)}`,
{ headers: HEADERS }
);
const { data } = await res.json();
if (data.length > 0) {
assetMap[asset] = data[0].id;
console.log(`${asset} -> ID ${data[0].id}`);
} else {
console.log(`${asset} -> not found`);
}
}
console.log(`Mapped ${Object.keys(assetMap).length}/${assets.length} assets`);
Step 2: Batch-Poll for Updates
Use /v1/brief/batch with all org IDs and since for efficient single-call monitoring.
# Use the IDs returned from Step 1
SINCE=$(date -u -v-24H +"%Y-%m-%dT%H:%M:%SZ") # macOS
# SINCE=$(date -u -d '24 hours ago' +"%Y-%m-%dT%H:%M:%SZ") # Linux
curl "https://api.shoal.xyz/v1/brief/batch?ids=YOUR_ORG_IDS&since=$SINCE&compact=true" \
-H "Authorization: Bearer YOUR_API_KEY"
import time
from datetime import datetime, timedelta, timezone
ids = ",".join(asset_map.values())
cursor = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
while True:
r = requests.get(
"https://api.shoal.xyz/v1/brief/batch",
headers=HEADERS,
params={"ids": ids, "since": cursor, "compact": "true"},
)
data = r.json()
for org in data["organizations"]:
total = org["counts"]["radar"] + org["counts"]["signal"]
if total > 0:
print(f"{org['label']}: {org['counts']['radar']} radar, {org['counts']['signal']} signals")
for event in org.get("radar", []):
print(f" [{event['eventCategory']}] {event['title']}")
print(f"Credits used: {data['creditCost']}")
time.sleep(900) # Poll every 15 minutes
const ids = Object.values(assetMap).join(',');
let cursor = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
async function pollAssets() {
const res = await fetch(
`https://api.shoal.xyz/v1/brief/batch?ids=${ids}&since=${cursor}&compact=true`,
{ headers: HEADERS }
);
const data = await res.json();
for (const org of data.organizations) {
const total = org.counts.radar + org.counts.signal;
if (total > 0) {
console.log(`${org.label}: ${org.counts.radar} radar, ${org.counts.signal} signals`);
for (const event of org.radar || []) {
console.log(` [${event.eventCategory}] ${event.title}`);
}
}
}
console.log(`Credits used: ${data.creditCost}`);
}
setInterval(pollAssets, 15 * 60 * 1000); // Poll every 15 minutes
Step 3: Filter by Risk Category
Use /v1/radar/byCategory for regulation_legal and security_incident to surface compliance-relevant events.
SINCE=$(date -u -v-24H +"%Y-%m-%dT%H:%M:%SZ") # macOS
# SINCE=$(date -u -d '24 hours ago' +"%Y-%m-%dT%H:%M:%SZ") # Linux
# Regulatory events
curl "https://api.shoal.xyz/v1/radar/byCategory?category=regulation_legal&since=$SINCE" \
-H "Authorization: Bearer YOUR_API_KEY"
# Security incidents
curl "https://api.shoal.xyz/v1/radar/byCategory?category=security_incident&since=$SINCE" \
-H "Authorization: Bearer YOUR_API_KEY"
RISK_CATEGORIES = ["regulation_legal", "security_incident"]
since = (datetime.now(timezone.utc) - timedelta(hours=24)).isoformat()
portfolio_ids = set(asset_map.values())
for category in RISK_CATEGORIES:
r = requests.get(
"https://api.shoal.xyz/v1/radar/byCategory",
headers=HEADERS,
params={"category": category, "since": since},
)
events = r.json()["data"]
# Filter to only portfolio assets
relevant = [e for e in events if str(e.get("organizationId")) in portfolio_ids]
if relevant:
print(f"\n{category.upper()} ({len(relevant)} events):")
for event in relevant:
print(f" {event['title']} (significance: {event['signal']})")
const RISK_CATEGORIES = ['regulation_legal', 'security_incident'];
const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
const portfolioIds = new Set(Object.values(assetMap).map(String));
for (const category of RISK_CATEGORIES) {
const res = await fetch(
`https://api.shoal.xyz/v1/radar/byCategory?category=${category}&since=${since}`,
{ headers: HEADERS }
);
const { data } = await res.json();
const relevant = data.filter(e => portfolioIds.has(String(e.organizationId)));
if (relevant.length > 0) {
console.log(`\n${category.toUpperCase()} (${relevant.length} events):`);
for (const event of relevant) {
console.log(` ${event.title} (significance: ${event.signal})`);
}
}
}
Example Output
The /v1/brief/batch response groups events by organization. See the full schema for all available fields.
{
"organizations": [
{
"id": 15,
"label": "Solana",
"counts": { "radar": 1, "signal": 3 },
"radar": [
{
"eventCategory": "regulation_legal",
"title": "SEC delays decision on Solana ETF application",
"signal": 8,
"latestPostTimestamp": "2026-03-09T11:30:00Z"
}
]
},
{
"id": 27,
"label": "Aave",
"counts": { "radar": 0, "signal": 1 },
"radar": []
}
],
"creditCost": 6
}
Tips
The since parameter is required on batch and polling endpoints. Always pass it to avoid fetching stale data and to minimize credit usage.
- Use
compact=true on batch calls to reduce payload size and stay under your credit budget.
- Poll regulatory categories (
regulation_legal, security_incident) more frequently than general radar — these events often require immediate action.
- Use
/v1/organizations/:id/signal-history to detect unusual activity on a specific asset before it hits mainstream news.
- The batch endpoint supports up to 25 org IDs per call — group assets by priority tier if you have more.