# Arkham Intel API v1.1.0 > Production-grade blockchain intelligence API. Entity attribution, address labels, transfer tracking, and on-chain analytics across 20+ chains. - Base URL: `https://api.arkm.com` - API Docs: https://arkm.com/api/docs - OpenAPI Spec: https://arkm.com/openapi.json - Schemas: https://arkm.com/llms/schemas/ - Endpoint Docs: https://arkm.com/llms/-.md — slug is the HTTP method + path with `/` replaced by `-` and `{braces}` removed (e.g. `POST /intelligence/address/batch/all` → https://arkm.com/llms/post-intelligence-address-batch-all.md) This file contains the full API guide, an endpoint reference table, and code examples. For detailed per-endpoint documentation (parameters, schemas, cURL examples), fetch the individual markdown files linked in the Endpoint Reference table below. --- # API Guide ## Overview **User guide for integrating the Arkham Intel API** Production-grade blockchain intelligence for your products, pipelines, and workflows. **Ready to start?** [Get API Access →](/getting-started/access) or jump to the [API Reference](#api-reference) ## Introduction This guide complements the [API Reference](#api-reference) with practical guidance on using the API effectively. It covers common integration topics including access and onboarding, authentication, rate limits, pagination, and the Arkham data model (addresses, entities, labels, and tags). Designed for both private- and public-sector users, this guide focuses on implementation guidance, conceptual clarity, and frequently asked questions. For endpoint specifications, request/response schemas, and code examples, see the [API Reference](#api-reference). This guide is continuously updated as the API expands and new best practices emerge. ## What This Guide Covers - **Access and onboarding** - How to get API access - **API keys and authentication** - Managing your credentials - **Rate limits** - Understanding and working within limits - **Credit pricing** - How API credits are calculated and billed per endpoint - **Pagination patterns** - Getting all your data - **Data model** - Addresses, entities, labels, and tags - **Best practices** - Security and operational recommendations ## Core Principles Entity-First Built around real-world actors, not isolated addresses Confidence-Scored Attribution is probabilistic, not binary claims Living Intelligence Labels evolve as new signals emerge ## When to Use This Guide vs API Docs Use This Guide When... - Setting up an integration - Clarifying expected behavior - Understanding the data model - Learning best practices Use API Docs When... - You need endpoint specifications - You need request/response schemas - You need code examples - You need parameter details ## Quick Links - **API Reference:** [API Reference](#api-reference) - **Request API Access:** [arkm.com/api](https://arkm.com/api) - **Arkham Platform:** [arkm.com](https://arkm.com) - **LLM-Friendly Docs:** [arkm.com/llms.txt](https://arkm.com/llms.txt) — machine-readable API docs for AI assistants - **Support:** [api@arkm.com](mailto:api@arkm.com) --- _Last updated: January 2026_ ## Design Principles A few core principles shape how the Arkham API is designed and how Arkham intelligence should be used. ## Entity-First, Not Address-First Arkham is built around **entities** (real-world actors), not isolated wallet addresses. Addresses are often disposable; entities persist. The API is optimized to help you understand activity at the **actor level** and then drill down into underlying addresses when needed. ## Explicit Confidence, Not Implied Certainty Attribution is treated as a **confidence-scored** problem, not a binary claim. Arkham distinguishes between higher-confidence verified labels and lower-confidence predictive attribution, so consumers can make informed decisions and apply appropriate corroboration. ## Labels as Living Intelligence Labels and entity composition evolve as new signals emerge and attribution improves. Arkham intelligence is continuously updated, and the API is designed to support workflows that account for **ongoing enrichment and corrections** over time. ## Coverage Without Hiding Uncertainty Arkham aims for broad coverage while being transparent about what is known versus inferred. Where attribution is probabilistic, the system surfaces that uncertainty rather than hiding it, enabling users to balance **coverage, confidence, and risk** according to their use case. --- See the [Data Model](/concepts/data-model) section for more details on how Arkham attribution coverage should be interpreted, including how to interpret labels vs. prediction labels, how Arkham represents confidence in attribution, and how to incorporate those signals into production workflows. ## Who This API Is For The Arkham API is built for teams that need **production-grade blockchain intelligence** inside their own products, pipelines, and investigative workflows. It provides direct programmatic access to the same intelligence layer that powers the Arkham platform, including entity attribution, labels, tags, transfers, wallet clusters, and entity predictions. At the core of the API is **ULTRA**, Arkham's proprietary address-matching engine. ULTRA links blockchain addresses to real-world entities so sophisticated users can customize data flows, automate monitoring, and integrate Arkham intelligence into internal systems. ## What You Can Query Through the API, you can access: - **Labels and tags** for addresses and entities - **Transaction activity** (transfers, counterparties, historical flows) - **Historical balance data** for addresses and entities ::: tip Scale & Coverage Arkham powers the world's largest blockchain intelligence database: - **3B+ tags** - **94% of on-chain volume attributed** - **$1T+ in labeled asset flows tracked** ::: ## Private Sector Use Cases The API is designed for organizations that need intelligence for trading, operations, and risk: - **Trading firms** (market makers, hedge funds, asset managers) - **Exchanges**, **DeFi protocols**, **payment providers**, and **crypto infrastructure** - **Risk management** across portfolios, venues, and counterparties - **Investment strategy** and research (flows, entities, market behavior) - **Real-time market intelligence** and monitoring (alerts, key wallet tracking) - **Compliance and fraud** workflows (AML checks, suspicious activity detection, exposure mapping) ## Public Sector Use Cases The API is designed for mission-driven environments where attribution and traceability matter: - **Government investigators and analysts**, often in SI- or CSP-hosted environments - **Systems integrators** embedding attribution into investigative workflows - **SaaS and data platforms** integrating crypto intelligence into their products - **Financial crime, sanctions, cybercrime, and fraud** analytics teams ## Getting Access To use the Arkham API, you'll need: 1. **An Arkham account** 2. **An API plan** 3. **An API key** generated from your Arkham account ## Step 1: Create an Arkham Account 1. Go to [arkm.com](https://arkm.com) 2. Click **Sign Up** in the top-right corner 3. Complete registration and log in ## Step 2: Get an API Plan Arkham API plans are **custom-built** based on your organization's needs, use cases, and expected usage. Subscriptions are usage-based (credits per month) and **start at $100**. Bespoke and pay-as-you-go (PAYG) plans are available. Pay-per-request (agentic payment) details [here](/add-ons/x402-agent-payments). **To request access:** 1. Go to [arkm.com/api](https://arkm.com/api) 2. Scroll to the bottom and complete the short form 3. Submit your details Our team reviews inbound requests daily and will follow up with either: - A **tailored proposal** (if you already know your use case and expected usage), or - **Trial access** so you can evaluate Arkham's data and endpoints before committing ## Step 3: Generate an API Key Once you have an API plan or trial access, you can generate and manage keys directly in the API Dashboard or from your account settings: 1. Go to [arkm.com/api-dashboard?tab=api-keys](https://arkm.com/api-dashboard?tab=api-keys) (while signed in) 2. Open the **API Keys** tab if it is not already selected 3. Click **CREATE API KEY** to create a new key, or manage existing keys there You can also create and manage keys from [arkm.com/settings](https://arkm.com/settings) in the **API / Developer** section. ::: info Key Limits By default, accounts with API access can create **up to 5 API keys**. On custom plans, this limit can be increased as needed. ::: You can manage your keys from the API Dashboard or settings at any time, including **naming**, **deleting**, and **rotating** keys. ## API Keys & Authentication In the Arkham User Interface: 1. Go to **Settings** → scroll to **API Keys** 2. Click **Create key** 3. **Name** the key (e.g., `prod-backend`, `staging`, `research-laptop`) 4. Copy it **once** and store it securely (password manager / secret manager) ::: warning Store Securely Your API key is only shown once. Store it immediately in a secure location. ::: ## Using the API Key All API requests **must** include a valid `API-Key` header. Requests without it will be rejected as unauthorized. **Header format:** ``` API-Key: ``` **Example (curl):** ```bash curl -H "API-Key: " "https://api.arkm.com/..." ``` ## Multiple Keys Yes, you can have multiple keys. **Best practice:** - Separate keys per **environment** (prod / staging / dev) - Separate keys per **service** (backend / analytics job / partner integration) This makes rotation and incident containment much easier. ## Rotating or Revoking Keys 1. Create a **new** key 2. Deploy it and confirm requests succeed 3. Revoke the **old** key in the dashboard ## REST vs WebSocket The Arkham API offers two kinds of endpoints: REST (HTTPS) Lookups, history, analytics, enrichment WebSocket (streaming) Real-time updates, live feeds, events ### When to Use REST - Historical data - Deterministic queries - Batch jobs and enrichment - Easy retry logic ### When to Use WebSocket - Low-latency updates - Continuous streaming without polling - Event-driven systems ::: tip WebSockets are for "what's changing right now"; REST is for "give me the full state / history." ::: ## Addresses, Entities, Labels Address A single on-chain address (wallet or contract) `0x1234...abcd` Entity A set of addresses attributed to the same real-world actor Binance, BlackRock, Uniswap **Entities are the "who". Addresses are the "where".** ## Labels **Labels** are identity metadata Arkham attaches to an address or entity (e.g., "Binance", "BlackRock", "Chainlink"). Labels are designed to answer the most important question in blockchain analysis: > **Who is behind this activity?** Labels let users interpret transactions quickly without manually investigating every trail on-chain. ## Tags **Tags** are qualitative descriptors that summarize the **type** or **behavior** of an address/entity, whether it is labeled or not. **Examples:** - "Fund" - "BTC Whale" - "OFAC Sanctioned" - "Hacker" - "Banned by USDT" - "High Transacting" Tags are useful for filtering, discovery, and grouping similar actors. ## Label Types & Confidence Arkham provides two label types: ### 1. Arkham Verified Labels Verified labels represent Arkham's highest-confidence attribution. - **How to identify:** Displayed with a **blue badge** on the Arkham platform - **Confidence:** Very high (internally thresholded, typically **≥98%**) ::: tip Use verified labels when you need maximum attribution reliability. ::: ### 2. Entity Predictions Entity Prediction labels are programmatically generated to identify addresses likely connected to a known entity. - **How to identify:** Displayed with a **pink badge**, often with a question mark - **Confidence:** Lower than verified (internally thresholded, typically **≥80%**) ::: warning Prediction labels are valuable for expanding coverage and spotting relationships, but should be treated as **probabilistic attribution** rather than certainty. ::: ## Entity Composition Changes Entity composition can change as new attribution is discovered and additional addresses are linked. Arkham's attribution database is continuously updated: - New entities are added - Existing entities expand as new addresses are identified - In rare cases, attribution may be corrected For enterprise users, Arkham offers API access to monitor database updates in near real time. ## Private Labels Arkham doesn't have a label you know about? You can add **private labels** via both the UI and the API. Private labels are visible only to: - Your account/workspace - Anyone you explicitly share access with With private labels you can: - Create a new custom entity - Add addresses to an existing entity - Manage your private data via the [Labels page](https://arkm.com/labels) ::: info Privacy Arkham does not access or publish users' private labels. ::: ## How Arkham Labels Wallets Arkham labels wallets by combining **on-chain behavior** with **public off-chain signals**, running that through our proprietary attribution system (**ULTRA**), and only publishing labels when they meet strict confidence thresholds. ## The Process (High Level) ### 1. Data Ingestion (Signals) On-chain - Transfers and interactions - Counterparty networks - Contract behavior - Timing patterns Off-chain (public) - Public disclosures - Official wallet posts - Protocol docs - Exchange/protocol announcements - Public attribution sources ### 2. Attribution & Clustering (ULTRA) ULTRA applies a mix of: - **Address clustering** - linking related addresses - **Address modeling** - behavioral patterns - **ML-driven heuristics** - probabilistic attribution - **Intelligence augmentation** - enrichment from external public signals - **Internal consistency checks** - across multiple data sources ### 3. Confidence Gating Verified labels ≥98% Prediction labels ≥80% Prediction labels are explicitly marked as lower-confidence. ### 4. Human Verification Labels that are high visibility or high impact can be reviewed **case-by-case** by analysts before publication. ## Why This Matters Labeling isn't a single "source of truth" problem. It's a **signal fusion** problem: ULTRA combines multiple independent signals and only publishes attribution when the evidence crosses defined confidence thresholds. This approach provides: - **Accuracy** - High confidence thresholds reduce false positives - **Coverage** - ML-driven predictions expand coverage beyond manual labeling - **Transparency** - Confidence scoring lets you decide how to weight attribution - **Adaptability** - Continuous updates as new signals emerge ## Rate Limits Yes. Rate limits vary by plan and may also depend on the endpoint type (REST vs WebSocket). Arkham API endpoints generally fall into two categories: Standard Most endpoints, with small burst allowance **100 requests/second** Heavy Resource-intensive endpoints **1 request/second** Both limits can be **scaled up** under custom API plans. ### Heavy Endpoints The following endpoints are resource-intensive and subject to the lower **1 request/second** limit: | Endpoint | | --- | | `GET /transfers` | | `GET /transfers/unenriched` | | `GET /transfers/histogram` | | `GET /swaps` | | `GET /token/top` | | `GET /token/top_flow/{id}` | | `GET /token/top_flow/{chain}/{address}` | | `GET /token/volume/{id}` | | `GET /token/volume/{chain}/{address}` | | `GET /intelligence/search` | | `GET /counterparties/address/{address}` | | `GET /counterparties/entity/{entity}` | | `GET /flow/address/{address}` | | `GET /flow/entity/{entity}` | | `GET /hypercore/trades` | | `GET /hypercore/trades/aggregate` | | `POST /intelligence/address/batch` | | `POST /intelligence/address/batch/all` | | `POST /intelligence/address_enriched/batch` | | `POST /intelligence/address_enriched/batch/all` | The batch endpoints accept up to 1000 addresses per request, so a single call covers what would otherwise take many. All other endpoints fall under the **Standard** limit. ## Increasing Your Limits If you need higher limits for a specific endpoint or across your entire plan, contact [api@arkm.com](mailto:api@arkm.com). ## What Happens When You Hit Limits If you exceed your limit, requests are rejected with: - **HTTP 429** (Too Many Requests) - A "too many requests" error response ## Avoiding Rate Limits Best practices for staying within limits: 1. **Cache** responses when possible 2. **Paginate correctly** (don't repeatedly hit the first page) 3. Use **bulk endpoints** where available 4. Implement **retries with exponential backoff + jitter** on 429 ### Backoff Example (Python) ```python import time import random import requests def request_with_backoff(url, headers, max_retries=5): for attempt in range(max_retries): resp = requests.get(url, headers=headers) if resp.status_code == 429: # Get retry delay from header, or use exponential backoff retry_after = int(resp.headers.get('Retry-After', 2 ** attempt)) # Add jitter to prevent thundering herd jitter = random.uniform(0, 1) time.sleep(retry_after + jitter) continue return resp raise Exception("Max retries exceeded") ``` ::: tip Always add jitter to your backoff delays to prevent synchronized retry storms. ::: ## Intel Label Limits In addition to per-second rate limits, the API enforces **label limits** that cap the number of unique labeled addresses you can look up per billing period. A "label lookup" is a unique blockchain address for which Arkham returns proprietary intelligence data (entity labels, tags, etc.). Repeated lookups of the same address within a billing period count only once. Trial Free trial accounts **10,000 label lookups/period** Individual Paying individual plans **1,000,000 label lookups/period** Organization Paying org plans, per seat **10,000,000 label lookups/period/seat** These limits can be **increased** upon request. Contact [api@arkm.com](mailto:api@arkm.com). ### Tracking Your Label Usage Every API response from intelligence endpoints includes headers showing your current usage: | Header | Description | | --- | --- | | `X-Intel-Datapoints-Usage` | Label lookups consumed so far this period | | `X-Intel-Datapoints-Limit` | Your per-seat limit for this period | | `X-Intel-Datapoints-Remaining` | Label lookups remaining before the limit is reached | You can also check your usage programmatically: ``` GET /subscription/intel-usage ``` This authenticated endpoint returns your total count, per-seat limit, per-chain breakdown, and period start date. ### What Happens When You Reach Your Label Limit Once your label limit is reached, intelligence requests are rejected with: - **HTTP 429** (Too Many Requests) - Message: `"Intelligence label lookup limit reached for this billing period."` Other endpoints that don't return intel label data (price data, etc.) are **not affected** by label limits. ::: tip You can request an increase to your label limit at any time by contacting api@arkm.com or through the "Request More" button on your API Dashboard. ::: ## Credit Pricing Every API request consumes **credits** from your monthly allowance. The cost depends on the endpoint and, for some endpoints, the amount of data returned. Per-Call Fixed credit cost each time you call the endpoint **Credits = weight** Per-Row Credits scale with the number of results returned **Credits = weight × rows returned** ::: info Failed requests (4xx/5xx responses) are not charged. ::: ## Credit Allowances Every request draws credits from your account. The allowance depends on your plan: Individual Trial Free trial on an individual account **100,000 credits*** Organization Trial Free trial on an organization account **1,000,000 credits*** Paid Subscriptions Usage-based plans **No limit** \*Trial users can request a higher credit cap — contact [api@arkm.com](mailto:api@arkm.com). When a free trial reaches its cap, API access is paused until you upgrade. ## Endpoint Credit Pricing ### Intelligence | Endpoint | Credits | Type | | ----------------------------------------------- | ------- | -------- | | `/intelligence/address/{address}` | 1 | per call | | `/intelligence/address/{address}/all` | 2 | per call | | `/intelligence/address_enriched/{address}` | 2 | per call | | `/intelligence/address_enriched/{address}/all` | 4 | per call | | `/intelligence/entity/{entity}` | 1 | per call | | `/intelligence/entity/{entity}/summary` | 1 | per call | | `/intelligence/entity_predictions/{entity}` | 1 | per call | | `/intelligence/contract/{chain}/{address}` | 1 | per call | | `/intelligence/token/{id}` | 1 | per call | | `/intelligence/token/{chain}/{address}` | 1 | per call | | `/intelligence/addresses/updates` | 100 | per call | | `/intelligence/entities/updates` | 30 | per call | | `/intelligence/tags/updates` | 30 | per call | | `/intelligence/address_tags/updates` | 100 | per call | | `POST /intelligence/address/batch` | 250 | per call | | `POST /intelligence/address_enriched/batch` | 500 | per call | | `POST /intelligence/address/batch/all` | 500 | per call | | `POST /intelligence/address_enriched/batch/all` | 1000 | per call | | `/intelligence/search` | 30 | per call | | `/intelligence/entity_balance_changes` | 2 | per call | | `/intelligence/entity_types` | 1 | per call | ### Transfers & Swaps | Endpoint | Credits | Type | | ----------------------------- | ------- | -------- | | `/transfers` | 2 | per row | | `/transfers/unenriched` | 1 | per row | | `/swaps` | 2 | per row | | `/transfers/histogram/simple` | 2 | per call | | `/transfers/histogram` | 4 | per call | ::: warning Per-Row Billing These endpoints charge per result returned. Total credits are the number of rows returned multiplied by the endpoint's listed credit value. For example, 50 `/transfers` rows cost 50 × 2 = **100 credits**, while 50 `/transfers/unenriched` rows cost 50 × 1 = **50 credits**. Use the `limit` parameter to control costs. ::: ### WebSocket Two WebSocket versions are available — **v2 is recommended**, v1 is deprecated. #### v2 (recommended) Streams are billed only for the data they deliver: creating, listing, and deleting a stream is free, and delivered transfers are charged per row. | Endpoint | Credits | Type | | ---------------------------- | ------- | -------- | | `GET /ws/v2/streams` | 0 | free | | `POST /ws/v2/streams` | 0 | free | | `DELETE /ws/v2/streams/{id}` | 0 | free | | `/ws/v2/transfers` | 2 | per row | #### v1 (deprecated) ::: warning Deprecated The v1 WebSocket endpoints are deprecated. Use the **v2 stream endpoints** above — v2 has no session-creation fee and bills only for delivered transfers. ::: | Endpoint | Credits | Type | | -------------------------- | ------- | -------- | | `POST /ws/sessions` | 500 | per call | | `GET /ws/sessions` | 1 | per call | | `GET /ws/sessions/{id}` | 1 | per call | | `DELETE /ws/sessions/{id}` | 1 | per call | | `/ws/transfers` | 2 | per row | | `/ws/session-info` | 1 | per call | | `/ws/active_connections` | 1 | per call | ### Token | Endpoint | Credits | Type | | ---------------------------------------- | ------- | -------- | | `/token/top` | 10 | per call | | `/token/trending` | 1 | per call | | `/token/trending/{id}` | 1 | per call | | `/token/holders/{id}` | 30 | per call | | `/token/holders/{chain}/{address}` | 30 | per call | | `/token/top_flow/{id}` | 10 | per call | | `/token/top_flow/{chain}/{address}` | 10 | per call | | `/token/volume/{id}` | 3 | per call | | `/token/volume/{chain}/{address}` | 3 | per call | | `/token/balance/{id}` | 1 | per call | | `/token/balance/{chain}/{address}` | 1 | per call | | `/token/arkham_exchange_tokens` | 1 | per call | | `/token/price/history/{id}` | 1 | per call | | `/token/price/history/{chain}/{address}` | 1 | per call | | `/token/market/{id}` | 1 | per call | | `/token/price_change/{id}` | 1 | per call | | `/token/addresses/{id}` | 1 | per call | ### Transaction | Endpoint | Credits | Type | | ---------------------- | ------- | -------- | | `/tx/{hash}` | 2 | per call | | `/transfers/tx/{hash}` | 2 | per call | ### History | Endpoint | Credits | Type | | ---------------------------- | ------- | -------- | | `/history/entity/{entity}` | 2 | per call | | `/history/address/{address}` | 1 | per call | ### Portfolio | Endpoint | Credits | Type | | ----------------------------------------- | ------- | -------- | | `/portfolio/entity/{entity}` | 2 | per call | | `/portfolio/address/{address}` | 1 | per call | | `/portfolio/timeSeries/entity/{entity}` | 3 | per call | | `/portfolio/timeSeries/address/{address}` | 2 | per call | ### Balances & Loans | Endpoint | Credits | Type | | -------------------------------------------------- | ------- | -------- | | `/balances/address/{address}` | 1 | per call | | `/balances/entity/{entity}` | 5 | per call | | `/balances/solana/subaccounts/address/{addresses}` | 1 | per call | | `/balances/solana/subaccounts/entity/{entities}` | 3 | per call | | `/loans/address/{address}` | 1 | per call | | `/loans/entity/{entity}` | 3 | per call | ### Counterparties & Flow | Endpoint | Credits | Type | | ----------------------------------- | ------- | -------- | | `/counterparties/address/{address}` | 50 | per call | | `/counterparties/entity/{entity}` | 50 | per call | | `/flow/address/{address}` | 2 | per call | | `/flow/entity/{entity}` | 3 | per call | | `/volume/address/{address}` | 1 | per call | | `/volume/entity/{entity}` | 2 | per call | ### Tags, Clusters & Labels | Endpoint | Credits | Type | | ------------------------------------------------- | ------- | -------- | | `/tag/{id}/params` | 5 | per call | | `/tag/{id}/summary` | 1 | per call | | `/cluster/{id}/summary` | 1 | per call | | `GET /user/labels` | 0 | per call | | `POST /user/labels` | 0 | per call | | `DELETE /user/labels/{chainType}/{address}` | 0 | per call | | `GET /user/entities` | 0 | per call | | `POST /user/entities` | 0 | per call | | `GET /user/entities/{id}` | 0 | per call | | `PUT /user/entities/{id}` | 0 | per call | | `DELETE /user/entities/{id}` | 0 | per call | | `PUT /user/entities/only_add/{id}` | 0 | per call | ### Alerts | Endpoint | Credits | Type | | -------------------------- | ------- | -------- | | `GET /user/alerts` | 1 | per call | | `POST /user/alerts` | 1 | per call | | `GET /user/alerts/{id}` | 1 | per call | | `PUT /user/alerts/{id}` | 1 | per call | | `DELETE /user/alerts/{id}` | 1 | per call | ### Market Data & Networks | Endpoint | Credits | Type | | --------------------------- | ------- | -------- | | `/marketdata/altcoin_index` | 1 | per call | | `/networks/history/{chain}` | 1 | per call | | `/chains` | 0 | free | | `/networks/status` | 0 | free | | `/arkm/circulating` | 0 | free | ### Account & Usage | Endpoint | Credits | Type | | --------------------------- | ------- | -------- | | `/subscription/intel-usage` | 0 | free | | `/analytics/credit-periods` | 0 | free | | `/analytics/endpoint-calls` | 0 | free | ### Polymarket | Endpoint | Credits | Type | | ---------------------------------------------------- | ------- | -------- | | `/polymarket/events/{eventId}` | 1 | per call | | `/polymarket/wallet/{addr}/summary/balance` | 1 | per call | | `/polymarket/wallet/{addr}/summary/biggest-win` | 1 | per call | | `/polymarket/prices` | 2 | per call | | `/polymarket/wallet/{addr}/summary/portfolio` | 2 | per call | | `/polymarket/wallet/{addr}/summary/pnl` | 2 | per call | | `/polymarket/wallet/{addr}/summary/stats` | 2 | per call | | `/polymarket/wallet/{addr}/tags` | 2 | per call | | `/polymarket/order-book/{conditionId}` | 2 | per call | | `/polymarket/stats` | 2 | per call | | `/polymarket/top-events/{eventId}/breakdown` | 2 | per call | | `/polymarket/events` | 3 | per call | | `/polymarket/positions/{addr}` | 3 | per call | | `/polymarket/top-holders/{conditionId}` | 3 | per call | | `/polymarket/event-positions/{conditionId}` | 3 | per call | | `/polymarket/pnl/chart` | 3 | per call | | `/polymarket/wallet/{addr}/event-history` | 3 | per call | | `/polymarket/wallet/{addr}/prediction-history` | 3 | per call | | `/polymarket/activity` | 5 | per call | | `/polymarket/top-events` | 10 | per call | | `/polymarket/leaderboard` | 10 | per call | ### Hypercore | Endpoint | Credits | Type | | ------------------------------------------------- | ------- | -------- | | `/hypercore/markets` | 1 | per call | | `/hypercore/account/{address}/active` | 1 | per call | | `/hypercore/account/{address}/summary` | 1 | per call | | `/hypercore/account/{address}/spot-balances` | 1 | per call | | `/hypercore/account/{address}/perp-positions` | 1 | per call | | `/hypercore/account/{address}/portfolio-history` | 1 | per call | | `/hypercore/account/{address}/subaccounts` | 1 | per call | | `/hypercore/account/{address}/trades` | 2 | per row | | `/hypercore/entity/{entity}/active` | 2 | per call | | `/hypercore/entity/{entity}/summary` | 2 | per call | | `/hypercore/entity/{entity}/spot-balances` | 2 | per call | | `/hypercore/entity/{entity}/perp-positions` | 2 | per call | | `/hypercore/entity/{entity}/portfolio-history` | 2 | per call | | `/hypercore/token/{pricingId}/positions` | 3 | per call | | `/hypercore/trades` | 2 | per row | ::: warning Per-Row Billing These endpoints charge per result returned. Total credits are the number of rows returned multiplied by the endpoint's listed credit value. For example, 50 `/hypercore/trades` rows cost 50 × 2 = **100 credits**. Use the `limit` parameter to control costs. ::: ### Risk Scoring ::: info Risk Scoring is an add-on to an API subscription. Contact [api@arkm.com](mailto:api@arkm.com) to enable access. ::: | Endpoint | Credits | Type | | ------------------------------- | ------- | -------- | | `/risk/address/{address}` | 5 | per call | | `/risk/entity/{entity_id}` | 5 | per call | | `/risk/address/{address}/paths` | 20 | per call | | `POST /risk/address/batch` | 5 | per row | | `POST /risk/entity/batch` | 5 | per row | ::: tip Endpoints with credit cost **0** are free and do not consume credits. ::: ## Monitoring Your Usage You can track your credit consumption in real time on the [API Dashboard](https://arkm.com/api-dashboard). The dashboard shows per-endpoint breakdowns, hourly usage trends, and remaining credits for your billing period. ## Need Help? For questions about credit usage, custom pricing, or plan upgrades, contact [api@arkm.com](mailto:api@arkm.com). ## Pagination The Arkham API uses **offset-based pagination**: - **`limit`** — Maximum results per page (defaults vary by endpoint) - **`offset`** — Number of results to skip (default: 0) Responses include `count` or `total` showing total matching records. ## Pagination Limits by Endpoint `/transfers` Default: 20 · Max: No hard cap · *50-500 recommended* `/swaps` Default: 50 · Max: No hard cap · *50-500 recommended* `/counterparties/*` Default: 1000 · Max: **1000** · *Hard limit enforced* `/tokens` Default: 100 · Max: 250 `/tag/{id}/params` Default: 100 · Max: 1000 ::: warning Why 50-500 recommended? Large result sets increase response time and credit cost. Elasticsearch queries have internal timeouts (~20s for complex queries), and performance might degrade above ~1000 results. ::: ## Iterating Through Results ### Option 1: Offset-Based (Simple) Simple but may miss/duplicate items if data changes during pagination: ```python offset, all_results = 0, [] while True: resp = api.get_transfers(limit=50, offset=offset) all_results.extend(resp['transfers']) if len(resp['transfers']) < 50: break offset += 50 ``` ### Option 2: Time-Window Sliding (Consistent) For frequently-changing data, page by time windows instead of offsets: ```python from datetime import datetime, timedelta window_end = datetime.utcnow() window_size = timedelta(hours=1) all_results = [] while window_end > earliest_date: window_start = window_end - window_size resp = api.get_transfers( timeGte=int(window_start.timestamp() * 1000), timeLte=int(window_end.timestamp() * 1000), limit=500 ) all_results.extend(resp['transfers']) window_end = window_start # slide window back ``` ::: tip Time-window sliding eliminates duplicates and missed records caused by new data arriving during pagination. ::: ## Timestamps & Sorting ### Absolute (Unix milliseconds) ``` ?timeGte=1696630274000&timeLte=1696716674000 ``` ### Relative Duration ``` ?timeLast=24h ``` Also supports: `7d`, `30d`, `1M`, `1y` ::: danger Cannot combine `timeLast` with `timeGte`/`timeLte` — returns HTTP 400. ::: ## Sorting - **`sortKey`** — `time` (default), `value`, `usd` - **`sortDir`** — `desc` (default), `asc` **Example:** ``` GET /transfers?sortKey=usd&sortDir=desc&timeLast=24h ``` ## Time Handling Notes - All timestamps are **UTC** - `timeLast` is calculated from server time at request - `/portfolio` time parameter is truncated to **UTC midnight** ## Risk Scoring (Beta) Risk Scoring assigns a compliance risk score to blockchain addresses and entities, based on their exposure to illicit activity. Use it to screen counterparties, flag high-risk wallets, and build compliance and monitoring workflows on top of the Arkham API. Risk Scoring is currently in **beta**. Coverage is still expanding, and the scoring model and response fields may change as we refine it. We welcome your feedback at [api@arkm.com](mailto:api@arkm.com). ::: info Risk Scoring is an optional **paid add-on** to an API subscription. It is not included in the standard tiers. To enable it, [contact sales](mailto:api@arkm.com). You can also evaluate it during a trial (see [Trialing Risk Scoring](#trialing-risk-scoring)). ::: ## How Scores Work Every address or entity is scored from **0–100** in each of several **risk categories**. The overall `risk_level` is derived from `max_score`, the highest individual category score: | `risk_level` | `max_score` | Meaning | | ------------ | ----------- | ----------------------------------------- | | `NONE` | 0–9 | No meaningful exposure detected | | `LOW` | 10–39 | Minor or distant exposure | | `MEDIUM` | 40–59 | Notable exposure | | `HIGH` | 60–79 | Strong, direct exposure | | `SEVERE` | 80–100 | Severe, direct exposure to illicit funds | ### Risk Categories A separate score (0–100) is computed for each risk category, so you can see why an address is risky, for example a high `sanctions_score` versus a high `scam_score`. `greatest_risk_category` names the category that drives the overall score. | Category | Score field | What it measures | | ------------------- | ----------------------- | -------------------------------------------------------- | | `hacker` | `hacker_score` | Funds linked to thefts, exploits, or hacks | | `sanctions` | `sanctions_score` | Exposure to sanctioned addresses or entities (e.g. OFAC) | | `scam` | `scam_score` | Exposure to scams such as phishing or fraud | | `mixer` | `mixer_score` | Exposure to mixing services that obscure fund flows | | `ransomware` | `ransomware_score` | Funds linked to ransomware operations | | `darkweb` | `darkweb_score` | Exposure to darknet marketplaces | | `gambling` | `gambling_score` | Exposure to gambling services | | `privacy` | `privacy_score` | Exposure to privacy-focused protocols | | `ponzi` | `ponzi_score` | Exposure to Ponzi or high-yield investment schemes | | `token-blacklist` | `token_blacklist_score` | Exposure to tokens flagged on blacklists | | `non-kyc-service` | `non_kyc_service_score` | Exposure to services that do not perform KYC | | `mixed-kyc-service` | `mixed_kyc_service_score` | Exposure to services with partial or inconsistent KYC | ### Forward & Backward Exposure Risk is propagated across the transaction graph in two directions: Forward Risk from funds the address **received** from risky sources `max_score_forward`, `greatest_risk_category_forward` Backward Risk from funds the address **sent** toward risky destinations `max_score_backward`, `greatest_risk_category_backward` ## Endpoints | Endpoint | Description | | ------------------------------------- | ----------------------------------------------------------------- | | `GET /risk/address/{address}` | Risk score for a single address, with category breakdown. | | `GET /risk/address/{address}/paths` | The transaction paths that connect the address to risky sources. | | `POST /risk/address/batch` | Risk scores for many addresses in one request. | | `GET /risk/entity/{entity_id}` | Risk score for an entity, aggregated across its addresses. | | `POST /risk/entity/batch` | Risk scores for many entities in one request. | See the [API Reference](/docs) for full request and response schemas. ::: tip The batch endpoints accept up to **200** addresses or entities per request and return a map keyed by the value you sent, so you can screen lists without making one request per item. ::: ## Response Fields A `GET /risk/address/{address}` response describes the address's overall risk, a per-category breakdown, how that risk was traced across the transaction graph, and the specific sources behind it. Optional fields are omitted when they do not apply, so an address with negligible risk exposure returns only `chain_type`, `address`, `risk_level`, and `is_seed`. A `risk_level` of `NONE` means no risk exposure was identified for the address at or above the reporting threshold (a `max_score` below 10), and is also returned for addresses that are not currently covered. ```json { "chain_type": "bitcoin", "address": "bc1qd5j8sd04hf4qem0x0ytl56w909nse4pgv9yt82", "risk_level": "HIGH", "hacker_score": 63, "sanctions_score": 63, "max_score": 63, "sanctioned_1hop_score": 55, "greatest_risk_category": "sanctions", "max_score_forward": 63, "greatest_risk_category_forward": "hacker", "is_seed": false, "hop_distance": 1, "max_hop_reached": 3, "risk_weighted_incoming_usd": 76402482.23, "risk_weighted_outgoing_usd": 0, "top_sources": [ { "seed_address": "3NbdrezMzAVVfXv5MTQJn4hWqKhYCTCJoB", "risk_category": "hacker", "direction": "forward", "contribution_pct": 28.73, "contribution_usd": 21952641.75, "hop_distance": 3, "first_ts": "2024-10-28T00:00:00Z", "last_ts": "2025-04-25T00:00:00Z" }, { "seed_address": "34VXKa5upLWVYMXmgid6bFM4BaQXHxSUoL", "risk_category": "sanctions", "direction": "forward", "contribution_pct": 21.27, "contribution_usd": 16248599.37, "hop_distance": 1, "first_ts": "2024-10-28T00:00:00Z", "last_ts": "2025-04-25T00:00:00Z" } ], "updated_at": "2026-06-17T04:31:44Z" } ``` **Overall** | Field | Type | Description | | ------------------------ | ------- | ----------------------------------------------------------------------------------- | | `chain_type` | string | Blockchain type (`evm`, `bitcoin`, `tron`, `solana`, `dogecoin`, `zcash`). May be empty if the address has no risk data. | | `address` | string | The queried address, in its canonical format. | | `risk_level` | string | Overall level: `NONE`, `LOW`, `MEDIUM`, `HIGH`, or `SEVERE`. Derived from `max_score`. | | `max_score` | integer | The highest individual category score (0–100). Determines `risk_level`. | | `greatest_risk_category` | string | The category that produced `max_score`. | **Category scores** Each [risk category](#risk-categories) has a corresponding `*_score` field (0–100). One additional score is not part of the category list: | Field | Type | Description | | ----------------------- | ------- | ----------------------------------------------------------- | | `sanctioned_1hop_score` | integer | Score for direct (one-hop) exposure to high-risk entities. | **Directional exposure** | Field | Type | Description | | --------------------------------- | ------- | --------------------------------------------------------------------------- | | `max_score_forward` | integer | Highest score from forward propagation (funds received from risky sources). | | `greatest_risk_category_forward` | string | Category driving the forward score. | | `max_score_backward` | integer | Highest score from backward propagation (funds sent toward risky destinations). | | `greatest_risk_category_backward` | string | Category driving the backward score. | **Tracing** | Field | Type | Description | | ---------------------------- | ------- | ----------------------------------------------------------------------- | | `is_seed` | boolean | `true` if the address is itself a known risk source, rather than scored via propagation. | | `hop_distance` | integer | Number of transaction hops to the nearest risky source. | | `max_hop_reached` | integer | Maximum hop distance analyzed when computing the score. | | `risk_weighted_incoming_usd` | number | Risk-weighted USD that reached this address from upstream risky sources. | | `risk_weighted_outgoing_usd` | number | Risk-weighted USD this address sent toward downstream risky sources. | **Top sources** `top_sources` is an array of the specific risky sources behind the score, ranked by contribution. Each entry contains: | Field | Type | Description | | ----------------- | ------- | ------------------------------------------------------------------------------ | | `seed_address` | string | Address of the risky source. | | `risk_category` | string | Risk category of the source. | | `direction` | string | `forward` (the source sent funds to this address) or `backward` (this address funded the source). | | `contribution_pct` | number | Percentage this source contributes to the assessment. | | `contribution_usd` | number | USD value of funds traced to this source. | | `hop_distance` | integer | Hops from the queried address to this source. | | `first_ts` | string | Timestamp of the first linking transaction (RFC3339). | | `last_ts` | string | Timestamp of the last linking transaction (RFC3339). | **Metadata** | Field | Type | Description | | ------------ | ------ | ----------------------------------------------------- | | `updated_at` | string | Timestamp of the most recent score update (RFC3339). | The entity endpoints return a similar structure aggregated across the entity's addresses. See the [API Reference](/docs) for the full entity schema. ## Access Risk Scoring is a paid add-on. The standard monthly and annual tiers **do not** include it; it is available under a **Custom** subscription. To enable Risk Scoring on your account, [contact sales](mailto:api@arkm.com). ### Trialing Risk Scoring You can evaluate Risk Scoring during a free trial. Trial usage is limited to **10,000 risk scores** in total, counted across all of the risk endpoints for the duration of your trial. Each single-address or single-entity lookup counts as one score, and each item in a batch counts individually (a batch of 50 addresses counts as 50). Addresses and entities with no risk exposure still count, since the API returns a `NONE` score for them. Once you reach the limit, the risk endpoints return `429 Too Many Requests`. To continue, [contact sales](mailto:api@arkm.com) for full access. ## x402: Pay-per-Request for Agents Buy Arkham API responses one request at a time with USDC — no account, no API key, no subscription. Built for AI agents that need blockchain intelligence on demand, priced exactly like the classic API. **Base URL:** `https://api.arkm.com/x402` — agent instructions at [/skill.md](https://api.arkm.com/x402/skill.md), machine-readable catalog with prices at [/openapi.json](https://api.arkm.com/x402/openapi.json), full API docs for agents at [arkm.com/llms.txt](https://arkm.com/llms.txt) ## How It Works Every endpoint speaks the [x402 protocol](https://x402.org) (v2): an unauthenticated request returns HTTP `402 Payment Required` with a signed price quote, the client pays in USDC on Base or Solana, and the same request succeeds. 1. **Request** — `POST` the endpoint with a JSON body (all parameters, including path parameters, go in the body). 2. **Quote** — the response is `402` with a `PAYMENT-REQUIRED` header: a base64 JSON challenge carrying the price, the receiving address, and the exact resource URL. 3. **Pay** — the client signs the payment for whichever network it picked from the challenge: an EIP-3009 USDC transfer authorization on Base, or a USDC payment transaction on Solana. No gas needed on your side; the facilitator broadcasts and pays gas. 4. **Response** — the request is retried with the payment header, Arkham serves the data, and settlement happens on-chain only after a successful response. ::: info **Failed requests are never charged.** Payment settles only when the upstream call returns a success — an invalid parameter or a server error costs you nothing. ::: Any x402-compatible client handles steps 2–4 automatically (the `@x402/*` SDKs among others); your agent code just makes HTTP calls with a funded wallet. ## Try It Inspect a price quote without paying — request any endpoint and decode the challenge: ```bash curl -s -X POST https://api.arkm.com/x402/balances/address \ -H "Content-Type: application/json" \ -d '{"address": "0x28C6c06298d514Db089934071355E5743bf21d60"}' \ -D - -o /dev/null | tr -d '\r' | grep -i payment-required | cut -d' ' -f2 | base64 -d ``` The decoded challenge lists every accepted network (`eip155:8453` — Base mainnet, `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` — Solana mainnet), each with its USDC asset address, the amount, and the `payTo` treasury address. ## Pricing Prices mirror the classic API's [credit weights](/usage/credit-pricing) at **$0.20 per credit** — the same request costs the same whether you pay with a subscription or with USDC. Per-Call Fixed price each time you call the endpoint **Price = credits × $0.20** Per-Row Priced by the `limit` you request (default 50 if omitted), whether or not that many rows come back **Price = credits × limit × $0.20** For per-row endpoints, pass the smallest `limit` that covers what you need: you pay for the limit you request, not the rows returned. Per-endpoint prices are published in the catalog: every operation in [`/openapi.json`](https://api.arkm.com/x402/openapi.json) carries an `x-payment-info` block, and [`/skill.md`](https://api.arkm.com/x402/skill.md) lists them in a table. ## Free Discovery Endpoints Three endpoints cost nothing so agents can orient before spending: `chains`, `networks/status`, and `arkm/circulating`. Instead of payment they ask for a **Sign-In-With-X** wallet signature (the `402` carries a `sign-in-with-x` challenge; sign it to prove wallet ownership — no funds move). ## Rate Limits Enforced per paying wallet, per endpoint: **100 requests/second** by default, **5 requests/second** on heavy endpoints (transfers, swaps, flow, counterparties, intelligence search, token leaderboards). The full list is in [`/skill.md`](https://api.arkm.com/x402/skill.md). ## Differences from the Classic API | | Classic API | x402 | |---|---|---| | Auth | API key header | none — payment is the auth | | Method | mostly `GET` | always `POST`, parameters in the JSON body | | Billing | monthly credits | USDC per request, settled on-chain | | Coverage | full API | public read catalog (~90 endpoints) | Response schemas are identical — data comes from the same API. ## When to Use Which Use **x402** for autonomous agents, one-off lookups, and integrations where provisioning an API key is impractical. Use the [classic API](/getting-started/access) for sustained volume — a subscription with monthly credits is cheaper at scale and covers endpoints outside the public read catalog. ## Errors & HTTP Codes - **200** — Success - **400** — Bad request (invalid params) - **401** — Unauthorized (API key issue) - **402** — Payment required - **403** — Forbidden (endpoint not in plan) - **404** — Not found - **429** — Rate limit exceeded - **500** — Server error Error responses follow this format: ```json { "error": "description" } ``` ## Common Errors `"error validating API key"` (401) Check key, use `API-Key` header (not `Authorization`) `"not allowed to access endpoint"` (403) Contact api@arkm.com for plan upgrade `"cannot use timeLast and timeGte together"` Use one or the other, not both `"limit must be between 1 and 1000"` Reduce limit (counterparties max: 1000) ## Rate Limit Handling (429) When you receive a 429, implement backoff: ```python import time import requests def request_with_backoff(url, headers, max_retries=5): for attempt in range(max_retries): resp = requests.get(url, headers=headers) if resp.status_code == 429: time.sleep(int(resp.headers.get('Retry-After', 60))) continue return resp ``` ## WebSocket Errors WebSocket errors are returned in this format: ```json { "type": "error", "payload": { "code": "INVALID_FILTER", "message": "..." } } ``` ### Error Codes - **`INVALID_PAYLOAD`** — Malformed request - **`INVALID_FILTER`** — Invalid filter parameters - **`INSUFFICIENT_CREDITS`** — Out of credits - **`TIER_RATE_LIMITED`** — Rate limit for your tier ### Credit Exhaustion When credits are exhausted: ```json { "type": "error", "payload": { "code": "INSUFFICIENT_CREDITS", "limitType": "hourly", "resetIn": 1800 } } ``` - **`code`** — `INSUFFICIENT_CREDITS` or `TIER_RATE_LIMITED` - **`limitType`** — Which limit: `minutely`, `hourly`, or `monthly` - **`resetIn`** — Seconds until the limit resets ## Security & Best Practices ### DO - Store in environment variables or secrets manager - Use separate keys per environment (prod / staging / dev) - Use separate keys per service (backend / analytics / partner) - Rotate periodically - Always use header: `API-Key: YOUR_KEY` ### DON'T - Commit to version control - Put in URLs as query parameters - Log in plaintext - Share across organizations - Expose in client-side code ## Query Optimization ### 1. Add Filters to Reduce Result Size ``` ?chain=ethereum&usdGte=10000&timeLast=24h ``` ### 2. Use Appropriate Time Windows - **Real-time monitoring** → `1h` or less - **Daily analysis** → `24h` - **Historical research** → `30d` ### 3. Implement Caching - **Intelligence data** → Hours - **Historical transfers** → Indefinitely - **Balances** → Minutes ## WebSocket Best Practices 1. **Always include filters** — use `from`, `to`, `tokens`, or `usdGte >= 10M` 2. **Save `sessionId`** for reconnection (valid 5 minutes) 3. **Implement ping/pong heartbeat** to detect disconnections ### WebSocket Quotas WebSocket connections consume credits based on: - **Connection time**: Charged per minute of active connection - **Transfer notifications**: Charged per notification (weight varies by plan) **Quota tiers:** - Per-minute (burst protection) - Per-hour - Per-month ### Recovery After Disconnection - Events are **buffered for 5 minutes** during disconnection - Reconnect using `sessionId` to restore filters and receive pending notifications - After 5 minutes, session expires and buffered events are discarded ## Production Checklist Before going to production, verify: - [ ] API keys stored in secrets manager (not in code) - [ ] Client-side rate limiting implemented - [ ] Exponential backoff for 429/5xx errors - [ ] Error handling for all HTTP status codes - [ ] Request logging enabled (without secrets) - [ ] Monitoring for 429/5xx rates - [ ] Caching strategy per endpoint type - [ ] Separate keys per environment ## Evidentiary Usage Arkham provides **investigative intelligence** designed to accelerate attribution, triage, and on-chain analysis. It is intended to support investigations and decision-making, not to serve as standalone evidence. ::: warning Customers are responsible for corroborating findings and meeting their own legal, regulatory, and evidentiary standards. ::: Arkham emphasizes: - **Confidence-scored attribution** - **Auditability and transparency** - Clear explanations of why labels exist and how they were derived ## Cookbook & Starter Kits This page provides ready-to-use tools and code examples to help you start using the Arkham API quickly. Whether you prefer a visual interface or copy-paste code snippets, we have options for different workflows. **What's here:** - **Postman Collection** — Import into Postman and start making requests immediately, no coding required - **Code Snippets** — Minimal Python and curl examples you can copy directly into your projects All tools come pre-configured with example addresses and sensible defaults so you can test the API right away. ## Postman Collection [Postman](https://www.postman.com/) is a free API client that lets you explore and test APIs without writing code. It's the fastest way to start making requests — add your key in the **Variables** tab and click Send. **What you get:** every endpoint in the public API, grouped by API resource, each pre-filled with a working example and a sample response ::: warning WebSocket Streaming This collection covers the HTTP endpoints, including the stream-management calls (`POST /ws/v2/streams`, etc.). Postman does support WebSocket requests, but live transfer streaming needs a session created first — the [Python snippet](#websocket-v2-stream-real-time-transfers-recommended) below handles the full connect-and-stream flow. ::: ### Get the collection **[Open the Arkham Intel API collection on Postman ↗](https://documenter.getpostman.com/view/56855106/2sBY4SLy6F)** — fork it into your own workspace to get an editable copy. Prefer a file? [**Download Collection JSON ↓**](/cookbook/postman/Arkham-API-Complete.postman_collection.json) — it also imports into Insomnia, Bruno and other API clients, or drop it straight into CI. ### Setup Instructions 1\. Fork the collection above, or download the JSON 2\. If you downloaded, open Postman and click **Import**, then select the file — a fork is already in your workspace 3\. Select the collection in the sidebar and open its **Variables** tab 4\. In the `apiKey` row, paste your API key into the **Value** column 5\. Start exploring endpoints in the **Collections** sidebar ![Postman setup — the numbered markers match the steps above](/cookbook/postman-setup.png) The collection defines `baseUrl` and `apiKey` on itself, so it sends requests without an environment file. ::: tip Keeping your copy up to date The published collection tracks the API. Your own copy doesn't follow it automatically: in a fork, open the collection's **⋯** menu and choose **Pull changes** to bring it in line. A downloaded file is a point-in-time snapshot — download it again to refresh. ::: ## Quick Code Examples ### REST: Get Address Intelligence **Python:** ```python import requests API_KEY = "your-api-key" address = "0x28C6c06298d514Db089934071355E5743bf21d60" # Binance response = requests.get( f"https://api.arkm.com/intelligence/address/{address}", headers={"API-Key": API_KEY} ) print(response.json()) ``` **curl:** ```bash curl -H "API-Key: YOUR_KEY" \ "https://api.arkm.com/intelligence/address/0x28C6c06298d514Db089934071355E5743bf21d60" ``` ### REST: Get Recent Transfers **Python:** ```python import requests API_KEY = "your-api-key" response = requests.get( "https://api.arkm.com/transfers", headers={"API-Key": API_KEY}, params={ "base": "0x28C6c06298d514Db089934071355E5743bf21d60", # Binance "timeLast": "1h", "limit": 10 } ) print(response.json()) ``` **curl:** ```bash curl -H "API-Key: YOUR_KEY" \ "https://api.arkm.com/transfers?base=0x28C6c06298d514Db089934071355E5743bf21d60&timeLast=1h&limit=10" ``` ### WebSocket v2: Stream Real-Time Transfers (Recommended) WebSocket v2 is the recommended way to stream real-time transfers. You create a stream with your filter via REST, then connect via WebSocket to receive transfers automatically — no subscribe/unsubscribe messages needed. ::: tip Stream Filters WebSocket streams take the address, token and value filters of `GET /transfers` (`base`, `from`, `to`, `chains`, `tokens`, `usdGte`, `usdLte`, `valueGte`, `valueLte`). The difference is that `POST /ws/v2/streams` accepts them as a **JSON request body** with arrays, while `GET /transfers` uses query parameters. ::: ::: tip Filter Requirement Streams require at least one of: `base`, `from`, `to`, `tokens`, or `usdGte >= 250,000`. ::: **Step 1: Create a stream with your filter (JSON body)** ```python import requests API_KEY = "your-api-key" # Create a stream that monitors CEX withdrawals >= $10k response = requests.post( "https://api.arkm.com/ws/v2/streams", headers={"API-Key": API_KEY}, json={"from": ["type:cex"], "usdGte": "10000"} ) print(response.json()) # {"streamId": "e561dd5b-a24d-4669-bbba-9e5cf37d5125", "id": 1, "createdAt": "..."} ``` **Step 2: Connect and receive transfers** ```python import asyncio import json import websockets # requires websockets>=13; older versions use extra_headers API_KEY = "your-api-key" STREAM_ID = "e561dd5b-a24d-4669-bbba-9e5cf37d5125" # the streamId returned by POST /ws/v2/streams in step 1 async def stream_transfers(): url = f"wss://api.arkm.com/ws/v2/transfers?stream_id={STREAM_ID}" headers = {"API-Key": API_KEY} # Reconnecting reuses the same stream — you do not need to create a new one. while True: try: async with websockets.connect(url, additional_headers=headers) as ws: # Transfers are delivered automatically — no subscribe message needed. # Each message is an envelope: {"type": "transfer", "payload": {"transfer": {...}, "alertId": N}} async for message in ws: data = json.loads(message) if data.get("type") == "transfer": transfer = data["payload"]["transfer"] print(transfer) except (websockets.WebSocketException, OSError): print("connection lost, reconnecting...") await asyncio.sleep(1) asyncio.run(stream_transfers()) ``` **Managing streams:** ```python import requests API_KEY = "your-api-key" STREAM_ID = "e561dd5b-a24d-4669-bbba-9e5cf37d5125" headers = {"API-Key": API_KEY} # List your streams print(requests.get("https://api.arkm.com/ws/v2/streams", headers=headers).json()) # Delete a stream print(requests.delete(f"https://api.arkm.com/ws/v2/streams/{STREAM_ID}", headers=headers).json()) ``` ::: info Reconnection Streams stay active for 10 minutes after disconnection. Reconnecting within that window reactivates the stream and delivers any transfers that occurred while you were disconnected. After 10 minutes the stream is cleaned up and you need to create a new one. ::: ::: info Stream limit You can have at most 10 active streams per user. Contact [api@arkm.com](mailto:api@arkm.com) for higher limits. ::: ### Migrating from WebSocket v1 to v2 | | v1 | v2 | |---|---|---| | **Create session/stream** | `POST /ws/sessions` (500 credits) | `POST /ws/v2/streams` (free) | | **Connect** | `wss://…/ws/transfers?session_id=…` | `wss://…/ws/v2/transfers?stream_id=…` | | **Set filter** | Send subscribe JSON message over WebSocket | Filter set at stream creation via JSON request body | | **Change filter** | Send unsubscribe then subscribe | Delete stream and create a new one | | **Per-transfer cost** | 2 credits | 2 credits | | **Filter parameters** | Same in v1 and v2 | Same in v1 and v2 | **What stays the same:** The filter fields (`base`, `from`, `to`, `chains`, `tokens`, `usdGte`, `valueGte`) are identical between v1 and v2. The transfer objects you receive have the same schema, though their fields vary by chain — UTXO chains such as Bitcoin have no token fields and report `fromAddresses` / `toAddresses` lists. **What changes:** In v2, the filter is set at stream creation time via a JSON request body on `POST /ws/v2/streams`, not via a subscribe message over the WebSocket. The WebSocket connection in v2 is purely for receiving transfers — no client messages needed. ### WebSocket v1: Stream Real-Time Transfers (Deprecated) ::: warning Deprecated WebSocket v1 is deprecated. Use v2 above for lower cost and simpler integration. V1 endpoints remain functional but are no longer recommended. ::: ```python import asyncio import json import requests import websockets # requires websockets>=13; older versions use extra_headers API_KEY = "your-api-key" # v1 requires a prepaid session: create one via REST, then pass its id on connect. resp = requests.post("https://api.arkm.com/ws/sessions", headers={"API-Key": API_KEY}) resp.raise_for_status() SESSION_ID = resp.json()["sessionId"] async def stream_transfers(): url = f"wss://api.arkm.com/ws/transfers?session_id={SESSION_ID}" headers = {"API-Key": API_KEY} subscribed = False while True: try: async with websockets.connect(url, additional_headers=headers) as ws: if not subscribed: # Subscribe to transfers from CEXs >= $10k subscribe_msg = { "id": "1", "type": "subscribe", "payload": { "filters": { "from": ["type:cex"], "usdGte": 10000 } } } await ws.send(json.dumps(subscribe_msg)) subscribed = True async for message in ws: data = json.loads(message) if data.get("type") == "transfer": transfer = data["payload"]["transfer"] print(transfer) except (websockets.WebSocketException, OSError): print("connection lost, reconnecting...") await asyncio.sleep(1) asyncio.run(stream_transfers()) ``` ## WebSocket Filter Reference WebSocket stream filters are sent as a JSON request body on `POST /ws/v2/streams`. - `base` (string[]) — Entity names or wallet addresses to monitor (sender or receiver) - `from` (string[]) — Source addresses, entities, or special syntax (`type:cex`, `deposit:binance`) - `to` (string[]) — Destination addresses, entities, or special syntax - `tokens` (string[]) — Token IDs (e.g. `ethereum`, `usd-coin`) or contract addresses. Ticker symbols like `USDC` are not accepted - `chains` (string[]) — Blockchains to filter by (e.g. `ethereum`, `solana`) - `usdGte` / `usdLte` (number) — USD value range - `valueGte` / `valueLte` (number) — Raw token amount range **Filter Requirements:** You must include at least one of: - `base`, `from`, `to`, or `tokens` filter, **OR** - `usdGte >= 250,000` ## Troubleshooting ### Common Errors | Error | Solution | | -------------------------- | --------------------------------------------------------------------- | | Error validating API key | Check your key is correct. Use `API-Key` header, not `Authorization`. | | Filter requirement not met | WebSocket needs `base`, `from`, `to`, `tokens`, or `usdGte` ≥ 250K | | HTTP 401 Unauthorized | Invalid or expired API key | | HTTP 429 Too Many Requests | Rate limited — back off and retry. See the [Rate Limits](/usage/rate-limits) guide for per-plan limits. | ## More Resources - **API Reference:** [API Reference](#api-reference) - **Request API Access:** [arkm.com/api](https://arkm.com/api) - **Support:** [api@arkm.com](mailto:api@arkm.com) ## Using with Coding Agents Connect the Arkham API to an AI coding agent so it can fetch on-chain intelligence for you in natural language. This page walks through setting up Claude as an Arkham API assistant via a local MCP (Model Context Protocol) server. ::: tip One URL to give any AI agent The entire Arkham API documentation, including this guide and the full endpoint reference, is available as a single machine-readable file at [arkm.com/llms.txt](https://arkm.com/llms.txt). It auto-updates from our docs, so handing this URL to Claude, Cursor, Copilot, or any other coding agent is the fastest way to give it full, current knowledge of the API. ::: ## Set Up Claude to Use the Arkham API ### What you'll need - **Claude Desktop**. Free download at [claude.ai/download](https://claude.ai/download). - An **Arkham API key**. You can generate one at [arkm.com](https://arkm.com) under Settings → API Keys. - **Python 3.10+** installed on your machine. You can check by opening Terminal (Mac) or PowerShell (Windows) and typing `python3 --version`. If it's missing, grab it from [python.org](https://python.org). ### Step 1: Ask Claude to set it up for you Open Claude (web or desktop) in a regular chat and paste this prompt: > Build me a local MCP server that connects to the Arkham Intel API. It should expose one tool called `arkham_api_request` that takes `method`, `path`, `query_params`, and `body`. The API key should be read from an environment variable called `ARKHAM_API_KEY`. Base URL is `https://api.arkm.com`. Auth header is `API-Key`. For reference on available endpoints and request/response shapes, read the full API docs at https://arkm.com/llms.txt. Give me the full file and tell me exactly where to save it and how to install dependencies. Claude will give you a Python file and the two commands to run. Save the file somewhere you'll remember, like `~/arkham-mcp/server.py`. ### Step 2: Install the dependencies Open Terminal (Mac) or PowerShell (Windows) and run whatever `pip install` command Claude gave you. Typically: ``` pip install mcp requests ``` Press Enter. Wait for it to finish. That's the only "coding" part. ### Step 3: Plug it into Claude Desktop Claude Desktop reads a config file that tells it what connectors to load. **On Mac:** open Finder, press `Cmd+Shift+G`, paste: ``` ~/Library/Application Support/Claude/ ``` **On Windows:** open File Explorer, paste into the address bar: ``` %APPDATA%\Claude\ ``` Find the file `claude_desktop_config.json` (create it if it doesn't exist). Open it with any text editor and paste: ```json { "mcpServers": { "arkham-intel": { "command": "python3", "args": ["/full/path/to/your/server.py"], "env": { "ARKHAM_API_KEY": "paste-your-key-here" } } } } ``` Replace the path with where you saved the file in Step 1, and paste your real API key. Save and **fully quit + reopen Claude Desktop**. ::: warning Where the key goes Your key lives in this config file on your own machine. It never touches the chat, never goes to Anthropic, never leaves your computer. Don't share the config file or commit it to git. ::: ### Step 4: Create a Project Now the Claude-side setup. This part is pure clicking. 1. In Claude, click **Projects** in the sidebar → **Create project** 2. Name it _Arkham API Assistant_ 3. In **Project knowledge**, add: - [arkm.com/llms.txt](https://arkm.com/llms.txt). This bundles the complete guide and endpoint reference in a single file, which is all Claude really needs. - Any of your own notes, internal conventions, or example use cases. ::: tip `llms.txt` auto-updates from our docs, so re-uploading it occasionally keeps your project's knowledge current as the API evolves. ::: ### Step 5: Add project instructions Click **Set project instructions** and paste: > You are an Arkham Intel API assistant. When I ask you to fetch, check, or run something, use the `arkham_api_request` tool to make the actual API call. Always tell me which endpoint you're about to hit before calling it. Only use endpoints that exist in the uploaded docs. If something isn't documented, say so instead of guessing. For `/transfers`, warn me about per-row billing. Respect pagination limits. Save. ### Step 6: Try it Open a new chat inside the project: > _"What are the top 10 counterparties for Binance's hot wallet on Ethereum over the last 7 days?"_ > _"Pull the last 100 transfers over $1M for address 0x..."_ Claude will tell you which endpoint it's calling, run it through your local connector, and return the results. ### If something's not working Symptom - Claude says the tool isn't available - 401 Unauthorized errors - "Cannot find module mcp" errors - Claude invents endpoints that don't exist Fix - Fully quit and reopen Claude Desktop after editing the config - Double-check the API key in the config file, no extra spaces - Re-run `pip install mcp requests` in Terminal - Upload more complete docs to project knowledge ::: tip If you get stuck on any step, paste the error message back into Claude. It's very good at debugging its own setup. ::: ## Support & Contact - **API Support** (plans, custom requests, technical issues) → [api@arkm.com](mailto:api@arkm.com) - **General Support** (non-API) → [support@arkm.com](mailto:support@arkm.com) ## Product Links - **Arkham Intel (Web App)** → [arkm.com](https://arkm.com) - **API Documentation** → [API Reference](#api-reference) - **API Access Request** → [arkm.com/api](https://arkm.com/api) ## LLM-Friendly Docs Machine-readable versions of these docs for use with AI assistants and LLM-powered tools: - **llms.txt** (index) → [arkm.com/llms.txt](https://arkm.com/llms.txt) - **llms-full.txt** (complete guide + endpoint reference) → [arkm.com/llms-full.txt](https://arkm.com/llms-full.txt) - **Per-endpoint docs** → `arkm.com/llms/-.md` (e.g. [get-transfers.md](https://arkm.com/llms/get-transfers.md)) ## Research - **Arkham Research** → [info.arkm.com/research](https://info.arkm.com/research) ## Social - **X (Twitter)** → [x.com/arkham](https://x.com/arkham) - **LinkedIn** → [linkedin.com/company/arkhamintelligence](https://linkedin.com/company/arkhamintelligence) - **YouTube** → [youtube.com/@ArkhamIntel](https://youtube.com/@ArkhamIntel) - **Telegram** → [t.me/arkhamintelligence](https://t.me/arkhamintelligence) - **Discord** → [discord.gg/arkham](https://discord.gg/arkham) --- ## Endpoint Reference | Method | Path | Summary | Key Parameters | |--------|------|---------|----------------| | [GET](https://arkm.com/llms/get-analytics-credit-periods.md) | `/analytics/credit-periods` | Get historical credit usage by billing period | | | [GET](https://arkm.com/llms/get-analytics-endpoint-calls.md) | `/analytics/endpoint-calls` | Get API endpoint call analytics | period, end_date, start_date | | [GET](https://arkm.com/llms/get-arkm-circulating.md) | `/arkm/circulating` | Get ARKM circulating supply | | | [GET](https://arkm.com/llms/get-balances-address-address.md) | `/balances/address/{address}` | Get token balances for an address | address, chains | | [GET](https://arkm.com/llms/get-balances-entity-entity.md) | `/balances/entity/{entity}` | Get token balances for an entity | entity, cheap, chains | | [GET](https://arkm.com/llms/get-balances-solana-subaccounts-address-addresses.md) | `/balances/solana/subaccounts/address/{addresses}` | Get Solana subaccount balances for addresses | addresses, pricingID, limit | | [GET](https://arkm.com/llms/get-balances-solana-subaccounts-entity-entities.md) | `/balances/solana/subaccounts/entity/{entities}` | Get Solana subaccount balances for entities | entities, pricingID, limit | | [GET](https://arkm.com/llms/get-chains.md) | `/chains` | Get supported chains list | | | [GET](https://arkm.com/llms/get-cluster-id-summary.md) | `/cluster/{id}/summary` | Get cluster summary statistics | id | | [GET](https://arkm.com/llms/get-counterparties-address-address.md) | `/counterparties/address/{address}` | Get top counterparties for an address | address, chains, flow, tokens, ... | | [GET](https://arkm.com/llms/get-counterparties-entity-entity.md) | `/counterparties/entity/{entity}` | Get top counterparties for an entity | entity, chains, flow, tokens, ... | | [GET](https://arkm.com/llms/get-flow-address-address.md) | `/flow/address/{address}` | Get historical USD flows for an address | address, chains | | [GET](https://arkm.com/llms/get-flow-entity-entity.md) | `/flow/entity/{entity}` | Get historical USD flows for an entity | entity, chains | | [GET](https://arkm.com/llms/get-history-address-address.md) | `/history/address/{address}` | Get historical data for an address | address, chains | | [GET](https://arkm.com/llms/get-history-entity-entity.md) | `/history/entity/{entity}` | Get historical data for an entity | entity, chains | | [GET](https://arkm.com/llms/get-hypercore-account-address-active.md) | `/hypercore/account/{address}/active` | Check for HyperCore activity | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-perp-positions.md) | `/hypercore/account/{address}/perp-positions` | Get HyperCore perp positions | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-portfolio-history.md) | `/hypercore/account/{address}/portfolio-history` | Get HyperCore account value / PnL history | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-spot-balances.md) | `/hypercore/account/{address}/spot-balances` | Get HyperCore spot balances | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-subaccounts.md) | `/hypercore/account/{address}/subaccounts` | Get HyperCore sub-accounts | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-summary.md) | `/hypercore/account/{address}/summary` | Get HyperCore account value summary | address | | [GET](https://arkm.com/llms/get-hypercore-account-address-trades.md) | `/hypercore/account/{address}/trades` | Get HyperCore trade history | address, limit, offset | | [GET](https://arkm.com/llms/get-hypercore-entity-entity-active.md) | `/hypercore/entity/{entity}/active` | Check for HyperCore entity activity | entity | | [GET](https://arkm.com/llms/get-hypercore-entity-entity-perp-positions.md) | `/hypercore/entity/{entity}/perp-positions` | Get HyperCore entity perp positions | entity | | [GET](https://arkm.com/llms/get-hypercore-entity-entity-portfolio-history.md) | `/hypercore/entity/{entity}/portfolio-history` | Get HyperCore entity account value / PnL history | entity | | [GET](https://arkm.com/llms/get-hypercore-entity-entity-spot-balances.md) | `/hypercore/entity/{entity}/spot-balances` | Get HyperCore entity spot balances | entity | | [GET](https://arkm.com/llms/get-hypercore-entity-entity-summary.md) | `/hypercore/entity/{entity}/summary` | Get HyperCore entity value summary | entity | | [GET](https://arkm.com/llms/get-hypercore-markets.md) | `/hypercore/markets` | List HyperCore markets | | | [GET](https://arkm.com/llms/get-hypercore-token-pricingId-positions.md) | `/hypercore/token/{pricingId}/positions` | Get top HyperCore perp positions for a token | pricingId, side, groupByEntity, sort, ... | | [GET](https://arkm.com/llms/get-hypercore-trades.md) | `/hypercore/trades` | Search HyperCore trades | base, coins, pricingIds, side, ... | | [GET](https://arkm.com/llms/get-hypercore-trades-aggregate.md) | `/hypercore/trades/aggregate` | Aggregate HyperCore trades | base, coins, pricingIds, side, ... | | [POST](https://arkm.com/llms/post-intelligence-address-batch.md) | `/intelligence/address/batch` | Batch lookup address intelligence | chains, chain | | [POST](https://arkm.com/llms/post-intelligence-address-batch-all.md) | `/intelligence/address/batch/all` | Batch lookup address intelligence across all chains | | | [GET](https://arkm.com/llms/get-intelligence-address-address.md) | `/intelligence/address/{address}` | Get intelligence about an address | address, chain | | [GET](https://arkm.com/llms/get-intelligence-address-address-all.md) | `/intelligence/address/{address}/all` | Get all intelligence about an address across chains | address | | [POST](https://arkm.com/llms/post-intelligence-address_enriched-batch.md) | `/intelligence/address_enriched/batch` | Batch lookup enriched address intelligence | chains, chain, includeTags, includeClusters, ... | | [POST](https://arkm.com/llms/post-intelligence-address_enriched-batch-all.md) | `/intelligence/address_enriched/batch/all` | Batch lookup enriched address intelligence across all chains | includeTags, includeClusters, includeEntityPredictions | | [GET](https://arkm.com/llms/get-intelligence-address_enriched-address.md) | `/intelligence/address_enriched/{address}` | Get intelligence about an address with additional address information | address, chain, includeTags, includeClusters, ... | | [GET](https://arkm.com/llms/get-intelligence-address_enriched-address-all.md) | `/intelligence/address_enriched/{address}/all` | Get intelligence about an address on all chains with additional address information | address, includeTags, includeEntityPredictions, includeClusters | | [GET](https://arkm.com/llms/get-intelligence-address_tags-updates.md) | `/intelligence/address_tags/updates` | Get address-tag association updates | since, from, to, status, ... | | [GET](https://arkm.com/llms/get-intelligence-addresses-updates.md) | `/intelligence/addresses/updates` | Get address intelligence updates | since, from, to, status, ... | | [GET](https://arkm.com/llms/get-intelligence-contract-chain-address.md) | `/intelligence/contract/{chain}/{address}` | Get intelligence about a contract | chain, address | | [GET](https://arkm.com/llms/get-intelligence-entities-updates.md) | `/intelligence/entities/updates` | Get entity intelligence updates | since, from, to, status, ... | | [GET](https://arkm.com/llms/get-intelligence-entity-entity.md) | `/intelligence/entity/{entity}` | Get intelligence about an entity | entity | | [GET](https://arkm.com/llms/get-intelligence-entity-entity-summary.md) | `/intelligence/entity/{entity}/summary` | Get entity summary statistics | entity | | [GET](https://arkm.com/llms/get-intelligence-entity_balance_changes.md) | `/intelligence/entity_balance_changes` | Get entity balance changes | orderBy, orderDir, interval, chains, ... | | [GET](https://arkm.com/llms/get-intelligence-entity_predictions-entity.md) | `/intelligence/entity_predictions/{entity}` | Get predictions for an entity | entity | | [GET](https://arkm.com/llms/get-intelligence-entity_types.md) | `/intelligence/entity_types` | Get all entity types | | | [GET](https://arkm.com/llms/get-intelligence-search.md) | `/intelligence/search` | Search addresses, entities, tokens, and pools | query, arkhamEntities, arkhamAddresses, userEntities, ... | | [GET](https://arkm.com/llms/get-intelligence-tags-updates.md) | `/intelligence/tags/updates` | Get tag definition updates | since, from, to, status, ... | | [GET](https://arkm.com/llms/get-intelligence-token-chain-address.md) | `/intelligence/token/{chain}/{address}` | Get intelligence on a token by chain/address | chain, address | | [GET](https://arkm.com/llms/get-intelligence-token-id.md) | `/intelligence/token/{id}` | Get intelligence on a token by CoinGecko pricing ID | id | | [GET](https://arkm.com/llms/get-loans-address-address.md) | `/loans/address/{address}` | Get loan/borrow positions for an address | address, chains | | [GET](https://arkm.com/llms/get-loans-entity-entity.md) | `/loans/entity/{entity}` | Get loan/borrow positions for an entity | entity, chains | | [GET](https://arkm.com/llms/get-marketdata-altcoin_index.md) | `/marketdata/altcoin_index` | Get Altcoin Index | | | [GET](https://arkm.com/llms/get-networks-history-chain.md) | `/networks/history/{chain}` | Get historical data for a blockchain network | chain | | [GET](https://arkm.com/llms/get-networks-status.md) | `/networks/status` | Get current status for all blockchain networks | | | [GET](https://arkm.com/llms/get-polymarket-activity.md) | `/polymarket/activity` | Polymarket activity feed | userAddresses, entityIDs, conditionID, eventID, ... | | [GET](https://arkm.com/llms/get-polymarket-event-positions-conditionId.md) | `/polymarket/event-positions/{conditionId}` | Polymarket positions in a market | conditionId, tokenAddresses, userAddresses, outcome, ... | | [GET](https://arkm.com/llms/get-polymarket-events.md) | `/polymarket/events` | List Polymarket events | tag, excludeTag, active, search, ... | | [GET](https://arkm.com/llms/get-polymarket-events-eventId.md) | `/polymarket/events/{eventId}` | Get a Polymarket event | eventId | | [GET](https://arkm.com/llms/get-polymarket-leaderboard.md) | `/polymarket/leaderboard` | Polymarket PnL leaderboard | period, order, limit, offset, ... | | [GET](https://arkm.com/llms/get-polymarket-order-book-conditionId.md) | `/polymarket/order-book/{conditionId}` | Polymarket order book | conditionId | | [GET](https://arkm.com/llms/get-polymarket-pnl-chart.md) | `/polymarket/pnl/chart` | Polymarket PnL chart | userAddress, conditionID, period, startTime, ... | | [GET](https://arkm.com/llms/get-polymarket-positions-addr.md) | `/polymarket/positions/{addr}` | Polymarket user positions | addr, sortBy, sortOrder, limit, ... | | [GET](https://arkm.com/llms/get-polymarket-prices.md) | `/polymarket/prices` | Polymarket price history | tokenAddress, conditionID, interval, range, ... | | [GET](https://arkm.com/llms/get-polymarket-stats.md) | `/polymarket/stats` | Polymarket platform stats | | | [GET](https://arkm.com/llms/get-polymarket-top-events.md) | `/polymarket/top-events` | Polymarket top events leaderboard | period, order, limit, offset | | [GET](https://arkm.com/llms/get-polymarket-top-events-eventId-breakdown.md) | `/polymarket/top-events/{eventId}/breakdown` | Polymarket top event breakdown | eventId, address, period | | [GET](https://arkm.com/llms/get-polymarket-top-holders-conditionId.md) | `/polymarket/top-holders/{conditionId}` | Polymarket market top holders | conditionId, limit, outcome | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-event-history.md) | `/polymarket/wallet/{addr}/event-history` | Polymarket wallet event history | addr, limit, offset, sort, ... | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-prediction-history.md) | `/polymarket/wallet/{addr}/prediction-history` | Polymarket prediction history | addr, limit, offset, sort, ... | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-balance.md) | `/polymarket/wallet/{addr}/summary/balance` | Polymarket wallet collateral balances | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-biggest-win.md) | `/polymarket/wallet/{addr}/summary/biggest-win` | Polymarket wallet biggest win | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-pnl.md) | `/polymarket/wallet/{addr}/summary/pnl` | Polymarket wallet PnL summary | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-portfolio.md) | `/polymarket/wallet/{addr}/summary/portfolio` | Polymarket wallet portfolio | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-rewards.md) | `/polymarket/wallet/{addr}/summary/rewards` | Polymarket wallet rewards | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-summary-stats.md) | `/polymarket/wallet/{addr}/summary/stats` | Polymarket wallet trading stats | addr | | [GET](https://arkm.com/llms/get-polymarket-wallet-addr-tags.md) | `/polymarket/wallet/{addr}/tags` | | addr, limit | | [GET](https://arkm.com/llms/get-portfolio-address-address.md) | `/portfolio/address/{address}` | Get address portfolio history | address, time, chains | | [GET](https://arkm.com/llms/get-portfolio-entity-entity.md) | `/portfolio/entity/{entity}` | Get entity portfolio history | entity, time, chains | | [GET](https://arkm.com/llms/get-portfolio-timeSeries-address-address.md) | `/portfolio/timeSeries/address/{address}` | Get daily time series data for an address's token | address, pricingId, chains | | [GET](https://arkm.com/llms/get-portfolio-timeSeries-entity-entity.md) | `/portfolio/timeSeries/entity/{entity}` | Get daily time series data for an entity's token | entity, pricingId, chains | | [POST](https://arkm.com/llms/post-risk-address-batch.md) | `/risk/address/batch` | Get risk scores for multiple addresses in one request | | | [GET](https://arkm.com/llms/get-risk-address-address.md) | `/risk/address/{address}` | Get risk score for an address | address | | [GET](https://arkm.com/llms/get-risk-address-address-paths.md) | `/risk/address/{address}/paths` | Get the transaction paths behind an address's risk score | address | | [POST](https://arkm.com/llms/post-risk-entity-batch.md) | `/risk/entity/batch` | Get risk scores for multiple entities in one request | | | [GET](https://arkm.com/llms/get-risk-entity-entity_id.md) | `/risk/entity/{entity_id}` | Get entity-level risk score | entity_id | | [GET](https://arkm.com/llms/get-subscription-intel-usage.md) | `/subscription/intel-usage` | Get intel data usage for the current billing period | | | [GET](https://arkm.com/llms/get-swaps.md) | `/swaps` | Get swaps | base, chains, flow, tokens, ... | | [GET](https://arkm.com/llms/get-tag-id-params.md) | `/tag/{id}/params` | Get tag parameters | id, limit, offset | | [GET](https://arkm.com/llms/get-tag-id-summary.md) | `/tag/{id}/summary` | Get tag summary statistics | id | | [GET](https://arkm.com/llms/get-token-addresses-id.md) | `/token/addresses/{id}` | Get chain addresses for a token | id | | [GET](https://arkm.com/llms/get-token-arkham_exchange_tokens.md) | `/token/arkham_exchange_tokens` | Get Arkham Exchange tokens | | | [GET](https://arkm.com/llms/get-token-balance-chain-address.md) | `/token/balance/{chain}/{address}` | Get token balance for an entity or address, for a specific chain/address | chain, address, entityID, address | | [GET](https://arkm.com/llms/get-token-balance-id.md) | `/token/balance/{id}` | Get token balance (all chains) for an entity or address | id, entityID, address | | [GET](https://arkm.com/llms/get-token-holders-chain-address.md) | `/token/holders/{chain}/{address}` | Get top token holders by chain and address | chain, address, groupByEntity, limit, ... | | [GET](https://arkm.com/llms/get-token-holders-id.md) | `/token/holders/{id}` | Get top token holders by pricing ID | id, groupByEntity, limit, offset, ... | | [GET](https://arkm.com/llms/get-token-market-id.md) | `/token/market/{id}` | Get current market data for a token | id | | [GET](https://arkm.com/llms/get-token-price-history-chain-address.md) | `/token/price/history/{chain}/{address}` | Get token price history by chain and address | chain, address, daily | | [GET](https://arkm.com/llms/get-token-price-history-id.md) | `/token/price/history/{id}` | Get token price history by pricing ID | id, daily | | [GET](https://arkm.com/llms/get-token-price_change-id.md) | `/token/price_change/{id}` | Get token price change since a timestamp | id, pastTime | | [GET](https://arkm.com/llms/get-token-top.md) | `/token/top` | Get top tokens by exchange activity | timeframe, orderByAgg, orderByDesc, orderByPercent, ... | | [GET](https://arkm.com/llms/get-token-top_flow-chain-address.md) | `/token/top_flow/{chain}/{address}` | Get top token flow | chain, address, timeLast, limit, ... | | [GET](https://arkm.com/llms/get-token-top_flow-id.md) | `/token/top_flow/{id}` | Get top flow for a token by pricing ID | id, timeLast, limit, chains | | [GET](https://arkm.com/llms/get-token-trending.md) | `/token/trending` | Get trending tokens | | | [GET](https://arkm.com/llms/get-token-trending-id.md) | `/token/trending/{id}` | Get a single trending token by ID | id | | [GET](https://arkm.com/llms/get-token-volume-chain-address.md) | `/token/volume/{chain}/{address}` | Get volume for a token by chain/address | chain, address, granularity, timeLast | | [GET](https://arkm.com/llms/get-token-volume-id.md) | `/token/volume/{id}` | Get volume for a token by pricing ID | id, granularity, timeLast | | [GET](https://arkm.com/llms/get-transfers.md) | `/transfers` | Get transfers | base, chains, flow, from, ... | | [GET](https://arkm.com/llms/get-transfers-histogram.md) | `/transfers/histogram` | Get a detailed histogram of transfers (API Only) | base, chains, flow, from, ... | | [GET](https://arkm.com/llms/get-transfers-tx-hash.md) | `/transfers/tx/{hash}` | Get transfers for a transaction | hash, transferType, chain | | [GET](https://arkm.com/llms/get-transfers-unenriched.md) | `/transfers/unenriched` | Get unenriched transfers | base, chains, flow, from, ... | | [GET](https://arkm.com/llms/get-tx-hash.md) | `/tx/{hash}` | Get transaction details | hash | | [GET](https://arkm.com/llms/get-user-alerts.md) | `/user/alerts` | List your alerts | | | [POST](https://arkm.com/llms/post-user-alerts.md) | `/user/alerts` | Create an alert | | | [GET](https://arkm.com/llms/get-user-alerts-id.md) | `/user/alerts/{id}` | Get a single alert | id | | [PUT](https://arkm.com/llms/put-user-alerts-id.md) | `/user/alerts/{id}` | Update an alert | id | | [DELETE](https://arkm.com/llms/delete-user-alerts-id.md) | `/user/alerts/{id}` | Delete an alert | id | | [GET](https://arkm.com/llms/get-user-entities.md) | `/user/entities` | List all private entities | includeAddresses | | [POST](https://arkm.com/llms/post-user-entities.md) | `/user/entities` | Create a custom entity | | | [PUT](https://arkm.com/llms/put-user-entities-only_add-id.md) | `/user/entities/only_add/{id}` | Add addresses to a custom entity | id | | [GET](https://arkm.com/llms/get-user-entities-id.md) | `/user/entities/{id}` | Get a private entity by ID | id | | [PUT](https://arkm.com/llms/put-user-entities-id.md) | `/user/entities/{id}` | Replace a custom entity | id | | [DELETE](https://arkm.com/llms/delete-user-entities-id.md) | `/user/entities/{id}` | Delete a custom entity | id | | [GET](https://arkm.com/llms/get-user-labels.md) | `/user/labels` | Get user labels | | | [POST](https://arkm.com/llms/post-user-labels.md) | `/user/labels` | Create labels | | | [DELETE](https://arkm.com/llms/delete-user-labels-chainType-address.md) | `/user/labels/{chainType}/{address}` | Delete a label | chainType, address | | [GET](https://arkm.com/llms/get-volume-address-address.md) | `/volume/address/{address}` | Get exchange and service volume for an address | address, chains | | [GET](https://arkm.com/llms/get-volume-entity-entity.md) | `/volume/entity/{entity}` | Get exchange and service volume for an entity | entity, chains | | [GET](https://arkm.com/llms/get-ws-active_connections.md) | `/ws/active_connections` | Get active WebSocket connections (deprecated — use v2) | | | [GET](https://arkm.com/llms/get-ws-session-info.md) | `/ws/session-info` | Get WebSocket pricing info (deprecated — use v2) | | | [GET](https://arkm.com/llms/get-ws-sessions.md) | `/ws/sessions` | List WebSocket sessions (deprecated — use v2) | | | [POST](https://arkm.com/llms/post-ws-sessions.md) | `/ws/sessions` | Create WebSocket session (deprecated — use v2) | | | [GET](https://arkm.com/llms/get-ws-sessions-id.md) | `/ws/sessions/{id}` | Get WebSocket session status (deprecated — use v2) | id | | [DELETE](https://arkm.com/llms/delete-ws-sessions-id.md) | `/ws/sessions/{id}` | Delete WebSocket session (deprecated — use v2) | id | | [GET](https://arkm.com/llms/get-ws-transfers.md) | `/ws/transfers` | WebSocket transfer streaming (deprecated — use v2) | | | [GET](https://arkm.com/llms/get-ws-v2-streams.md) | `/ws/v2/streams` | List WebSocket v2 streams | | | [POST](https://arkm.com/llms/post-ws-v2-streams.md) | `/ws/v2/streams` | Create a WebSocket v2 stream | | | [DELETE](https://arkm.com/llms/delete-ws-v2-streams-id.md) | `/ws/v2/streams/{id}` | Delete a WebSocket v2 stream | id | | [GET](https://arkm.com/llms/get-ws-v2-transfers.md) | `/ws/v2/transfers` | WebSocket v2 transfer streaming | | --- # Code Examples ## Bash — arkham-cli ```bash #!/bin/bash # ============================================================================ # Arkham API CLI Demo # ============================================================================ # A command-line tool demonstrating Arkham Intelligence API capabilities # including REST endpoints and WebSocket streaming. # # Prerequisites: # - curl (for REST API calls) # - websocat (for WebSocket streaming) - https://github.com/vi/websocat # - jq (optional, for pretty JSON output) # # Usage: # export ARKHAM_API_KEY="your-api-key" # ./arkham-cli.sh [options] # # ============================================================================ set -e # Configuration ARKHAM_API_KEY="${ARKHAM_API_KEY:-}" ARKHAM_BASE_URL="${ARKHAM_BASE_URL:-https://api.arkm.com}" ARKHAM_WS_URL="${ARKHAM_WS_URL:-wss://api.arkm.com/ws/v2/transfers}" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================================================ # Helper Functions # ============================================================================ print_error() { echo -e "${RED}Error:${NC} $1" >&2 } print_success() { echo -e "${GREEN}$1${NC}" } print_info() { echo -e "${BLUE}$1${NC}" } print_warning() { echo -e "${YELLOW}$1${NC}" } check_api_key() { if [ -z "$ARKHAM_API_KEY" ]; then print_error "ARKHAM_API_KEY environment variable is not set." echo "" echo "Please set your API key:" echo " export ARKHAM_API_KEY=\"your-api-key-here\"" echo "" exit 1 fi } check_dependency() { if ! command -v "$1" &> /dev/null; then print_error "$1 is required but not installed." echo "" case "$1" in websocat) echo "Install websocat:" echo " macOS: brew install websocat" echo " Linux: cargo install websocat" echo " Or download from: https://github.com/vi/websocat/releases" ;; jq) echo "Install jq (optional, for pretty JSON):" echo " macOS: brew install jq" echo " Linux: apt-get install jq" ;; *) echo "Please install $1 to continue." ;; esac exit 1 fi } # Make API request api_request() { local endpoint="$1" local method="${2:-GET}" local response response=$(curl -s -w "\n%{http_code}" \ -X "$method" \ -H "API-Key: $ARKHAM_API_KEY" \ -H "Content-Type: application/json" \ "${ARKHAM_BASE_URL}${endpoint}") local http_code http_code=$(echo "$response" | tail -n1) local body body=$(echo "$response" | sed '$d') if [ "$http_code" -ge 400 ]; then print_error "API request failed (HTTP $http_code)" echo "$body" return 1 fi # Pretty print if jq is available if command -v jq &> /dev/null; then echo "$body" | jq . else echo "$body" fi } # ============================================================================ # Commands # ============================================================================ cmd_help() { cat << 'EOF' Arkham API CLI Demo Usage: ./arkham-cli.sh [options] Commands: test Test API connection (health check) chains List supported blockchains address Get intelligence for an address transfers [options] Get recent transfers counterparties Get counterparties for an address ws-stream [options] Stream real-time transfers via WebSocket v2 help Show this help message Transfer Options: --chain Filter by blockchain (e.g., ethereum) --usd-gte Minimum USD value --limit Number of results (default: 10) WebSocket Options: --usd-gte Minimum USD value (default: 10000000, kept high so a demo run does not spend many credits). Must be >= 250000 unless using --from, --to, or --tokens --chains Filter by chains (comma-separated) --from Source addresses or entity names (comma-separated) --to Destination addresses or entity names (comma-separated) --tokens CoinGecko pricing IDs (e.g. usd-coin) or contract addresses (comma-separated) Examples: ./arkham-cli.sh test ./arkham-cli.sh address 0x28C6c06298d514Db089934071355E5743bf21d60 ./arkham-cli.sh transfers --chain ethereum --usd-gte 100000 ./arkham-cli.sh counterparties 0x28C6c06298d514Db089934071355E5743bf21d60 ./arkham-cli.sh ws-stream ./arkham-cli.sh ws-stream --from binance --usd-gte 100000 ./arkham-cli.sh ws-stream --tokens tether,usd-coin --chains ethereum Environment Variables: ARKHAM_API_KEY Your Arkham API key (required) ARKHAM_BASE_URL API base URL (default: https://api.arkm.com) ARKHAM_WS_URL WebSocket URL (default: wss://api.arkm.com/ws/v2/transfers) EOF } cmd_test() { print_info "Testing API connection..." check_api_key # Test with chains endpoint (requires auth) local response response=$(curl -s -w "\n%{http_code}" \ -H "API-Key: $ARKHAM_API_KEY" \ "${ARKHAM_BASE_URL}/chains") local http_code http_code=$(echo "$response" | tail -n1) local body body=$(echo "$response" | sed '$d') if [ "$http_code" -eq 200 ]; then print_success "Connection successful!" echo "" echo "Supported chains:" if command -v jq &> /dev/null; then echo "$body" | jq -r '.[]' else echo "$body" fi else print_error "Connection failed (HTTP $http_code)" echo "$body" exit 1 fi } cmd_chains() { print_info "Fetching supported chains..." check_api_key api_request "/chains" } cmd_address() { local address="$1" if [ -z "$address" ]; then print_error "Address is required." echo "Usage: ./arkham-cli.sh address
" exit 1 fi print_info "Fetching intelligence for address: $address" check_api_key api_request "/intelligence/address/$address" } cmd_transfers() { local chain="" local usd_gte="" local limit="10" # Parse options while [[ $# -gt 0 ]]; do case "$1" in --chain) chain="$2" shift 2 ;; --usd-gte) usd_gte="$2" shift 2 ;; --limit) limit="$2" shift 2 ;; *) shift ;; esac done # Build query parameters local params="limit=$limit" # Default to Binance address for demo local base_addr="0x28C6c06298d514Db089934071355E5743bf21d60" params="$params&base=$base_addr" # Add time filter (last 1 hour by default - use shorter window for reliability) params="$params&timeLast=1h" if [ -n "$chain" ]; then params="$params&chains=$chain" fi if [ -n "$usd_gte" ]; then params="$params&usdGte=$usd_gte" fi print_info "Fetching transfers..." echo " Base: $base_addr (Binance)" echo " Time: Last 1 hour" [ -n "$chain" ] && echo " Chain: $chain" [ -n "$usd_gte" ] && echo " Min USD: \$$usd_gte" echo "" check_api_key api_request "/transfers?$params" } cmd_counterparties() { local address="$1" shift 2>/dev/null || true if [ -z "$address" ]; then print_error "Address is required." echo "Usage: ./arkham-cli.sh counterparties
" exit 1 fi print_info "Fetching counterparties for: $address" echo "" check_api_key api_request "/counterparties/address/$address?limit=20" } cmd_ws_stream() { local usd_gte="10000000" local chains="" local from_filter="" local to_filter="" local tokens="" # Parse options while [[ $# -gt 0 ]]; do case "$1" in --usd-gte) usd_gte="$2" shift 2 ;; --chains) chains="$2" shift 2 ;; --from) from_filter="$2" shift 2 ;; --to) to_filter="$2" shift 2 ;; --tokens) tokens="$2" shift 2 ;; *) shift ;; esac done # Validate filter requirements local has_address_filter=false [[ -n "$from_filter" || -n "$to_filter" || -n "$tokens" ]] && has_address_filter=true if [ "$has_address_filter" = "false" ] && [ "$usd_gte" -lt 250000 ]; then print_error "Filter requirement not met!" echo "" echo "WebSocket filters must include at least one of:" echo " - --from (source addresses/entities)" echo " - --to (destination addresses/entities)" echo " - --tokens (CoinGecko pricing IDs/addresses)" echo " - --usd-gte >= 250000 (250 thousand USD)" echo "" exit 1 fi check_api_key check_dependency websocat print_info "Creating stream and connecting..." echo " URL: $ARKHAM_WS_URL" echo " Min USD: \$$usd_gte" [ -n "$chains" ] && echo " Chains: $chains" [ -n "$from_filter" ] && echo " From: $from_filter" [ -n "$to_filter" ] && echo " To: $to_filter" [ -n "$tokens" ] && echo " Tokens: $tokens" echo "" print_warning "Press Ctrl+C to stop streaming" print_info "Note: Initial transfers may take up to 90 seconds after subscribing" echo "" # Build filter JSON local filter_json="{\"usdGte\":$usd_gte" if [ -n "$chains" ]; then local chains_array chains_array=$(echo "$chains" | sed 's/,/","/g') filter_json="$filter_json,\"chains\":[\"$chains_array\"]" fi if [ -n "$from_filter" ]; then local from_array from_array=$(echo "$from_filter" | sed 's/,/","/g') filter_json="$filter_json,\"from\":[\"$from_array\"]" fi if [ -n "$to_filter" ]; then local to_array to_array=$(echo "$to_filter" | sed 's/,/","/g') filter_json="$filter_json,\"to\":[\"$to_array\"]" fi if [ -n "$tokens" ]; then local tokens_array tokens_array=$(echo "$tokens" | sed 's/,/","/g') filter_json="$filter_json,\"tokens\":[\"$tokens_array\"]" fi filter_json="$filter_json}" print_info "Creating stream with filter:" echo "$filter_json" | (command -v jq &> /dev/null && jq . || cat) echo "" # v2 sets the filter when the stream is created, so the socket carries no # client messages — connect with the returned streamId and read. local create_response stream_id create_response=$(curl -s -X POST \ -H "API-Key: $ARKHAM_API_KEY" \ -H "Content-Type: application/json" \ -d "$filter_json" \ "${ARKHAM_BASE_URL}/ws/v2/streams") stream_id=$(echo "$create_response" | sed -n 's/.*"streamId":"\([^"]*\)".*/\1/p') if [ -z "$stream_id" ]; then print_error "Could not create stream:" echo "$create_response" exit 1 fi print_success "Stream created: $stream_id" echo "---" echo "" websocat --header="API-Key:$ARKHAM_API_KEY" "${ARKHAM_WS_URL}?stream_id=${stream_id}" | while read -r line; do if command -v jq &> /dev/null; then echo "$line" | jq . else echo "$line" fi echo "---" done } # ============================================================================ # Main # ============================================================================ main() { local command="${1:-help}" shift 2>/dev/null || true case "$command" in test) cmd_test ;; chains) cmd_chains ;; address) cmd_address "$@" ;; transfers) cmd_transfers "$@" ;; counterparties) cmd_counterparties "$@" ;; ws-stream|ws|stream) cmd_ws_stream "$@" ;; help|--help|-h) cmd_help ;; *) print_error "Unknown command: $command" echo "" cmd_help exit 1 ;; esac } main "$@" ``` ## Python — arkham demo ```python #!/usr/bin/env python3 """ Arkham API Demo - WebSocket & REST Examples A demonstration tool showcasing Arkham Intelligence API capabilities including REST endpoints and real-time WebSocket streaming. Prerequisites: pip install websockets requests Usage: export ARKHAM_API_KEY="your-api-key" python arkham_demo.py [options] Examples: python arkham_demo.py test python arkham_demo.py chains python arkham_demo.py address 0x28C6c06298d514Db089934071355E5743bf21d60 python arkham_demo.py transfers --chain ethereum --usd-gte 100000 python arkham_demo.py counterparties 0x28C6c06298d514Db089934071355E5743bf21d60 python arkham_demo.py ws-stream --usd-gte 1000000 --chains ethereum """ import argparse import asyncio import json import os import signal import sys from datetime import datetime, timedelta, timezone from typing import Any, Dict, List, Optional # Check for optional dependencies try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False try: import websockets HAS_WEBSOCKETS = True except ImportError: HAS_WEBSOCKETS = False # ============================================================================ # Configuration # ============================================================================ DEFAULT_BASE_URL = "https://api.arkm.com" DEFAULT_WS_URL = "wss://api.arkm.com/ws/transfers" # Example addresses for demos EXAMPLE_ADDRESSES = { "binance": "0x28C6c06298d514Db089934071355E5743bf21d60", "vitalik": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", } # ============================================================================ # Utilities # ============================================================================ class Colors: """ANSI color codes for terminal output.""" RED = "\033[0;31m" GREEN = "\033[0;32m" YELLOW = "\033[1;33m" BLUE = "\033[0;34m" RESET = "\033[0m" @classmethod def disable(cls): """Disable colors (for non-TTY output).""" cls.RED = cls.GREEN = cls.YELLOW = cls.BLUE = cls.RESET = "" # Disable colors if not a TTY if not sys.stdout.isatty(): Colors.disable() def print_error(msg: str) -> None: """Print error message in red.""" print(f"{Colors.RED}Error:{Colors.RESET} {msg}", file=sys.stderr) def print_success(msg: str) -> None: """Print success message in green.""" print(f"{Colors.GREEN}{msg}{Colors.RESET}") def print_info(msg: str) -> None: """Print info message in blue.""" print(f"{Colors.BLUE}{msg}{Colors.RESET}") def print_warning(msg: str) -> None: """Print warning message in yellow.""" print(f"{Colors.YELLOW}{msg}{Colors.RESET}") def print_json(data: Any) -> None: """Pretty print JSON data.""" print(json.dumps(data, indent=2)) def get_api_key() -> str: """Get API key from environment or exit with error.""" api_key = os.environ.get("ARKHAM_API_KEY", "") if not api_key: print_error("ARKHAM_API_KEY environment variable is not set.") print("") print("Please set your API key:") print(' export ARKHAM_API_KEY="your-api-key-here"') print("") sys.exit(1) return api_key def check_requests() -> None: """Check if requests library is available.""" if not HAS_REQUESTS: print_error("requests library is required for REST API calls.") print("") print("Install it with:") print(" pip install requests") print("") sys.exit(1) def check_websockets() -> None: """Check if websockets library is available.""" if not HAS_WEBSOCKETS: print_error("websockets library is required for WebSocket streaming.") print("") print("Install it with:") print(" pip install websockets") print("") sys.exit(1) # ============================================================================ # REST API Client # ============================================================================ class ArkhamAPI: """REST API client for Arkham Intelligence.""" def __init__(self, api_key: str, base_url: str = DEFAULT_BASE_URL): """Initialize the API client.""" check_requests() self.api_key = api_key self.base_url = base_url.rstrip("/") self.session = requests.Session() self.session.headers.update({ "API-Key": api_key, "Content-Type": "application/json", }) def _request(self, endpoint: str, method: str = "GET", **kwargs) -> Dict[str, Any]: """Make an API request.""" url = f"{self.base_url}{endpoint}" response = self.session.request(method, url, **kwargs) if response.status_code >= 400: print_error(f"API request failed (HTTP {response.status_code})") try: print_json(response.json()) except json.JSONDecodeError: print(response.text) sys.exit(1) return response.json() def test_connection(self) -> List[str]: """Test API connection by fetching supported chains.""" return self._request("/chains") def get_chains(self) -> List[str]: """Get list of supported blockchains.""" return self._request("/chains") def get_address(self, address: str) -> Dict[str, Any]: """Get intelligence for an address.""" return self._request(f"/intelligence/address/{address}") def get_transfers( self, base: Optional[str] = None, chain: Optional[str] = None, usd_gte: Optional[int] = None, time_last: str = "1h", limit: int = 10, ) -> Dict[str, Any]: """Get transfers with optional filters.""" params = {"limit": limit, "timeLast": time_last} if base: params["base"] = base if chain: params["chains"] = chain if usd_gte: params["usdGte"] = usd_gte return self._request("/transfers", params=params) def get_counterparties( self, address: str, limit: int = 20, ) -> Dict[str, Any]: """Get counterparties for an address.""" params = {"limit": limit} return self._request(f"/counterparties/address/{address}", params=params) # ============================================================================ # WebSocket Client # ============================================================================ class ArkhamWebSocket: """WebSocket streaming client for Arkham Intelligence.""" def __init__(self, api_key: str, ws_url: str = DEFAULT_WS_URL): """Initialize the WebSocket client.""" check_websockets() self.api_key = api_key self.ws_url = ws_url self.websocket = None self.session_id: Optional[str] = None self.filter_id: Optional[int] = None self._running = True self._message_id = 0 def _next_message_id(self) -> str: """Generate next message ID.""" self._message_id += 1 return str(self._message_id) def create_session(self) -> str: """Create a prepaid session (REST). v1 requires a session_id to connect.""" check_requests() resp = requests.post( f"{DEFAULT_BASE_URL}/ws/sessions", headers={"API-Key": self.api_key}, timeout=30, ) resp.raise_for_status() self.session_id = resp.json()["sessionId"] print_success(f"Created session: {self.session_id}") return self.session_id async def connect(self) -> None: """Connect to the WebSocket server (creates a session if needed).""" if not self.session_id: self.create_session() sep = "&" if "?" in self.ws_url else "?" url = f"{self.ws_url}{sep}session_id={self.session_id}" headers = {"API-Key": self.api_key} self.websocket = await websockets.connect( url, additional_headers=headers, ping_interval=30, ping_timeout=10, ) print_success("Connected to WebSocket") async def subscribe(self, filters: Dict[str, Any]) -> Dict[str, Any]: """Subscribe to transfer notifications with filters.""" if not self.websocket: raise RuntimeError("Not connected. Call connect() first.") message = { "id": self._next_message_id(), "type": "subscribe", "payload": {"filters": filters}, } print_info("Sending subscription:") print_json(message) print("") await self.websocket.send(json.dumps(message)) # Wait for acknowledgment response = await self.websocket.recv() data = json.loads(response) # ACK structure: {"type": "ack", "payload": {"success": true, "data": {"filterId": X, "sessionId": "..."}}} payload = data.get("payload", {}) if data.get("type") == "ack" and payload.get("success"): ack_data = payload.get("data", {}) self.session_id = ack_data.get("sessionId") self.filter_id = ack_data.get("filterId") print_success(f"Subscribed successfully!") print(f" Session ID: {self.session_id}") print(f" Filter ID: {self.filter_id}") print("") return data else: print_error("Subscription failed:") print_json(data) raise RuntimeError("Subscription failed") async def stream(self) -> None: """Stream transfer notifications, reconnecting until stopped.""" if not self.websocket: raise RuntimeError("Not connected. Call connect() first.") print_info("Streaming transfers (Press Ctrl+C to stop)...") print_info("Note: Initial transfers may take up to 90 seconds after subscribing") print("---") while self._running: if await self._stream_once(): break # The session keeps its filter across reconnects, so reconnect only — # re-subscribing would be refused with FILTER_EXISTS. await asyncio.sleep(1) try: await self.connect() except (websockets.WebSocketException, OSError) as e: print_warning(f"Reconnect failed: {e}") async def _stream_once(self) -> bool: """Read until the connection drops. True means stop, False means reconnect.""" try: async for message in self.websocket: if not self._running: break data = json.loads(message) msg_type = data.get("type", "unknown") payload = data.get("payload", {}) if msg_type == "transfer": self._print_transfer(payload) elif msg_type == "error": error_code = payload.get("code", "UNKNOWN") error_msg = payload.get("message", "Unknown error") print_error(f"Server error [{error_code}]: {error_msg}") if error_code == "INSUFFICIENT_CREDITS": print_warning("You've run out of WebSocket credits.") break elif error_code == "TIER_RATE_LIMITED": reset_in = payload.get("resetIn", "?") print_warning(f"Rate limited. Resets in {reset_in} seconds.") elif msg_type == "ack": # Already handled in subscribe, but print if we get another print_info("Received ACK") print_json(data) print("---") else: # Print any other message types print_json(data) print("---") except (websockets.WebSocketException, OSError) as e: # Catch the library's whole error tree, not just ConnectionClosed: a # failed reconnect raises InvalidStatus, and a network failure raises # OSError. Both must keep the loop alive. print_warning(f"Connection lost: {e}") return False return not self._running def _print_transfer(self, payload: Dict[str, Any]) -> None: """Pretty print a transfer notification. Payload structure from docs: { "transfer": { "fromAddress": {"address": "...", "arkhamEntity": {...}, "arkhamLabel": {...}}, "toAddress": {"address": "...", ...}, "tokenSymbol": "USDT", "historicalUSD": 503.9, "unitValue": 503.9, "chain": "ethereum", "transactionHash": "0x...", "blockTimestamp": "2025-08-28T11:01:35Z" }, "alertId": 49 } """ transfer = payload.get("transfer", {}) alert_id = payload.get("alertId") from_addr = transfer.get("fromAddress", {}) to_addr = transfer.get("toAddress", {}) # Get labels - prefer entity name, then label name, then truncated address from_entity = from_addr.get("arkhamEntity", {}) from_label_obj = from_addr.get("arkhamLabel", {}) from_label = from_entity.get("name") or from_label_obj.get("name") or from_addr.get("address", "Unknown")[:16] + "..." to_entity = to_addr.get("arkhamEntity", {}) to_label_obj = to_addr.get("arkhamLabel", {}) to_label = to_entity.get("name") or to_label_obj.get("name") or to_addr.get("address", "Unknown")[:16] + "..." # USD value is in historicalUSD field usd_value = transfer.get("historicalUSD", 0) unit_value = transfer.get("unitValue", 0) token_symbol = transfer.get("tokenSymbol", "???") chain = transfer.get("chain", "unknown") tx_hash = transfer.get("transactionHash", "N/A") timestamp = transfer.get("blockTimestamp", "") print(f"{Colors.GREEN}Transfer:{Colors.RESET} {from_label} -> {to_label}") print(f" Value: ${usd_value:,.2f} ({unit_value:,.4f} {token_symbol})") print(f" Chain: {chain}") print(f" Time: {timestamp}") print(f" Hash: {tx_hash[:42]}...") if alert_id: print(f" Alert ID: {alert_id}") print("---") async def disconnect(self) -> None: """Disconnect from the WebSocket server.""" self._running = False if self.websocket: await self.websocket.close() print_info("Disconnected from WebSocket") # ============================================================================ # Command Handlers # ============================================================================ def cmd_test(args: argparse.Namespace) -> None: """Test API connection.""" print_info("Testing API connection...") api_key = get_api_key() api = ArkhamAPI(api_key) chains = api.test_connection() print_success("Connection successful!") print("") print("Supported chains:") for chain in chains: print(f" - {chain}") def cmd_chains(args: argparse.Namespace) -> None: """List supported chains.""" print_info("Fetching supported chains...") api_key = get_api_key() api = ArkhamAPI(api_key) chains = api.get_chains() print_json(chains) def cmd_address(args: argparse.Namespace) -> None: """Get address intelligence.""" print_info(f"Fetching intelligence for: {args.address}") api_key = get_api_key() api = ArkhamAPI(api_key) data = api.get_address(args.address) print_json(data) def cmd_transfers(args: argparse.Namespace) -> None: """Get transfers with filters.""" api_key = get_api_key() api = ArkhamAPI(api_key) # Default to Binance address for demo base = args.base or EXAMPLE_ADDRESSES["binance"] print_info("Fetching transfers...") print(f" Base: {base}") print(f" Time: Last 1 hour") if args.chain: print(f" Chain: {args.chain}") if args.usd_gte: print(f" Min USD: ${args.usd_gte:,}") print(f" Limit: {args.limit}") print("") data = api.get_transfers( base=base, chain=args.chain, usd_gte=args.usd_gte, limit=args.limit, ) print_json(data) def cmd_counterparties(args: argparse.Namespace) -> None: """Get counterparties for an address.""" api_key = get_api_key() api = ArkhamAPI(api_key) print_info(f"Fetching counterparties for: {args.address}") print("") data = api.get_counterparties(address=args.address) print_json(data) def cmd_ws_stream(args: argparse.Namespace) -> None: """Stream transfers via WebSocket.""" api_key = get_api_key() # Build filters filters: Dict[str, Any] = {"usdGte": args.usd_gte} if args.chains: filters["chains"] = [c.strip() for c in args.chains.split(",")] if args.from_filter: filters["from"] = [f.strip() for f in args.from_filter.split(",")] if args.to: filters["to"] = [t.strip() for t in args.to.split(",")] if args.tokens: filters["tokens"] = [t.strip() for t in args.tokens.split(",")] # Validate filter requirements has_address_filter = any(k in filters for k in ["from", "to", "tokens"]) if not has_address_filter and args.usd_gte < 10000000: print_error("Filter requirement not met!") print("") print("WebSocket filters must include at least one of:") print(" - --from (source addresses/entities)") print(" - --to (destination addresses/entities)") print(" - --tokens (token IDs/addresses)") print(" - --usd-gte >= 10000000 (10 million USD)") print("") sys.exit(1) print_info("WebSocket Configuration:") print(f" URL: {DEFAULT_WS_URL}") print(f" Min USD: ${args.usd_gte:,}") if args.chains: print(f" Chains: {args.chains}") if args.from_filter: print(f" From: {args.from_filter}") if args.to: print(f" To: {args.to}") if args.tokens: print(f" Tokens: {args.tokens}") print("") async def run_stream(): ws = ArkhamWebSocket(api_key) # Handle Ctrl+C gracefully def signal_handler(sig, frame): print("") print_warning("Interrupt received, disconnecting...") ws._running = False signal.signal(signal.SIGINT, signal_handler) try: await ws.connect() await ws.subscribe(filters) await ws.stream() except Exception as e: print_error(f"WebSocket error: {e}") finally: await ws.disconnect() asyncio.run(run_stream()) # ============================================================================ # Main # ============================================================================ def main() -> None: """Main entry point.""" parser = argparse.ArgumentParser( description="Arkham API Demo - WebSocket & REST Examples", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: python arkham_demo.py test python arkham_demo.py chains python arkham_demo.py address 0x28C6c06298d514Db089934071355E5743bf21d60 python arkham_demo.py transfers --chain ethereum --usd-gte 100000 python arkham_demo.py counterparties 0x28C6c06298d514Db089934071355E5743bf21d60 --days 30 python arkham_demo.py ws-stream --usd-gte 1000000 --chains ethereum Environment Variables: ARKHAM_API_KEY Your Arkham API key (required) """, ) subparsers = parser.add_subparsers(dest="command", help="Available commands") # test command subparsers.add_parser("test", help="Test API connection") # chains command subparsers.add_parser("chains", help="List supported blockchains") # address command address_parser = subparsers.add_parser("address", help="Get intelligence for an address") address_parser.add_argument("address", help="Blockchain address to look up") # transfers command transfers_parser = subparsers.add_parser("transfers", help="Get recent transfers") transfers_parser.add_argument("--base", help="Base address to filter by") transfers_parser.add_argument("--chain", help="Blockchain to filter by") transfers_parser.add_argument("--usd-gte", type=int, help="Minimum USD value") transfers_parser.add_argument("--limit", type=int, default=10, help="Number of results (default: 10)") # counterparties command cp_parser = subparsers.add_parser("counterparties", help="Get counterparties for an address") cp_parser.add_argument("address", help="Address to get counterparties for") # ws-stream command ws_parser = subparsers.add_parser("ws-stream", help="Stream real-time transfers via WebSocket") ws_parser.add_argument("--usd-gte", type=int, default=10000000, help="Minimum USD value (default: 10000000). Must be >= 10M unless using --from/--to/--tokens") ws_parser.add_argument("--chains", help="Comma-separated list of chains to filter") ws_parser.add_argument("--from", dest="from_filter", help="Comma-separated source addresses or entity names") ws_parser.add_argument("--to", help="Comma-separated destination addresses or entity names") ws_parser.add_argument("--tokens", help="Comma-separated token IDs (e.g. usd-coin) or addresses") args = parser.parse_args() if not args.command: parser.print_help() sys.exit(0) # Dispatch to command handler commands = { "test": cmd_test, "chains": cmd_chains, "address": cmd_address, "transfers": cmd_transfers, "counterparties": cmd_counterparties, "ws-stream": cmd_ws_stream, } handler = commands.get(args.command) if handler: handler(args) else: print_error(f"Unknown command: {args.command}") parser.print_help() sys.exit(1) if __name__ == "__main__": main() ``` ## Python — rest example ```python #!/usr/bin/env python3 """ Arkham API - REST Example Shows how to make a simple REST API call to get address intelligence. Usage: export ARKHAM_API_KEY="your-api-key" python rest_example.py """ import os import requests API_KEY = os.environ.get("ARKHAM_API_KEY", "") if not API_KEY: print("Error: Set ARKHAM_API_KEY environment variable") print(' export ARKHAM_API_KEY="your-api-key"') exit(1) # Example: Get intelligence for Binance hot wallet address = "0x28C6c06298d514Db089934071355E5743bf21d60" response = requests.get( f"https://api.arkm.com/intelligence/address/{address}", headers={"API-Key": API_KEY} ) if response.ok: import json print(json.dumps(response.json(), indent=2)) else: print(f"Error {response.status_code}: {response.text}") ``` ## Python — websocket example ```python #!/usr/bin/env python3 """ Arkham API - WebSocket Streaming Example Streams real-time transfer notifications via WebSocket. Press Ctrl+C to stop. Requirements: pip install websockets requests Usage: export ARKHAM_API_KEY="your-api-key" python websocket_example.py """ import asyncio import json import os import signal import requests import websockets API_KEY = os.environ.get("ARKHAM_API_KEY", "") if not API_KEY: print("Error: Set ARKHAM_API_KEY environment variable") print(' export ARKHAM_API_KEY="your-api-key"') exit(1) running = True def handle_signal(sig, frame): global running print("\nDisconnecting...") running = False signal.signal(signal.SIGINT, handle_signal) async def stream_transfers(): # v1 requires a prepaid session: create one via REST, then pass its id on connect. resp = requests.post("https://api.arkm.com/ws/sessions", headers={"API-Key": API_KEY}) resp.raise_for_status() session_id = resp.json()["sessionId"] url = f"wss://api.arkm.com/ws/transfers?session_id={session_id}" headers = {"API-Key": API_KEY} # Connections can drop at any time, so reconnect in a loop until interrupted. # The session keeps its filter across reconnects, so only subscribe on the # first connection. subscribed = False while running: try: async with websockets.connect(url, additional_headers=headers) as ws: if not subscribed: # Subscribe to transfers from CEXs >= $10k subscribe = { "id": "1", "type": "subscribe", "payload": { "filters": { "from": ["type:cex"], "usdGte": 10000 } } } await ws.send(json.dumps(subscribe)) # Wait for ACK ack = await ws.recv() print("Subscribed:", json.loads(ack)) print("Waiting for transfers (may take up to 90 seconds)...") print("Press Ctrl+C to stop\n---") subscribed = True # Stream transfers async for message in ws: if not running: break data = json.loads(message) if data.get("type") == "transfer": t = data["payload"]["transfer"] print(t) except (websockets.WebSocketException, OSError): if running: print("connection lost, reconnecting...") await asyncio.sleep(1) asyncio.run(stream_transfers()) ``` ## Postman Collections - [Arkham-API-Complete.postman_collection](https://arkm.com/cookbook/postman/Arkham-API-Complete.postman_collection.json)