Skip to main content

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.
Exchange asset intelligence integration showing Shoal events flowing into exchange monitoring workflows
You’ll need an API key to follow along. See the Quickstart guide to create one.

Endpoints Used

EndpointPurposeReference
/v1/organizations/byOrganizationNameMap token names to org IDsDocs
/v1/brief/batchMonitor up to 25 assets in one callDocs
/v1/radar/byCategoryRegulatory & security eventsDocs
/v1/signal/byOrganizationIdPer-asset signal feedDocs

Workflow

Step 1: Map Your Portfolio

Resolve each asset to a Shoal org ID via name lookup and store the mapping.
cURL
curl "https://api.shoal.xyz/v1/organizations/byOrganizationName?name=Ethereum" \
  -H "Authorization: Bearer YOUR_API_KEY"
Python
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")
JavaScript
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.
cURL
# 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"
Python
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
JavaScript
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.
cURL
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"
Python
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']})")
JavaScript
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.