Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
113 changes: 112 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,114 @@ served DB is unavailable, the endpoint returns `503` instead of reporting zero v

---

### CoinMarketCap Endpoints

Implements the DEX endpoints from [Section C] of CoinMarketCap's integration
requirements. Served under `/cmc/`. The shapes mirror the CoinGecko adapter —
CMC's DEX spec is field-for-field close — and both feeds are built from the same
on-chain DAO discovery and rolling-24h ETL metrics.

**API versioning.** At CMC's request, every endpoint is also served under an
explicit version prefix: `/cmc/v1/summary`, `/cmc/v1/ticker`, `/cmc/v1/assets`.
The unversioned paths remain published as-is and are treated as the current (v1)
contract — the two are URL aliases for the same handler, so they never diverge.
A future breaking change would land under `/cmc/v2/…` while the existing paths
keep serving v1.

`/cmc/summary` and `/cmc/ticker` carry 24h volume, so they require
`DATABASE_PG_URL` and return `503` (never zero volume) if the served DB is
unavailable. `/cmc/assets` is pure on-chain metadata and does not require it.

No dedicated auth: like every route, CMC reuses the shared rate-limit tiers —
anonymous by IP, or the elevated per-key bucket when a trusted `X-API-Key`
(`TRUSTED_API_KEYS`) is sent.

Set `CMC_ALLOWED_MINTS` (comma-separated base mints) to restrict the CMC feed to
a specific set of tokens; empty (the default) serves every discovered DAO.

#### GET `/cmc/summary`

24h overview of every tradeable pair.

**Response:**
```json
[
{
"trading_pairs": "ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta_EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"base_currency": "ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta",
"quote_currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"type": "spot",
"last_price": 0.081340728222,
"lowest_ask": 0.081747431863,
"highest_bid": 0.080934024581,
"base_volume": 30024.8104,
"quote_volume": 2441.23456789,
"highest_price_24h": 0.085,
"lowest_price_24h": 0.078,
"price_change_percent_24h": 4.28
}
]
```

`base_currency` / `quote_currency` are Solana mint (contract) addresses — the
same ids `/cmc/assets` is keyed by, so CMC maps pairs → assets consistently.

`highest_price_24h` / `lowest_price_24h` are omitted when the ETL window has no
real high/low.

`price_change_percent_24h` is the 24h price change in percent, computed from the
AMM's **exact** price 24h ago. The FutarchyAMM price is a pure function of pool
reserves, and reserves only change on a swap, so the reserves of the last swap
≥24h ago (`futarchy.user_pool_swaps`) are the pool's exact state 24h ago — priced
through the same formula as `last_price` (a true mid-vs-mid comparison). It is
**omitted** for a market younger than 24h (no swap before the cutoff), where the
change is undefined — never fabricated as `0%`. If the swaps source is briefly
unavailable, the field is omitted for that response but price/volume still serve
(unlike the volume source, whose absence returns `503`).

#### GET `/cmc/ticker`

24h price and volume keyed by the `BASE_QUOTE` trading pair.

**Response:**
```json
{
"ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta_EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": {
"base_id": "ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta",
"quote_id": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"base_name": "ZKFG",
"base_symbol": "ZKFG",
"quote_name": "USD Coin",
"quote_symbol": "USDC",
"last_price": 0.081340728222,
"base_volume": 30024.8104,
"quote_volume": 2441.23456789,
"isFrozen": 0
}
}
```

#### GET `/cmc/assets`

Token identity keyed by mint address (both base and quote of every pair).

**Response:**
```json
{
"ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta": {
"name": "ZKFG",
"symbol": "ZKFG",
"contractAddress": "ZKFHiLAfAFMTcDAuCtjNW54VzpERvoe7PBF9mYgmeta",
"can_withdraw": "true",
"can_deposit": "true",
"maker_fee": 0.005,
"taker_fee": 0.005
}
}
```

---

### DexScreener Adapter Endpoints

Implements the [DexScreener Adapter Spec v1.1](https://dexscreener.notion.site/DEX-Screener-Adapter-Specs-cc1223cdf6e74a7799599106b65dcd0e). All endpoints are served under `/dexscreener/`. Requires `DATABASE_PG_URL` to be configured for the served DB.
Expand Down Expand Up @@ -236,7 +344,7 @@ Create a `.env` file in the root directory (see `example.env` for reference):
| `TRUSTED_RATE_LIMIT_MAX` | Per-bucket request count per minute for trusted keys | `600` |
| `CACHE_TICKERS_TTL` | On-chain data cache TTL (ms) | `55000` |
| **Served indexer DB (required — the only database this API uses)** | | |
| `DATABASE_PG_URL` | Read-only connection to the served indexer DB (Meteora, tickers, DexScreener, first-trade-dates). **Required** — `/api/market-data` returns 503 without it. | — |
| `DATABASE_PG_URL` | Read-only connection to the served indexer DB (Meteora, tickers, DexScreener, first-trade-dates). **Required** — `/api/market-data`, `/api/tickers`, `/cmc/summary`, `/cmc/ticker`, and the DexScreener routes return 503 without it. | — |
| `DATABASE_PG_SSL` | Enable SSL (server cert verified against system CAs) | `false` |
| `DATABASE_PG_CA_CERT` | PEM CA cert content for private-CA verification | — |
| `DATABASE_PG_SSL_NO_VERIFY` | Explicit opt-out of TLS verification (stopgap only) | `false` |
Expand All @@ -246,6 +354,7 @@ Create a `.env` file in the root directory (see `example.env` for reference):
| **Protocol** | | |
| `PROTOCOL_FEE_RATE` | Protocol fee rate | `0.005` (0.5%) |
| `EXCLUDED_DAOS` | Comma-separated DAO addresses to exclude | — |
| `CMC_ALLOWED_MINTS` | Comma-separated base-mint allowlist for the `/cmc/*` routes; empty serves all. Validated at startup; if set but matching zero discovered DAOs, the CMC routes fail closed with 503. | — |
| **Alerts** | | |
| `ALERT_WEBHOOK_URL` | Telegram alert webhook URL | — |
| `ALERT_WEBHOOK_SECRET` | Webhook secret | — |
Expand All @@ -263,6 +372,7 @@ src/
├── routes/
│ ├── index.ts # Route registration
│ ├── coingecko.ts # GET /api/tickers
│ ├── coinmarketcap.ts # CoinMarketCap DEX adapter (summary/ticker/assets)
│ ├── dexscreener.ts # DexScreener adapter (4 endpoints)
│ ├── market.ts # GET /api/market-data (user_pool ETL)
│ ├── supply.ts # GET /api/supply/*
Expand All @@ -278,6 +388,7 @@ src/
│ └── metricsService.ts # Prometheus counters/histograms
├── types/
│ ├── coingecko.ts # CoinGecko response types
│ ├── coinmarketcap.ts # CoinMarketCap response types
│ └── dexscreener.ts # DexScreener response types
├── middleware/
│ ├── errorHandler.ts # Error handling & asyncHandler
Expand Down
4 changes: 4 additions & 0 deletions example.env
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ SOLANA_RPC_URL=https://api.mainnet-beta.solana.com
# DEX_FORK_TYPE=Custom
# FACTORY_ADDRESS=
# ROUTER_ADDRESS=
# Allowlist of base mints exposed on the CoinMarketCap routes (/cmc/*),
# comma-separated. Empty (default) serves every discovered DAO, same as the
# CoinGecko/DexScreener adapters.
# CMC_ALLOWED_MINTS=
# DAOs excluded from /api/tickers (comma-separated mints). Default: none.
EXCLUDED_DAOS=DMB74TZgN7Rqfwtqqm3VQBgKBb2WYPdBqVtHbvB4LLeV,AE7jPb9jYzbUE5GYJToKvXaRkJL2Q7Mm3Ek6KqyBGuxe,E3BjsvLSFqUqVtDP76qMw4QbETkxvqvg8RTSbRZxWCK4,CnUUCGbSrAoaJniPifRU8zHRZ6e5uGRVSpCEj2WMeeSv,CLoqV77NtkbrsvtCRDP1vdYxgPZua3nnh7gCNPLzDQQ8,CJCgDqiDtkQvwXT2iiyY7QVajKLH3VRVbcsNQgtttrHn,651uV1hcd7SprwwkumFfkWtx5WrnD53awpjduGtGsHzS,4rW6iVKUq1RWYQ1VBTrjvP9FL4G3Sn7mBj7Yg12kuckv,Eo1BLMVRLJspjP5dDnwzK1m6FxMUcQDG6kDA8CjWPzRW,CTYxPujxrXiiqwG3gSBVNKuBk8u7mPG9qVMUc4aT1L8u,EbcsPbXZa81xUunDSmzYrcAWGURxcZB6BTkgzqvNJBZH,BgNq2V6vea2C7Z3cZhDUJTbmN4Y9bKG6dfEPhH19J7Fb,DHjQLd6LCM4yzZ9e8eabyGofDJLjbouqpuX8wh1rQuBs,BQjNtXjZB7b9WrqgJZQWfR52T1MqZoqMELAoombywDi8,j6Hx7bdAzcj1NsoRBqdafFuRkgEU48QeZ1i5NVXz9fF

Expand Down
19 changes: 19 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { PublicKey } from '@solana/web3.js';
export const config = {
solana: {
Expand Down Expand Up @@ -51,6 +51,25 @@
// Protocol fee rate (0.005 = 0.5%); used to report fee bps on DexScreener routes.
protocolFeeRate: parseFloat(process.env.PROTOCOL_FEE_RATE || '0.005'),
},
coinmarketcap: {
// Optional allowlist of base-mint addresses exposed on the CoinMarketCap
// routes. Empty (the default) means "serve every discovered DAO", matching
// the CoinGecko/DexScreener adapters. When set, ONLY these base mints appear
// — this is how we map our tokens onto CMC's expected asset ids without
// leaking test/never-listed DAOs into the CMC feed.
//
// Each entry is validated as a Solana pubkey at startup (like EXCLUDED_DAOS):
// a typo throws here — fail fast — rather than silently filtering every pair
// and serving an empty /cmc feed that a poller would read as "delisted". The
// normalized base58 form is stored so lookups match baseMint.toString().
allowedMints: new Set<string>(
(process.env.CMC_ALLOWED_MINTS || '')
.split(',')
.map(m => m.trim())
.filter(Boolean)
.map(m => new PublicKey(m).toString())

Check warning on line 70 in src/config.ts

View workflow job for this annotation

GitHub Actions / package-protection

Public key construction change: + .map(m => new PublicKey(m).toString())
),
},
alerts: {
webhookUrl: process.env.ALERT_WEBHOOK_URL || 'https://telegram-webhook-relay.themetadao-org.workers.dev',
webhookSecret: process.env.ALERT_WEBHOOK_SECRET || '',
Expand Down
Loading
Loading