Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
773d2e1
feat(address-enrichment): add new service for address labeling
alextnetto Jan 22, 2026
042433d
fix(address-enrichment): improve sync script output and fix BigInt query
alextnetto Jan 23, 2026
6d53f22
feat(address-enrichment): add batch endpoint for multiple addresses
alextnetto Jan 23, 2026
605edda
feat(api-gateway): integrate address-enrichment service
alextnetto Jan 23, 2026
cc27bf3
docs: update address-enrichment README
alextnetto Feb 5, 2026
9f6f8b6
docs: update address-enrichment README
alextnetto Feb 5, 2026
7e44be8
feat(address-enrichment): add ENS client and integrate ENS data enric…
alextnetto Feb 5, 2026
e27430d
feat(address-enrichment): add database migrations and update enrichme…
alextnetto Feb 5, 2026
59153c9
fix: dotenv
PedroBinotto Feb 11, 2026
602702a
Merge branch 'dev' into feat/address-enrichment
PedroBinotto Feb 11, 2026
f56b41f
fix: anticapture integration
PedroBinotto Feb 11, 2026
d7ee033
fix: bundling
PedroBinotto Feb 11, 2026
72c15cb
fix: multi dao calls
PedroBinotto Feb 12, 2026
960cf0e
feat: infinite scrolling + multi DAO support
PedroBinotto Feb 12, 2026
60bc479
fix: remove console log
PedroBinotto Feb 12, 2026
31d4174
fix: pull all addresses
PedroBinotto Feb 12, 2026
e8a71db
fix: filter out null (0) value addresses
PedroBinotto Feb 12, 2026
f7f9159
chore: index OP
PedroBinotto Feb 15, 2026
fb8ffb4
refactor: generate migrations directly from TS schema
PedroBinotto Feb 19, 2026
fa3bc4a
fix: remove unnecessary code
PedroBinotto Feb 19, 2026
68ef2cb
chore: remove dead code
PedroBinotto Feb 19, 2026
9561ede
chore: update lockfile
PedroBinotto Feb 19, 2026
9c70b58
fix: always convert addresses to checksum format
PedroBinotto Feb 19, 2026
55087ec
fix: reverting addresses to lowercase for now
PedroBinotto Feb 19, 2026
1d6bac2
refactor: parse results using zod schema
PedroBinotto Feb 20, 2026
a01342e
Merge branch 'dev' into feat/address-enrichment
PedroBinotto Feb 20, 2026
8f66977
chore: remove 400 responses
pikonha Feb 20, 2026
dbf2a58
chore: add address enrichment graphql documents
pikonha Feb 20, 2026
104b6eb
refactor: mappers on routes
pikonha Feb 20, 2026
03e0781
chore: generate client codegen
pikonha Feb 20, 2026
1801eaa
Merge branch 'dev' into feat/address-enrichment
pikonha Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/address-enrichment/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/address_enrichment

# Arkham Intel API
ARKHAM_API_KEY=your_arkham_api_key
ARKHAM_API_URL=https://api.arkhamintelligence.com

# Ethereum RPC (for EOA/contract detection fallback)
RPC_URL=https://eth-mainnet.g.alchemy.com/v2/your_key

# Anticapture API (for sync command)
ANTICAPTURE_API_URL=http://localhost:4000/graphql
DAO_ID=ENS

# ENS cache TTL in minutes (default: 60)
# ENS_CACHE_TTL_MINUTES=60


28 changes: 28 additions & 0 deletions apps/address-enrichment/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
module.exports = {
root: true,
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json",
tsconfigRootDir: __dirname,
},
plugins: ["@typescript-eslint", "prettier"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
rules: {
"prettier/prettier": "error",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
ignorePatterns: [
"node_modules",
"dist",
"drizzle",
".eslintrc.js",
"drizzle.config.ts",
],
};
243 changes: 243 additions & 0 deletions apps/address-enrichment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Address Enrichment Service

A service that enriches Ethereum addresses with labels from Arkham Intel API, ENS data from ethfollow, and determines whether addresses are EOAs or contracts.

## Features

- **Arkham Labels**: Fetches entity names and labels from Arkham Intel API
- **ENS Resolution**: Fetches ENS name, avatar, and banner via ethfollow API
- **Address Type Detection**: Determines if an address is an EOA or contract via RPC
- **Hybrid Caching**: Arkham data is stored permanently. ENS data is cached with a configurable TTL and refreshed automatically.
- **OpenAPI Documentation**: Swagger UI available at `/docs`

## API Endpoints

### `GET /address/:address`

Returns enriched data for a single Ethereum address.

**Response:**

```json
{
"address": "0x245445940b317e509002eb682e03f4429184059d",
"isContract": false,
"arkham": {
"entity": "Upbit",
"entityType": "cex",
"label": "Cold Wallet",
"twitter": "Upbit_Global"
},
"ens": {
"name": "example.eth",
"avatar": "https://euc.li/example.eth",
"banner": "https://i.imgur.com/example.png"
},
"createdAt": "2024-01-20T10:30:00.000Z"
}
```

> `ens` is `null` when the address has no ENS name.

### `POST /addresses`

Batch endpoint for resolving multiple addresses at once (max 100 per request).

**Request:**

```json
{
"addresses": [
"0x245445940b317e509002eb682e03f4429184059d",
"0x1234567890abcdef1234567890abcdef12345678"
]
}
```

**Response:**

```json
{
"results": [
{
"address": "0x245445940b317e509002eb682e03f4429184059d",
"isContract": false,
"arkham": {
"entity": "Upbit",
"entityType": "cex",
"label": "Cold Wallet",
"twitter": "Upbit_Global"
},
"ens": {
"name": "example.eth",
"avatar": "https://euc.li/example.eth",
"banner": "https://i.imgur.com/example.png"
},
"createdAt": "2024-01-20T10:30:00.000Z"
}
],
"errors": [
{
"address": "0x1234567890abcdef1234567890abcdef12345678",
"error": "Failed to fetch from Arkham API"
}
]
}
```

### `GET /health`

Health check endpoint.

## Environment Variables

| Variable | Description | Required |
| ----------------------- | ---------------------------------- | -------------------------------------------------- |
| `DATABASE_URL` | PostgreSQL connection string | Yes |
| `ARKHAM_API_KEY` | Arkham Intel API key | Yes |
| `ARKHAM_API_URL` | Arkham API base URL | No (default: `https://api.arkhamintelligence.com`) |
| `RPC_URL` | Ethereum RPC URL | Yes |
| `ANTICAPTURE_API_URL` | Anticapture GraphQL API URL | Yes (for sync command) |
| `ENS_CACHE_TTL_MINUTES` | TTL in minutes for cached ENS data | No (default: `60`) |
| `PORT` | Server port | No (default: `3001`) |

## Development

```bash
# From monorepo root
pnpm address-enrichment dev

# Run database migrations
pnpm address-enrichment db:push

# Type check
pnpm address-enrichment typecheck

# Lint
pnpm address-enrichment lint
```

## Sync Command

Batch-sync top addresses from Anticapture API (delegates + token holders):

```bash
# Sync top 100 delegates and top 100 token holders
pnpm address-enrichment sync --limit 100

# Sync only delegates
pnpm address-enrichment sync --limit 50 --delegates-only

# Sync only token holders
pnpm address-enrichment sync --limit 50 --holders-only

# Show help
pnpm address-enrichment sync --help
```

The sync command:

- Fetches top addresses from Anticapture API (delegates by voting power, holders by balance)
- Skips addresses already in the database (no re-fetching)
- Only calls Arkham API and RPC for new addresses
- Deduplicates addresses that appear in both lists

## Database Schema

The service uses a single table `address_enrichment`:

- `address` (PK): Ethereum address (42 chars)
- `is_contract`: Boolean indicating if address is a contract
- `arkham_entity`: Entity name from Arkham (e.g., "Upbit", "Binance")
- `arkham_entity_type`: Entity type from Arkham (e.g., "cex", "dex", "defi")
- `arkham_label`: Specific label from Arkham (e.g., "Cold Wallet", "Hot Wallet")
- `arkham_twitter`: Twitter/X handle from Arkham (e.g., "Upbit_Global")
- `ens_name`: ENS name (e.g., "vitalik.eth")
- `ens_avatar`: ENS avatar URL
- `ens_banner`: ENS banner/header URL
- `ens_updated_at`: Timestamp when ENS data was last fetched (used for TTL)
- `created_at`: Timestamp when the record was first created

> Arkham data is permanent. ENS data is refreshed when `ens_updated_at` is older than `ENS_CACHE_TTL_MINUTES`.

## Data Flow

1. Request comes in for `GET /address/0x123...`
2. Check if address exists in database
3. If found:
- Arkham data: return as-is (permanent)
- ENS data: check `ens_updated_at` against TTL
- Fresh: return cached ENS data
- Stale or missing: refetch from ethfollow API, update row
4. If not found:
- Call Arkham API and ethfollow API in parallel
- If Arkham doesn't have contract info, fall back to RPC `getCode`
- Store everything in PostgreSQL
- Return enriched data

## Sequence Diagram — `GET /address/:address`

```mermaid
sequenceDiagram
actor Client
participant API as Address Enrichment API
participant DB as PostgreSQL
participant Arkham as Arkham Intel API
participant ENS as ethfollow API
participant RPC as Ethereum RPC

Client->>API: GET /address/0x123...
API->>API: Validate address format

alt Invalid address
API-->>Client: 400 Invalid address
end

%% ── Check local storage ──
API->>DB: Lookup address
DB-->>API: result

alt Already enriched
alt ENS data fresh
API-->>Client: 200 Enriched data (cached)
else ENS data stale or missing
API->>ENS: GET /api/v1/users/0x123.../ens
ENS-->>API: name, avatar, banner
API->>DB: UPDATE ens columns
API-->>Client: 200 Enriched data (ENS refreshed)
end
end

%% ── Address not in DB: fetch all in parallel ──
par Fetch in parallel
API->>Arkham: Get address intelligence
Arkham-->>API: labels, entity, contract info
and
API->>ENS: GET /api/v1/users/0x123.../ens
ENS-->>API: name, avatar, banner
end

%% ── Determine if contract or EOA ──
alt Arkham knows contract type
API->>API: Use Arkham's answer
else Arkham doesn't know
API->>RPC: Check bytecode on-chain
RPC-->>API: bytecode (or empty)
end

%% ── Store ──
API->>DB: INSERT enriched address
DB-->>API: stored

API-->>Client: 200 Enriched data
```

## Summary

Architecture overview:

- Framework: Hono with OpenAPI/Zod validation
- Database: PostgreSQL with Drizzle ORM
- External APIs: Arkham Intel API, ethfollow (ENS), Ethereum RPC, Anticapture GraphQL API
- Features: Single and batch enrichment endpoints, hybrid caching (permanent Arkham + TTL-based ENS), sync script
- Data flow: Check DB → Fetch Arkham + ENS in parallel → Fallback to RPC → Store → Return
9 changes: 9 additions & 0 deletions apps/address-enrichment/drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "drizzle-kit";

export default defineConfig({
schema: "./src/db/schema.ts",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL || "",
},
});
41 changes: 41 additions & 0 deletions apps/address-enrichment/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "@anticapture/address-enrichment",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsup src/index.ts --format esm --clean",
"start": "node dist/index.mjs",
"sync": "tsx src/scripts/sync-top-addresses.ts",
"lint": "eslint . --ext .ts",
"lint:fix": "eslint . --ext .ts --fix",
"typecheck": "tsc --noEmit",
"clean": "rm -rf node_modules dist *.tsbuildinfo"
},
"dependencies": {
"@hono/node-server": "^1.13.7",
"@hono/swagger-ui": "^0.5.1",
"@hono/zod-openapi": "^0.19.6",
"dotenv": "^17.2.4",
"drizzle-kit": "^0.31.4",
"drizzle-orm": "~0.41.0",
"hono": "^4.7.10",
"pg": "^8.13.1",
"viem": "^2.37.11",
"zod": "^3.25.3"
},
"devDependencies": {
"@types/node": "^20.16.5",
"@types/pg": "^8.11.10",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.5.3",
"tsup": "^8.5.1",
"tsx": "^4.19.4",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=18.14"
}
}
Loading
Loading