Token image service for Euler Finance. Fetches token logos from multiple providers, stores them in AWS S3 (or local storage for debugging), and serves them via HTTP.
- Project Structure
- Setup
- Scripts
- HTTP Server & API Endpoints
- Sync Flow
- Image Providers
- Storage Architecture
- Adding Custom Token Logos
- Pendle PT Tokens
- CI/CD
euler-token-images/
├── src/
│ ├── server.ts # Hono HTTP server
│ ├── utils.ts # Shared utilities & rate-limit config
│ ├── providers/ # Image provider implementations
│ │ ├── interface.ts # ImageProvider / ImageResult types
│ │ ├── local-images-provider.ts # Local filesystem (images/ folder)
│ │ ├── coingecko-provider.ts # CoinGecko Pro API
│ │ ├── oneinch-provider.ts # 1inch token list
│ │ ├── alchemy-provider.ts # Alchemy API
│ │ ├── sim-dune-provider.ts # Sim Dune API
│ │ ├── pendle-provider.ts # Pendle API
│ │ └── token-list-provider.ts # Aggregates 25+ public token lists
│ └── services/
│ ├── sync-service.ts # Orchestrates the sync process
│ ├── fetch-image-service.ts # ImageProviderManager (priority chain)
│ ├── image-storage-service.ts # Unified S3 / local storage abstraction
│ ├── image-processing-service.ts # Sharp-based image manipulation (PT ring)
│ └── pendle-pt-service.ts # Pendle PT token detection & exceptions
├── scripts/
│ ├── migration.ts # Bulk migration: .data/ token lists → images/
│ └── force-update-token.ts # Force-update a single token image in storage
├── images/ # Local source images (committed to repo)
│ ├── default.png # Fallback image when no logo is found
│ └── {chainId}/{address}/image.{ext} # Per-token images
├── .data/ # Token list JSON files per chain
│ ├── ethereumTokenList.json
│ ├── baseTokenList.json
│ └── ...
├── local-storage/ # Local storage mirror (gitignored, debug only)
├── .github/workflows/
│ └── fetch-token-images.yml # Daily CI workflow
├── .env.example # Environment variable template
├── package.json
└── tsconfig.json
- Bun runtime
bun installCopy .env.example to .env and fill in the values:
# Required
COINGECKO_API_KEY= # CoinGecko Pro API key
EULER_API_URL= # Euler Finance API (default: https://v3.euler.finance/v3)
# S3 storage (optional - falls back to local-storage/ if not set)
AWS_REGION= # AWS region (default: eu-west-1)
EULER_AWS_ACCESS_KEY= # AWS access key for S3
EULER_AWS_SECRET_ACCESS_KEY= # AWS secret key for S3
# Optional
PORT= # Server port (default: 4000)
SIM_DUNE_API_KEY= # Sim Dune API key
RPC_HTTP_1= # Ethereum RPC endpointWhen S3 credentials are not provided, all storage operations fall back to the local-storage/ directory. This is useful for local development.
| Script | Command | Description |
|---|---|---|
| start | bun run start |
Start the HTTP server (default port 4000) |
| migration | bun run migration |
Bulk migration: reads .data/*.json token lists, downloads images from S3/providers, writes them to images/ |
| force-update | bun run force-update -- --chainId <id> --address <addr> |
Force-update a single token's image in storage (S3 or local) |
Overwrites the stored image for a specific token. Useful when a token's logo has changed or needs manual correction.
# Update USDC on Ethereum mainnet
bun run force-update -- --chainId 1 --address 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48How it works:
- Checks
images/{chainId}/{address}/for a local image file - If not found locally, queries all image providers
- Uploads (overwrites) the image in storage
bun run start # port 4000
PORT=3000 bun run start # custom port| Method | Path | Description |
|---|---|---|
GET |
/{chainId}/{address} |
Serve token image (falls back to default.png) |
GET |
/sync/{chainId} |
Trigger sync for a chain (or return running status) |
GET |
/sync/{chainId}/status |
Get sync status without triggering |
GET |
/health |
Health check |
GET /1/0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 → USDC on Ethereum
GET /8453/0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 → USDC on Base
- Returns the image from storage (S3 or local) with appropriate
Content-Type - Falls back to
images/default.pngif not found - Cached with
Cache-Control: public, max-age=86400(24 hours) - Pendle PT tokens with local overrides automatically get a teal ring applied
GET /sync/1 → sync all Ethereum mainnet tokens
GET /sync/8453 → sync all Base tokens
Rate limited to 1 sync per chain per minute. Returns 429 if rate limited.
The sync process follows four steps:
1. Fetch Token List → Euler API returns all tokens for the chain
2. Check Storage → Bulk-check S3/local for existing images (skip those)
3. Migrate Local Images → Upload images from images/ folder to S3
4. Download Missing → Query providers, download, and upload to storage
{
"success": true,
"data": {
"chainId": 1,
"totalTokens": 150,
"existingImages": 120,
"migratedFromLocal": 15,
"downloadedImages": 10,
"failedDownloads": 5,
"duration": 45000,
"details": [
{ "address": "0x...", "status": "exists|migrated|downloaded|failed", "provider": "coingecko" }
]
}
}All providers implement the ImageProvider interface and are queried in parallel. The first successful result by priority order wins.
| Priority | Provider | Source | Notes |
|---|---|---|---|
| 1 | Local | images/ folder |
Committed to repo |
| 2 | CoinGecko | CoinGecko Pro API | Requires COINGECKO_API_KEY |
| 3 | 1inch | 1inch token list | Skips chains 239, 80094, 60808, 1923 |
| 4 | Alchemy | Alchemy API | Skips chains 80094, 43114 |
| 5 | Sim Dune | Sim Dune API | Requires SIM_DUNE_API_KEY |
| 6 | Pendle | Pendle API | Supports chains 1, 42161 |
| 7 | Token Lists | 25+ public token lists | Uniswap, Aave, etc. |
Bucket: euler-token-images
euler-token-images/
├── 1/
│ └── 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48/
│ └── image ← no file extension; extension stored in metadata
├── 8453/
│ └── ...
└── {chainId}/{address}/image
Each S3 object includes metadata:
{
"ContentType": "image/png",
"Metadata": {
"extension": "png",
"provider": "coingecko",
"downloadDate": "2025-01-21T10:30:00.000Z",
"originalUrl": "https://assets.coingecko.com/..."
}
}When S3 credentials are not configured, the same structure is mirrored under local-storage/ with an additional metadata.json file per token.
To add or replace a token logo manually:
- Place the image at
images/{chainId}/{address}/image.{ext}(address must be lowercase) - Run
bun run force-update -- --chainId <chainId> --address <address>to push it to S3
Or simply commit the image to the repo - it will be picked up on the next sync as the local provider has the highest priority.
Pendle PT tokens automatically receive a teal ring (#17e3c2) when served. This applies when:
- The token has
isPendlePT: truein its.data/*.jsontoken list entry, OR - The token address is in the
PENDLE_PT_EXCEPTIONSset insrc/services/pendle-pt-service.ts
AND the token has a local image override in the images/ folder.
To add a new PT exception:
// src/services/pendle-pt-service.ts
const PENDLE_PT_EXCEPTIONS = new Set([
"0xb6168f597cd37a232cb7cb94cd1786be20ead156",
// Add new PT addresses here (lowercase)
]);| Chain | ID |
|---|---|
| Ethereum | 1 |
| Arbitrum | 42161 |
| Base | 8453 |
| Avalanche | 43114 |
| BSC | 56 |
| Sonic | 146, 1923 |
| Berachain | 80094 |
| Bob | 60808 |
| Unichain | 130 |
| Linea | 59144 |
| Mantle | 5000 |
| Swell | 1868 |
| TAC | 2741 |
| Ink | 57073 |
| HyperEVM | 999 |
| Monad | 143 |
Keeps token images up to date by triggering the sync endpoint on the production service.
Schedule: Daily at midnight UTC (also supports manual workflow_dispatch).
How it works:
- Loops through all 17 supported chain IDs
- Sends
GET https://token-images.euler.finance/sync/{chainId}for each chain - Waits 1 minute between chain requests to respect the sync rate limit
- Logs the result per chain:
200— sync triggered successfully429— rate limited or already syncing (non-fatal, skipped)- Any other status — logged as a failure and emits a GitHub Actions warning
No checkout, build, or credentials are needed — the workflow only makes HTTP requests to the deployed service, which handles fetching from providers and uploading to S3 internally.