SDK
Official SDKs for the Chainara Blockchain Threat Intelligence API. Both SDKs cover the full API v2 surface with typed models, automatic retry with exponential backoff, and rate-limit-aware error handling.
Source: github.com/exfil-chris/chainara-sdk
Installation
- TypeScript
- Python
npm install @chainara/sdk
Requires Node.js 18+. Also works in browser environments.
pip install git+https://github.com/exfil-chris/chainara-sdk.git#subdirectory=python
Requires Python 3.9+. Both sync and async clients are included.
Initialization
Pass your API key and tenant base URL. The API key can also be read from the CHAINARA_API_KEY environment variable.
- TypeScript
- Python
import { ChainaraClient } from "@chainara/sdk";
const client = new ChainaraClient({
apiKey: "ek_your_api_key",
baseUrl: "https://ripple-platform.chainara.io/api/v2",
timeout: 30000, // ms, default 30s
maxRetries: 3, // default 3
});
from chainara import ChainaraClient
client = ChainaraClient(
api_key="ek_your_api_key",
base_url="https://ripple-platform.chainara.io/api/v2",
timeout=30, # seconds, default 30
max_retries=3, # default 3
)
# Always close the client when done (or use as a context manager)
client.close()
An async client is also available:
from chainara import AsyncChainaraClient
async with AsyncChainaraClient(
api_key="ek_your_api_key",
base_url="https://ripple-platform.chainara.io/api/v2",
) as client:
intel = await client.wallets.get_intelligence(1, "rAddress...")
Replace ripple-platform with your tenant subdomain. The default Chainara instance uses platform.chainara.io.
Resources
Wallets
Fetch full threat intelligence or a fast risk score for any blockchain address.
- TypeScript
- Python
// Full threat intelligence (risk score, signals, associated domains, fund flow)
const intel = await client.wallets.getIntelligence(1, "rAddress...");
console.log(intel.riskScore, intel.riskLevel);
// Risk score only (faster, lower latency)
const score = await client.wallets.getRiskScore(1, "rAddress...");
# Full threat intelligence
intel = client.wallets.get_intelligence(1, "rAddress...")
print(intel.risk_score, intel.risk_level)
# Risk score only (faster)
score = client.wallets.get_risk_score(1, "rAddress...")
The first argument is the blockchain ID. Use 1 for XRPL, 2 for Stellar. See Blockchain IDs for the full list.
Fraud Reports
Submit reports and query the fraud intelligence database.
- TypeScript
- Python
// List reports (paginated)
const reports = await client.fraudReports.list({ page: 1, limit: 20 });
// Submit a new report
const report = await client.fraudReports.create({
walletAddress: "rAddress...",
reportType: "scam_website",
description: "Fake XRP giveaway site impersonating Ripple",
});
# List reports
reports = client.fraud_reports.list(page=1, limit=20)
# Submit a report
report = client.fraud_reports.create(
wallet_address="rAddress...",
report_type="scam_website",
description="Fake XRP giveaway site impersonating Ripple",
evidence="https://scam-domain.com",
amount_lost="500.00",
)
Blacklist
Query confirmed-malicious addresses.
- TypeScript
- Python
const entries = await client.blacklist.list({ page: 1, limit: 50 });
entries = client.blacklist.list(page=1, limit=50)
Threat Feed
Pull a full threat intelligence snapshot or an incremental update. Enterprise plan required.
- TypeScript
- Python
// Full snapshot
const snapshot = await client.feed.getSnapshot();
// Incremental: only indicators added or updated since last sync
const delta = await client.feed.getSnapshot({
since: "2026-03-01T00:00:00Z",
});
// Filtered: blacklisted wallets on XRPL only, high confidence
const filtered = await client.feed.getSnapshot({
types: "wallet",
severityTier: "blacklisted",
minConfidence: 80,
blockchain: "xrpl",
limit: 1000,
});
// Paginate with cursor
let cursor: string | undefined;
do {
const page = await client.feed.getSnapshot({ cursor, limit: 1000 });
for (const indicator of page.indicators) {
process(indicator);
}
cursor = page.nextCursor;
} while (cursor);
# Full snapshot
snapshot = client.feed.get_snapshot()
# Incremental sync
delta = client.feed.get_snapshot(since="2026-03-01T00:00:00Z")
# Filtered
filtered = client.feed.get_snapshot(
types="wallet",
severity_tier="blacklisted",
min_confidence=80,
blockchain="xrpl",
limit=1000,
)
# Paginate with cursor
cursor = None
while True:
page = client.feed.get_snapshot(cursor=cursor, limit=1000)
for indicator in page.indicators:
process(indicator)
cursor = getattr(page, "next_cursor", None)
if not cursor:
break
Store generated_at from each response and pass it as since on the next call to receive only new or updated indicators.
Webhooks
Register endpoints to receive real-time threat indicator events. Enterprise plan required.
- TypeScript
- Python
// Create a subscription
const sub = await client.webhooks.create({
url: "https://myapp.example.com/hooks/chainara",
eventTypes: ["indicator_added", "indicator_updated", "indicator_removed"],
description: "Production SIEM hook",
});
// Save sub.signingSecret. It is returned once and cannot be retrieved again.
// List subscriptions
const subs = await client.webhooks.list();
// Send a test delivery
await client.webhooks.test(sub.id);
// Rotate signing secret
const rotated = await client.webhooks.rotateSecret(sub.id);
// View delivery history
const history = await client.webhooks.listDeliveries(sub.id, 50);
// Update or delete
await client.webhooks.update(sub.id, { url: "https://new-url.example.com/hook" });
await client.webhooks.delete(sub.id);
# Create a subscription
sub = client.webhooks.create(
url="https://myapp.example.com/hooks/chainara",
event_types=["indicator_added", "indicator_updated", "indicator_removed"],
description="Production SIEM hook",
)
# Save sub.signing_secret. Returned once, cannot be retrieved again.
# List, test, rotate, update, delete
subs = client.webhooks.list()
client.webhooks.test(sub.id)
rotated = client.webhooks.rotate_secret(sub.id)
history = client.webhooks.get_deliveries(sub.id)
client.webhooks.update(sub.id, url="https://new-url.example.com/hook")
client.webhooks.delete(sub.id)
Ingest
Bulk-submit threat intelligence from your own sources. All ingest submissions are tagged pending and capped at risk score 65 until an analyst verifies them.
- TypeScript
- Python
// Bulk fraud reports
await client.ingest.reports([
{ walletAddress: "rAddr1...", reportType: "scam_website", description: "..." },
{ walletAddress: "rAddr2...", reportType: "phishing", description: "..." },
]);
// Bulk suspicious wallets
await client.ingest.wallets([
{ address: "rAddr1...", blockchainId: 1, reason: "Giveaway scam", confidence: 0.9 },
]);
// Bulk malicious domains
await client.ingest.domains([
{ domain: "evil-xrp.com", reason: "XRP phishing site" },
]);
// Async batch job (up to 10,000 items)
const job = await client.ingest.batch("wallets", items, "my-feed");
const status = await client.ingest.getBatchJob(job.jobId);
# Bulk fraud reports
client.ingest.reports([
{"wallet_address": "rAddr1...", "report_type": "scam_website", "description": "..."},
{"wallet_address": "rAddr2...", "report_type": "phishing", "description": "..."},
])
# Bulk suspicious wallets
client.ingest.wallets([
{"address": "rAddr1...", "blockchain_id": 1, "reason": "Giveaway scam", "confidence": 0.9},
])
# Bulk malicious domains
client.ingest.domains([
{"domain": "evil-xrp.com", "reason": "XRP phishing site"},
])
# Async batch job (up to 10,000 items)
job = client.ingest.batch(type="wallets", items=items, source_label="my-feed")
status = client.ingest.get_batch_job(job_id=job.job_id)
Health
- TypeScript
- Python
const status = await client.health.check();
status = client.health.check()
Webhook Signature Verification
Verify every incoming delivery before processing it. Both SDKs use constant-time comparison to prevent timing attacks.
- TypeScript
- Python
import express from "express";
import { ChainaraClient } from "@chainara/sdk";
const app = express();
app.use("/hooks/chainara", express.raw({ type: "application/json" }));
app.post("/hooks/chainara", (req, res) => {
const isValid = ChainaraClient.verifyWebhookSignature(
req.body, // raw Buffer
req.headers["x-chainara-signature"] as string,
process.env.WEBHOOK_SECRET!,
);
if (!isValid) return res.status(401).send("Invalid signature");
const event = JSON.parse(req.body.toString());
console.log("Event:", event.event, event.indicators.length, "indicators");
res.sendStatus(200);
});
Note: use express.raw() (not express.json()) to preserve the raw bytes needed for HMAC verification.
from flask import Flask, request, abort
from chainara import verify_webhook_signature
app = Flask(__name__)
@app.post("/hooks/chainara")
def chainara_webhook():
is_valid = verify_webhook_signature(
payload=request.get_data(), # raw bytes
signature=request.headers.get("X-Chainara-Signature", ""),
secret=os.environ["WEBHOOK_SECRET"],
)
if not is_valid:
abort(401)
event = request.get_json(force=True)
print(f"Event: {event['event']}, {len(event['indicators'])} indicators")
return "", 200
Error Handling
All SDK errors extend ChainaraError. Catch specific subclasses for targeted handling.
- TypeScript
- Python
import {
ChainaraError,
ChainaraConnectionError,
AuthenticationError,
PermissionDeniedError,
NotFoundError,
RateLimitError,
} from "@chainara/sdk";
try {
const intel = await client.wallets.getIntelligence(1, "rAddress...");
} catch (error) {
if (error instanceof AuthenticationError) {
// Invalid or missing API key
console.error("Check your API key");
} else if (error instanceof PermissionDeniedError) {
// Valid key but endpoint requires a higher plan
console.error("Endpoint requires Enterprise plan");
} else if (error instanceof NotFoundError) {
// Address not in database. Treat as unknown, not safe.
console.warn("Address not found");
} else if (error instanceof RateLimitError) {
// Respect Retry-After
const wait = error.retryAfter ?? 60;
console.warn(`Rate limited. Retry in ${wait}s`);
await new Promise(r => setTimeout(r, wait * 1000));
} else if (error instanceof ChainaraConnectionError) {
console.error("Could not reach the API");
} else if (error instanceof ChainaraError) {
console.error(`SDK error: ${error.message}`);
}
}
from chainara import (
ChainaraError,
ChainaraConnectionError,
AuthenticationError,
PermissionDeniedError,
NotFoundError,
RateLimitError,
)
import time
try:
intel = client.wallets.get_intelligence(1, "rAddress...")
except AuthenticationError:
print("Check your API key")
except PermissionDeniedError:
print("Endpoint requires Enterprise plan")
except NotFoundError:
print("Address not found. Treat as unknown, not safe.")
except RateLimitError as e:
wait = e.retry_after or 60
print(f"Rate limited. Retry in {wait}s")
time.sleep(wait)
except ChainaraConnectionError as e:
print(f"Could not reach the API: {e}")
except ChainaraError as e:
print(f"SDK error: {e}")
Exception hierarchy
ChainaraError (base, catches everything)
├── ChainaraConnectionError (server unreachable)
├── ChainaraValidationError (client-side validation failed)
├── ChainaraTimeoutError (request timed out)
└── ChainaraAPIError (HTTP error response)
├── AuthenticationError 401
├── PermissionDeniedError 403
├── NotFoundError 404
├── BadRequestError 400
├── ConflictError 409
├── RateLimitError 429 (exposes retry_after / retryAfter)
└── InternalServerError 5xx
Environment Variables
- TypeScript
- Python
export CHAINARA_API_KEY="ek_..."
export CHAINARA_API_KEY="ek_..."
When set, the SDK reads the key automatically and you can omit apiKey from the constructor.
SIEM and Webhook Integration Examples
The SDK repository includes production-ready integration examples under integrations/:
| Example | Description |
|---|---|
webhooks/webhook_server.py | Flask receiver: verifies signatures, dispatches events by type |
webhooks/webhook-server.ts | Express receiver: same flow in TypeScript |
webhooks/webhook_client.py | Full webhook lifecycle: create, test, rotate secret, delete |
siem/elastic_push.py | Bulk-index threat feed into Elasticsearch with incremental sync |
siem/splunk_push.py | Push threat feed to Splunk HEC in batches |
siem/generic_siem.py | Reusable Fetch → Transform → Push scaffold for any SIEM |
All SIEM scripts support incremental sync: the first run fetches the full snapshot; subsequent runs fetch only indicators updated since the last successful run using a state file.