# Zing Indexbind API Documentation
REST API for the zing-indexbind service - a global semantic search index for decentralized content.
## Base URL
```
http://localhost:3001
```
Or set via environment variable:
```bash
export BASE=http://localhost:3001
```
---
## Score Filtering
All search endpoints apply **server-side relevance score filtering** after the search pipeline completes. Results with `relevance_score < 0.15` are automatically excluded from responses.
This ensures:
- Consistent quality across all endpoints (free and paid)
- No low-quality noise in AI query context
- Predictable result sets regardless of client parameters
---
## Authentication
The API has two access modes:
### 1. Free Access (Internal App)
Our web app uses these endpoints freely via backend proxy:
| `X-App-Key` | Your `APP_API_KEY` env var |
**Security:** Never expose this key in the browser. Proxy through your Next.js backend.
### 2. Paid Access (Public API)
External developers pay per request via USDC on Sui:
1. Sign an `ApiAccessMessage` with your Sui wallet
2. Send USDC to the platform address
3. Include `signature`, `bytes`, and `transaction_digest` in the request
**Request fields (all paid endpoints):**
| `q` | string | Search query |
| `wiki` | string | `global` or owner address |
| `owner` | string | Optional single owner address |
| `limit` | integer | Max results (default varies) |
| `expand` | bool | **Chunk routes only.** Return full chunk_text instead of excerpt |
| `article_ids` | string[] | **Chunk routes only.** Filter to specific article IDs |
| `transaction_digest` | string | Sui transaction digest |
| `signature` | string | Base64-encoded signature |
| `bytes` | string | Base64-encoded BCS `ApiAccessMessage` |
**ApiAccessMessage (BCS encoded):**
```rust
struct ApiAccessMessage {
q: String,
wiki: String,
transaction_digest: String,
timestamp: u64, // Must be within 5 minutes
expand: Option<bool>,
article_ids: Option<Vec<String>>,
}
```
**Payment flow:**
1. Client sends **all USDC to platform address only** (no per-creator transfers)
2. Server verifies total inflow >= `min_payment_usdc` (default: 0.01 USDC)
3. Server runs search and returns results up to budget
4. Excess payment is kept by platform (shown as `remaining_usdc`)
---
## Global Search (Free)
### `GET /search` - Global search across all wikis
Search across all indexed wikis. Requires `X-App-Key` header. Returns top results sorted by relevance score.
**Request:**
| `q` | string | (required) | Search query |
| `limit` | int | `20` | Max results |
**Auth:** `X-App-Key` header
**Example:**
```bash
curl "$BASE/search?q=what+is+DeFi&limit=5" -H "X-App-Key: $APP_API_KEY"
```
**Response:**
```json
[
{
"relative_path": "0x1aa2.../0xf6bd.../main.md",
"owner_address": "0x1aa2...",
"title": "What is DeFi?",
"summary": "Decentralized Finance...",
"tags": ["markets", "finance"],
"relevance_score": 2.14,
"chunk_token_count": 156,
"excerpt": "DeFi is a new paradigm...",
"heading_path": ["Introduction", "What is DeFi"],
"created_at": 1712345678
}
]
```
---
## Wiki Search (Free)
### `GET /query` - Search within a wiki
Search within a specific wiki's content. Requires `X-App-Key` header. Returns `SearchResult` with `results`, `total`, and `wiki_tags`.
**URL:** `/query?wiki={owner_address}&q={query}`
**Example:**
```bash
curl "$BASE/query?wiki=0x1aa2...&q=what+is+DeFi&limit=5" \
-H "X-App-Key: $APP_API_KEY"
```
**Response:** Returns `SearchResult` with `results`, `total`, and `wiki_tags` — same format as the detailed response below.
---
Search articles within a single owner's wiki. **Requires `X-App-Key` header.**
**Parameters:**
| `wiki` | string | yes | Owner address (e.g., `0x123...`) |
| `q` | string | yes | Search query |
| `mode` | string | no | Search mode: `vector`, `lexical`, or `hybrid` (default: `hybrid`) |
| `limit` | integer | no | Max results (default: 20) |
| `rerank` | string | no | Reranker options (default: `cross-encoder-onnx`) |
**Example:**
```bash
curl -H "X-App-Key: $APP_API_KEY" "$BASE/query?wiki=0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389&q=Web3"
```
**Response:**
```json
{
"results": [
{
"article_id": "0xdef456...",
"relative_path": "0xowner/0xarticle/main.md",
"title": "What is Web3",
"summary": null,
"best_match": {
"excerpt": "Web3 is the next generation of the internet...",
"heading_path": ["Introduction", "What is Web3"],
"char_start": 0,
"char_end": 200
},
"chunk_token_count": 116,
"raw_vector_score": 0.72,
"raw_lexical_score": 3.25,
"tags": ["cryptocurrency", "defi"],
"signals": {
"relevance_score": 0.85,
"article_token_count": 4200,
"recency_days": 12,
"tag_confidence": 0.72,
"wiki_file_count": 42,
"primary_tag": "cryptocurrency"
}
}
],
"total": 1,
"wiki_tags": [
{"tag": "cryptocurrency", "frequency": 10, "avg_score": 0.7}
]
}
```
Tags are extracted per-file using chunk embeddings. The system classifies by max similarity across 36 topic categories, storing the top 5 tags per file regardless of score threshold. Tags appear in the response as a sorted array (highest score first).
---
## Paid Search
### `POST /search` - Paid global search
Search across all indexed wikis with per-result pricing.
**Cost structure:**
- Flat search fee: covers cross-encoder inference (default: 0.0005 USDC for limit ≤ 20, scales linearly for larger limits)
- Per-result fee: based on token count (from DB, estimated at indexing time) × base rate (default: 0.0004 USDC per 1K tokens) + creator content fees
- `creators_fee_usdc` typically low if creators haven't set their content fee (default: 800 USDC per 1K tokens = 0.0008 USDC)
**Request body:**
```json
{
"q": "what is DeFi?",
"wiki": "global",
"owner": null,
"limit": 20,
"transaction_digest": "8xK2...",
"signature": "base64_sig...",
"bytes": "base64_bcs..."
}
```
**Notes:**
- `limit` is capped at max **50** results
- Flat fee scales when `limit > 20`: `fee = base_fee × (limit / 20)` (default base: 0.0005 USDC)
- Per-result token fees use `BASE_CHUNK_FEE_PER_1K_TOKENS_USDC=400` and creator content fees between `MIN_CONTENT_FEE=100` and `MAX_CONTENT_FEE=2000`
**Example:**
```bash
curl -X POST "$BASE/search" \
-H "Content-Type: application/json" \
-d '{
"q": "blockchain",
"wiki": "global",
"limit": 10,
"transaction_digest": "0xabc...",
"signature": "sig...",
"bytes": "bcs..."
}'
```
**Response:**
```json
{
"query_text": "blockchain",
"wiki_scope": "global",
"results": [
{
"article_id": "0xcb07...",
"relative_path": "0x1aa2.../0xcb07.../main.md",
"title": "What is Web3",
"summary": null,
"best_match": {
"excerpt": "Web3 is the next generation...",
"heading_path": ["Introduction"],
"char_start": 0,
"char_end": 200
},
"chunk_token_count": 116,
"raw_vector_score": 0.72,
"raw_lexical_score": 3.25,
"tags": ["cryptocurrency"],
"signals": {
"relevance_score": 1.40,
"article_token_count": 4200,
"recency_days": 3,
"tag_confidence": 0.72,
"wiki_file_count": 42,
"primary_tag": "cryptocurrency"
}
}
],
"budget": {
"paid_usdc": "0.010000",
"consumed_usdc": "0.000797",
"remaining_usdc": "0.009203",
"platform_fee_usdc": "0.000743",
"creators_fee_usdc": "0.000054",
"items_returned": 5,
"items_searched": 20
},
"payments": [
{"recipient": "0xplatform...", "amount_usdc": "0.000743"},
{"recipient": "0xcreator1...", "amount_usdc": "0.000053"}
]
}
```
---
---
## Search Estimate (Admin)
### `POST /search/estimate` - Estimate search cost
Runs the full search pipeline without payment verification. Returns all results with budget and payment breakdown for debugging. **Admin-only** — requires `X-App-Key` header.
**Cost structure:** Same as `POST /search` — uses `FLAT_SEARCH_FEE_USDC=500`, `BASE_CHUNK_FEE_PER_1K_TOKENS_USDC=400`, and creator fees between `MIN_CONTENT_FEE=100` and `MAX_CONTENT_FEE=2000`. No budget cap — costs are calculated for all results.
**Request body:**
```json
{
"q": "what is DeFi?",
"wiki": "global",
"owner": null,
"limit": 20
}
```
**Example:**
```bash
curl -X POST "$BASE/search/estimate" \
-H "Content-Type: application/json" \
-H "X-App-Key: $APP_API_KEY" \
-d '{"q": "blockchain", "wiki": "global", "limit": 10}'
```
**Response:** Same as `POST /search` (`SearchPaidResponse`) with `paid_usdc=0`, full `consumed_usdc` for all results, and complete `payments` breakdown.
---
## Paid Chunk Retrieval
### `POST /chunks` - Paid chunk retrieval
Retrieve semantic chunks with per-chunk pricing. Returns results greedily accumulated within budget (highest relevance first).
**Cost structure:**
- Flat infrastructure fee: covers cross-encoder + retrieval pipeline (default: 0.0005 USDC for limit ≤ 20, scales linearly)
- Per-chunk base fee: `chunk_tokens / 1000 × base_rate_per_1k` (default: 0.0004 USDC per 1K tokens)
- Per-chunk content fee: `chunk_tokens / 1000 × creator_rate[level]` (0 if subscribed). The fee rate is determined by the chunk's article `subscription_level` (fetched on-chain at query time). Unconfigured levels fall back to the platform default.
**Token count:** Uses the `chunk_token_count` field (from the DB `token_count` column, estimated at indexing time, target: ~200 tokens per chunk). For CJK text, this uses `len-1` formula (not byte-length/4), ensuring fair billing across languages.
**Request body:**
```json
{
"q": "what is DeFi?",
"wiki": "global",
"owner": null,
"limit": 20,
"expand": false,
"article_ids": null,
"transaction_digest": "8xK2...",
"signature": "base64_sig...",
"bytes": "base64_bcs..."
}
```
**Notes:**
- `limit` is capped at max **50** (internally capped to 50 documents, 500 raw chunks)
- Flat fee scales when `limit > 20`: `fee = base_fee × (limit / 20)` (default base: 0.0005 USDC)
- `expand`: optional boolean (defaults to `false`). When `true`, `text` returns the full `chunk_text` instead of the `excerpt`, and `truncated` is always `null`. Must match the signed `ApiAccessMessage`. No additional charge — same pricing either way.
- `article_ids`: optional array of article IDs to filter by. When provided, only chunks from these articles are returned. Must match the signed `ApiAccessMessage`. SQL-level filter — uses indexed `article_id` columns in both `chunk_embeddings` and `wiki_chunks`.
**Example:**
```bash
curl -X POST "$BASE/chunks" \
-H "Content-Type: application/json" \
-d '{
"q": "what is Web3?",
"wiki": "global",
"limit": 10,
"article_ids": null,
"transaction_digest": "0xabc...",
"signature": "sig...",
"bytes": "bcs..."
}'
```
**Response:**
```json
{
"query_text": "what is Web3?",
"wiki_scope": "global",
"chunks": [
{
"chunk_id": 94,
"article_id": "0xf6bd...",
"relative_path": "0x1aa2.../0xf6bd.../main.md",
"owner_address": "0x1aa2...",
"title": "What is Web3",
"heading_path": ["Introduction"],
"chunk_token_count": 116,
"scores": {
"document": 1.40,
"passage": 3.25,
"blended": 2.14,
"vector": 0.72,
"lexical": 3.25
},
"text": "Web3 is the next generation of the internet...",
"content_type": "prose",
"language": null,
"truncated": null
}
],
"budget": {
"paid_usdc": "0.010000",
"consumed_usdc": "0.000797",
"remaining_usdc": "0.009203",
"platform_fee_usdc": "0.000743",
"creators_fee_usdc": "0.000054",
"items_returned": 5,
"items_searched": 5
},
"payments": [
{"recipient": "0xplatform...", "amount_usdc": "0.000743"},
{"recipient": "0xcreator1...", "amount_usdc": "0.000053"}
],
"formatted_context": "\n\n[Source 1]...",
"total_tokens": 580
}
```
**Scores:**
| `document` | float | Cross-encoder reranked score for the parent document (unbounded) |
| `passage` | float | RRF-merged vector+lexical score for this chunk (unbounded) |
| `blended` | float | Content-type-aware blending: prose=`document×0.6+passage×0.4`, code/table=`document×0.3+passage×0.7` |
| `vector` | float or null | Raw cosine similarity score (only if chunk appeared in vector results) |
| `lexical` | float or null | Raw `ts_rank_cd` + bigram score (only if chunk appeared in lexical results) |
**Content fields:**
| `text` | string or null | Chunk excerpt (truncated for display), or full `chunk_text` when `expand=true` (see Chunking below) |
| `content_type` | string | `"prose"`, `"code"`, or `"table"` |
| `language` | string or null | Programming language for code chunks (e.g., `"typescript"`) |
| `truncated` | object or null | Metadata about what was truncated, or `null` when `expand=true` or when excerpt is full text |
**Truncation metadata (`truncated`):**
| `content_type` | string | Always present |
| `table_rows_total` / `table_rows_shown` | int | Table chunks with hidden rows |
| `code_lines_total` / `code_lines_shown` | int | Code chunks truncated by length |
| `prose_chars_total` / `prose_chars_shown` | int | Prose chunks truncated by length |
When `truncated` is `null`, the excerpt contains the full chunk text.
---
### `POST /chunk/expand` - Expand chunks (paid)
Retrieve the full untruncated text for up to 20 chunks by their IDs. Requires a signed `ExpandAccessMessage` and an on-chain USDC payment. Budget capping applies — chunks are returned greedily within budget (sorted by provided order, not score).
**Request body:**
```json
{
"chunk_ids": [1603, 94, 1802],
"transaction_digest": "5KzN...",
"signature": "AM3x...",
"bytes": "BQAAAGNo..."
}
```
**Access message** (BCS-encoded, base64 `bytes` field):
```
struct ExpandAccessMessage {
chunk_ids: Vec<i64>, // up to 20
transaction_digest: String,
timestamp: u64, // ms since epoch, max 5 min drift
}
```
**Response:**
```json
{
"chunks": [
{
"chunk_id": 1603,
"article_id": "0xb195...",
"owner_address": "0x1aa2...",
"ordinal": 2,
"heading_path": ["Configure OpenID Providers", "OpenID providers"],
"char_start": 500,
"char_end": 900,
"token_count": 88,
"chunk_text": "| Provider | Can support? | Devnet | ... |\n(all rows)",
"content_type": "table",
"language": null,
"truncated": {
"content_type": "table",
"table_rows_total": 15,
"table_rows_shown": 3
}
}
],
"budget": {
"paid_usdc": "1.000000",
"consumed_usdc": "0.002320",
"remaining_usdc": "0.997680",
"platform_fee_usdc": "0.001200",
"creators_fee_usdc": "0.001120",
"items_returned": 1,
"items_searched": 1
},
"payments": [
{},
{}
]
}
```
**Errors:**
- `400` — More than 20 chunk IDs, empty chunk_ids
- `400` — Invalid BCS encoding, expired timestamp, bad signature
- `402` — Insufficient payment (flat fee not covered)
---
### `POST /chunk/expand/estimate` - Estimate expand cost (Admin)
Estimate the cost of an expand request without charging. **Admin-only** — requires `X-App-Key` header.
**Request body:**
```json
{
"chunk_ids": [1603, 94, 1802]
}
```
**Example:**
```bash
curl -X POST "$BASE/chunk/expand/estimate" \
-H "Content-Type: application/json" \
-H "X-App-Key: $APP_API_KEY" \
-d '{"chunk_ids": [1603, 94]}'
```
**Response:**
Same `ExpandPaidResponse` structure as `/chunk/expand`, with `budget.paid_usdc: 0` and an empty `payments` array. The estimate fetches actual subscription levels from on-chain, so costing is identical.
---
### `POST /chunk/estimate` - Estimate chunk cost (Admin)
Estimate the cost of a chunk query. **Admin-only** — requires `X-App-Key` header. Runs the full search pipeline and returns a cost breakdown without charging.
**Request body:**
```json
{
"q": "what is DeFi?",
"wiki": "global",
"owner": null,
"limit": 10,
"expand": false,
"article_ids": null
}
```
**Example:**
```bash
curl -X POST "$BASE/chunk/estimate" \
-H "Content-Type: application/json" \
-H "X-App-Key: $APP_API_KEY" \
-d '{
"q": "what is Web3?",
"wiki": "global",
"limit": 10,
"expand": false,
"article_ids": null
}'
```
**Response:**
```json
{
"total_tokens": 580,
"budget": {
"paid_usdc": "0.000000",
"consumed_usdc": "0.001297",
"remaining_usdc": "0.000000",
"platform_fee_usdc": "0.000980",
"creators_fee_usdc": "0.000720",
"items_returned": 5,
"items_searched": 5
},
"unique_creators": 2,
"creator_breakdown": [
{
"owner_address": "0xcreator1...",
"chunk_token_count": 300,
"fee_per_1k_tokens_usdc": 800,
"fee_usdc": 240,
"waived": false
},
{
"owner_address": "0xcreator2...",
"chunk_token_count": 280,
"fee_per_1k_tokens_usdc": 1200,
"fee_usdc": 336,
"waived": false
}
],
"chunks": [
{
"chunk_id": 94,
"article_id": "0xf6bd...",
"relative_path": "0x1aa2.../0xf6bd.../main.md",
"owner_address": "0x1aa2...",
"title": "What is Web3",
"heading_path": ["Introduction"],
"chunk_token_count": 116,
"scores": {
"document": 1.40,
"passage": 3.25,
"blended": 2.14,
"vector": 0.72,
"lexical": 3.25
}
}
],
"payments": [
{"recipient": "0xplatform...", "amount_usdc": "0.000980"},
{"recipient": "0xcreator1...", "amount_usdc": "0.000216"},
{"recipient": "0xcreator2...", "amount_usdc": "0.000302"}
]
}
```
The estimate fetches the actual `subscription_level` from on-chain for each chunk, just like the paid `/chunks` endpoint, so the costing is identical.
**Budget fields (same structure as `/chunks`):**
All USDC amounts are serialized as strings with 6 decimal places (micro-USDC representation).
| `paid_usdc` | string | Always `"0.000000"` for estimates (no payment) |
| `consumed_usdc` | string | Total cost: flat_fee + base_fee + content_fee |
| `remaining_usdc` | string | Always `"0.000000"` for estimates |
| `platform_fee_usdc` | string | Platform share: flat_fee + base_fee + platform_share |
| `creators_fee_usdc` | string | Creator payout after platform share deduction |
| `items_returned` | integer | Number of chunks returned |
| `items_searched` | integer | Number of chunks passing all filters (before budget) |
---
## Scoring Formula Endpoints
### `GET /search/formula` - Search scoring formula
Returns the exact weights used in the search scoring pipeline. No authentication required.
**Example:**
```bash
curl "$BASE/search/formula"
```
**Response:**
```json
{
"rrf_k": 60,
"rrf_raw_weight": 0.5,
"reranker": {
"cross_encoder": {
"original_score_weight": 0.2,
"cross_encoder_score_weight": 0.8
},
"embedding": {
"original_score_weight": 0.2,
"rerank_score_weight": 0.8,
"embedding_weight": 0.8,
"heuristic_weight": 0.2
},
"heuristic": {
"original_score_weight": 0.35,
"heuristic_score_weight": 0.65,
"coverage_weights": {
"title": 0.45,
"heading": 0.20,
"excerpt": 0.25,
"path": 0.10
}
}
},
"min_score": 0.15,
"vector_distance_metric": "cosine",
"embedding_model": "minishlab/potion-base-128M",
"embedding_dimensions": 256,
"cross_encoder_model": "onnx-community/bge-reranker-v2-m3-ONNX"
}
```
---
---
### `GET /chunk/formula` - Chunk scoring formula
Returns the exact weights used in the chunk scoring pipeline, including the doc/passage blending weights. No authentication required.
**Example:**
```bash
curl "$BASE/chunk/formula"
```
**Response:** Same as `/search/formula` but with additional chunk blending fields:
```json
{
"rrf_k": 60,
"rrf_raw_weight": 0.5,
"doc_weight": 0.6,
"passage_weight": 0.4,
"content_type_weights": {
"prose": { "doc_weight": 0.6, "passage_weight": 0.4 },
"code": { "doc_weight": 0.3, "passage_weight": 0.7 },
"table": { "doc_weight": 0.3, "passage_weight": 0.7 }
},
"reranker": { ... },
"min_score": 0.15,
"vector_distance_metric": "cosine",
"embedding_model": "minishlab/potion-base-128M",
"embedding_dimensions": 256,
"cross_encoder_model": "onnx-community/bge-reranker-v2-m3-ONNX"
}
```
**Blending formula (content-type-aware):**
```
// Prose (default)
blended_score = document_score × 0.6 + passage_score × 0.4
// Code and table (content-type boost, more weight on chunk-level passage score)
blended_score = document_score × 0.3 + passage_score × 0.7
```
---
---
## Creator Fee Configuration
Content fees are **level-based**. Each article on-chain has a `subscription_level` (0-2). Creators set different fees per level. When a user accesses content, the article's subscription level determines which fee rate applies. If a level isn't configured, the platform default applies.
### `GET /chunk/config/{owner_address}` - Get fee config
Get the creator's level-based content fees and search opt-out status. The response shows the **effective** fee for each supported level — configured fees where set, platform defaults otherwise.
**Example:**
```bash
curl "$BASE/chunk/config/0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6"
```
**Response:**
```json
{
"owner_address": "0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"fee_per_1k_tokens_usdc": {
"0": 800,
"1": 1200,
"2": 800
},
"search_enabled": true
}
```
| `fee_per_1k_tokens_usdc` | object | Map of `level` → effective `fee_per_1k_tokens_usdc` for all supported levels (0, 1, 2). Uses creator's configured fee if set, otherwise the platform default (800). |
| `search_enabled` | boolean | Whether the wiki is included in global search |
Levels 0-2 are supported. An entirely empty response means all three levels use the platform default fee.
---
### `PUT /chunk/config` - Set content fee level
Creators set a single level's content fee. Each call configures one level's fee in the JSONB config. To set multiple levels, make multiple calls. Does **not** affect search visibility — use `PUT /chunk/config/enabled` for that.
**Request body:**
```json
{
"level": 2,
"fee_per_1k_tokens_usdc": 1200,
"enabled": true,
"signature": "base64_sig...",
"bytes": "base64_bcs..."
}
```
`bytes` decodes to BCS `SetFeeMessage`:
```rust
struct SetFeeMessage {
owner_address: String,
level: u8,
fee_per_1k_tokens_usdc: u64,
enabled: bool,
timestamp: u64,
}
```
**Behavior by `enabled` value:**
- `true`: Sets the level's fee in the JSONB config
- `false`: Removes the level entry from the config
**Constraints:**
- `level` must be between 0 and 2
- Fee must be between `MIN_CONTENT_FEE_PER_1K_TOKENS_USDC` (default: 100) and `MAX_CONTENT_FEE_PER_1K_TOKENS_USDC` (default: 2000)
- Setting `enabled: false` removes the level entry (does not affect search visibility)
- Request must be signed by `owner_address`
- Timestamp must be within 5 minutes
**Example:**
```bash
curl -X PUT "$BASE/chunk/config" \
-H "Content-Type: application/json" \
-d '{
"level": 2,
"fee_per_1k_tokens_usdc": 1200,
"enabled": true,
"signature": "sig...",
"bytes": "bcs..."
}'
```
**Response:**
```json
{
"owner_address": "0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"level": 2,
"fee_per_1k_tokens_usdc": 1200,
"enabled": true,
"success": true
}
```
---
### `PUT /chunk/config/enabled` - Toggle search visibility
Toggle whether a creator's wiki appears in global search results. Requires a signed message. Does **not** affect fee level configuration — use `PUT /chunk/config` for that.
**Request body:**
```json
{
"enabled": true,
"signature": "base64_sig...",
"bytes": "base64_bcs..."
}
```
`bytes` decodes to BCS `SetEnabledMessage`:
```rust
struct SetEnabledMessage {
owner_address: String,
enabled: bool,
timestamp: u64,
}
```
**Constraints:**
- `enabled: true` — wiki appears in search results
- `enabled: false` — wiki excluded from search results
- Request must be signed by `owner_address`
- Timestamp must be within 5 minutes
**Example:**
```bash
curl -X PUT "$BASE/chunk/config/enabled" \
-H "Content-Type: application/json" \
-d '{
"enabled": false,
"signature": "sig...",
"bytes": "bcs..."
}'
```
**Response:**
```json
{
"owner_address": "0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"enabled": false,
"success": true
}
```
---
---
## Index Management
### `POST /index/{owner_address}` - Trigger full rebuild
Queue an owner's wiki for full re-indexing. **Admin-only** — requires `X-Admin-Key` header.
**Auth:** `X-Admin-Key` header matching the server's `APP_API_KEY`
**Example:**
```bash
curl -X POST "$BASE/index/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389" \
-H "X-Admin-Key: your-admin-key"
```
---
### `POST /index/{owner_address}/articles` - Index specific articles
Index specific articles without re-indexing the entire wiki.
**Auth:** Signature verification, or `X-Admin-Key` header (bypasses signature + on-chain checks)
**Request Body:**
```json
{
"article_ids": [
"0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087"
],
"signature": "base64_sig...",
"bytes": "base64_bcs..."
}
```
`bytes` decodes to BCS `IndexArticlesMessage`:
```rust
struct IndexArticlesMessage {
owner_address: String,
article_ids: Vec<String>,
timestamp: u64,
}
```
**Constraints (non-admin):**
- Signer must match `owner_address` in path
- `article_ids` match between signed message and request body
- Timestamp within 5 minutes
- Each `article_id` must exist on-chain and be owned by the signer
- Articles that fail this check are returned in `skipped_article_ids`
**Admin mode:** Pass `X-Admin-Key: your-key` header → signature and on-chain ownership checks skipped, all articles queued directly.
**Response (non-admin):**
```json
{
"status": "queued",
"queued_article_ids": [
"0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087"
],
"skipped_article_ids": [
"0xabc..."
]
}
```
**Response (admin):**
```json
{
"status": "queued",
"queued_article_ids": [
"0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087"
],
"skipped_article_ids": []
}
```
- `queued_article_ids`: Articles queued for indexing
- `skipped_article_ids`: Articles not owned by signer or not found on-chain (non-admin only)
- If all articles are skipped, the server returns **400** with an error
---
---
### `GET /index/{owner_address}/status` - Get indexing status
Check the indexing status of a wiki.
**Example:**
```bash
curl "$BASE/index/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389/status"
```
**Response:**
```json
{
"owner_address": "0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389",
"status": "ready",
"last_indexed_at": "2026-04-21T15:30:00Z",
"file_count": 42
}
```
**Status values:**
- `pending` - Queued for indexing
- `indexing` - Currently being indexed
- `ready` - Indexed and searchable
- `error` - Indexing failed
---
---
### `DELETE /index/{owner_address}` - Delete entire wiki index
Remove all indexed content for an owner. **Admin-only** — requires `X-Admin-Key` header.
**Auth:** `X-Admin-Key` header matching the server's `APP_API_KEY`
**Example:**
```bash
curl -X DELETE "$BASE/index/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389" \
-H "X-Admin-Key: your-admin-key"
```
**Response:**
```json
{
"status": "deleted",
"owner_address": "0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389"
}
```
---
---
### `DELETE /index/{owner_address}/articles/{article_id}` - Delete article index
Remove indexed content for an article that has been **destroyed on-chain**. Single-article endpoint.
**Auth:** Signature verification (`X-Signature` + `X-Bytes` headers), or `X-Admin-Key` header (bypasses signature + on-chain checks)
**Headers (non-admin):**
| `X-Signature` | Base64-encoded signature |
| `X-Bytes` | Base64-encoded BCS `DeleteArticleMessage` |
`X-Bytes` decodes to BCS `DeleteArticleMessage`:
```rust
struct DeleteArticleMessage {
owner_address: String,
article_id: String,
timestamp: u64,
}
```
**Constraints (non-admin):**
- Signer must match `owner_address` in path
- `article_id` must match between signed message and path
- Timestamp within 5 minutes
- Article must exist in the DB index
- Article must **not** exist on-chain (destroyed) — returns **409 Conflict** if still on-chain
**Admin mode:** Pass `X-Admin-Key: your-key` header → signature and on-chain existence check skipped, article deleted directly.
**Example (signature):**
```bash
curl -X DELETE "$BASE/index/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389/articles/0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087" \
-H "X-Signature: base64_sig..." \
-H "X-Bytes: base64_bcs..."
```
**Example (admin):**
```bash
curl -X DELETE "$BASE/index/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389/articles/0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087" \
-H "X-Admin-Key: your-admin-key"
```
**Response (deleted):**
```json
{
"status": "deleted",
"owner_address": "0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389",
"article_id": "0x2fc59c01610675543263d6a052dd877787e9bacc38b9e0c4d9a74498a5ca7087",
"deleted": true,
"reason": null
}
```
**Response (skipped — not found in index):**
```json
{
"status": "skipped",
"owner_address": "0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389",
"article_id": "0xabc...",
"deleted": false,
"reason": "Article not found in index"
}
```
---
---
### `POST /admin/clear-all-indexes` - Clear all indexes
**Admin-only** — requires `X-Admin-Key` header. Truncates all indexing tables.
**Auth:** `X-Admin-Key` header matching the server's `APP_API_KEY`
---
## Wiki Tags
Get tags and their frequencies for a specific wiki.
**Example:**
```bash
curl "$BASE/tags/0x32c21ad0de99fd2150a231fab36f4c0b2e55a95d6c3e367621870fb33bdb2389"
```
**Response:**
```json
{
"owner_address": "0x32c21ad...",
"tags": [
{"tag": "cryptocurrency", "frequency": 10, "avg_score": 0.7, "parent_tag": "markets"},
{"tag": "defi", "frequency": 5, "avg_score": 0.65, "parent_tag": "markets"}
]
}
```
---
### `POST /tags/batch` - Batch wiki tags
Get tags for multiple wikis in a single request.
**Request body:**
```json
{
"addresses": [
"0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"0x2bb3..."
]
}
```
**Response:**
```json
{
"results": [
{
"owner_address": "0x32c21ad...",
"tags": [
{"tag": "cryptocurrency", "frequency": 10, "avg_score": 0.7},
{"tag": "defi", "frequency": 5, "avg_score": 0.65}
],
"article_count": 42
},
{
"owner_address": "0xabc...",
"tags": [],
"article_count": 0
}
]
}
```
---
### `GET /tags/trending` - Trending tags
Get globally trending tags across all wikis, sorted by total frequency descending (aggregated across all owners).
**Example:**
```bash
curl "$BASE/tags/trending"
```
**Response:**
```json
{
"tags": [
{"tag": "cryptocurrency", "frequency": 150, "avg_score": 0.72, "parent_tag": "markets"},
{"tag": "ai", "frequency": 120, "avg_score": 0.68, "parent_tag": "technology"},
{"tag": "defi", "frequency": 85, "avg_score": 0.65, "parent_tag": "markets"}
]
}
```
---
### `GET /tags/{tag}/articles` - Articles by tag
Get articles that have a specific tag.
**Parameters:**
| `tag` | string (path) | yes | - | Tag name (parent category or child tag) |
| `limit` | integer | no | 20 | Max articles (max: 100) |
| `offset` | integer | no | 0 | Pagination offset |
| `owner` | string | no | - | Scope to a specific owner |
**Tag expansion:** If the input matches a parent category (e.g., `technology`), all child tags are matched. See `matched_tags` in response.
**Example:**
```bash
curl "$BASE/tags/technology/articles?limit=10&owner=0x1aa2..."
```
**Response:**
```json
{
"tag": "technology",
"matched_tags": ["ai", "software-dev", "blockchain"],
"total": 42,
"limit": 10,
"offset": 0,
"articles": [
{
"article_id": "0xf6bd...",
"owner_address": "0x1aa2...",
"title": "Article Title",
"summary": null,
"excerpt": "Article excerpt...",
"relative_path": "...",
"tag_score": 0.87,
"article_token_count": 244,
"recency_days": 3
}
]
}
```
---
### `GET /creators/{tag}` - Creators by tag
Get creators whose wikis have a specific tag.
**Parameters:**
| `tag` | string (path) | yes | - | Exact tag name |
| `limit` | integer | no | 50 | Max creators |
| `offset` | integer | no | 0 | Pagination offset |
| `min_score` | float | no | 0.0 | Minimum avg_score threshold |
| `include_tags` | boolean | no | false | Include creator's other high-scoring tags |
**Example:**
```bash
curl "$BASE/creators/cryptocurrency?limit=20&offset=0&min_score=0.1&include_tags=true"
```
**Response:**
```json
{
"tag": "cryptocurrency",
"total": 150,
"limit": 20,
"offset": 0,
"creators": [
{
"owner_address": "0x32c21ad...",
"file_count": 42,
"last_indexed_at": "2026-04-21T15:30:00Z",
"tag_frequency": 10,
"tag_avg_score": 0.7,
"other_tags": [
{"tag": "defi", "frequency": 8, "avg_score": 0.65}
]
}
]
}
---
## Earnings
### `GET /earnings/summary` - Creator earnings summary
Get aggregated earnings summary for a creator.
**Parameters:**
| `owner_address` | string | yes | Creator's Sui address |
**Example:**
```bash
curl "$BASE/earnings/summary?owner_address=0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6"
```
**Response:**
```json
{
"owner_address": "0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"total_earned_usdc": 150000,
"total_attributions": 25,
"last_settled_at": null
}
```
---
---
Get paginated earnings history for a creator.
**Parameters:**
| `owner_address` | string | yes | Creator's Sui address |
| `limit` | integer | no | Max results (default: 50) |
| `offset` | integer | no | Pagination offset (default: 0) |
---
---
### `GET /earnings/monthly` - Monthly earnings breakdown
Get earnings aggregated by calendar month for a creator.
**Parameters:**
| `address` | string | yes | Creator's Sui address |
| `year` | integer | no | Filter by 4-digit year (e.g. 2026) |
| `month` | integer | no | Filter by month 1-12 (requires `year`) |
**Example:**
```bash
curl "$BASE/earnings/monthly?address=0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6"
```
**Response:**
```json
{
"creator_address": "0x1aa2c40369fa0fffb12fe6e1415b8aba52d15cc3cf59e001adc5d2687920fbd6",
"monthly": [
{
"month": "2026-05",
"gross_usdc": 85000,
"net_usdc": 76500,
"platform_fee_usdc": 8500,
"query_count": 12
},
{
"month": "2026-04",
"gross_usdc": 120000,
"net_usdc": 108000,
"platform_fee_usdc": 12000,
"query_count": 18
}
],
"total": {
"month": "total",
"gross_usdc": 205000,
"net_usdc": 184500,
"platform_fee_usdc": 20500,
"query_count": 30
}
}
```
**With filters:**
```bash
curl "$BASE/earnings/monthly?address=0x1aa2...&year=2026&month=5"
```
---
---
Get earnings broken down by article for a creator. Results ordered by `total_usdc` descending.
**Parameters:**
| `address` | string | yes | Creator's Sui address |
| `limit` | integer | no | Max results (default: 50) |
| `offset` | integer | no | Pagination offset (default: 0) |
**Response:**
```json
{
"creator_address": "0x1aa2...",
"items": [
{
"article_id": "0x8a0f...",
"total_usdc": 1285,
"query_count": 1,
"last_queried": "2026-05-17T15:30:13.471632Z",
"title": "My Article Title"
}
],
"total": 142
}
```
---
---
## Attributions
### `GET /attributions/article` - Attributions for an article
Get payment attributions for a specific article.
**Parameters:**
| `article_id` | string | yes | Article address |
---
---
### `GET /articles/overview` - Batch article titles
Resolve article IDs to human-readable titles. Never soft-deleted — populated at index time, persists even after articles are removed.
**Parameters:**
| `ids` | string | yes | Comma-separated article IDs |
**Example:**
```bash
curl "$BASE/articles/overview?ids=0x8a0f7eeae06e9e06ee50b30d4674d12ef3403793a582a07e7e78860796ed2c32,0x1234..."
```
**Response:**
```json
{
"items": [
{
"article_id": "0x8a0f...",
"owner_address": "0x1aa2...",
"title": "My Article Title"
}
]
}
```
---
---
### `GET /attributions/article/stats` - Article attribution stats
Get aggregated attribution statistics across all articles.
---
---
### `GET /attributions/creator` - Attributions for a creator
Get payment attributions for a specific creator.
**Parameters:**
| `owner_address` | string | yes | Creator's Sui address |
---
---
### 401 Unauthorized
```json
{
"error": "Unauthorized: Invalid or missing app key"
}
```
### 402 Payment Required
```json
{
"error": "Payment insufficient: received 5000 USDC, minimum required is 10000 USDC"
}
```
### 400 Bad Request
```json
{
"error": "Invalid input: Request expired"
}
```
### 404 Not Found
```json
{
"error": "Wiki 0x123... not indexed. POST /index/0x123... to trigger indexing."
}
```
### 500 Internal Server Error
```json
{
"error": "Database error: connection pool exhausted"
}
```
---
## Error Responses
### 401 Unauthorized
```json
{
"error": "Unauthorized: Invalid or missing app key"
}
```
### 402 Payment Required
```json
{
"error": "Payment insufficient: received 5000 USDC, minimum required is 10000 USDC"
}
```
### 400 Bad Request
```json
{
"error": "Invalid input: Request expired"
}
```
### 404 Not Found
```json
{
"error": "Wiki 0x123... not indexed. POST /index/0x123... to trigger indexing."
}
```
### 500 Internal Server Error
```json
{
"error": "Database error: connection pool exhausted"
}
```
---
## Config Reference
| `PORT` | `3001` | Server port |
| `DATABASE_URL` | (required) | PostgreSQL connection string |
| `SUI_NETWORK` | `testnet` | Sui network |
| `SUI_PRIVATE_KEY` | (required) | Sui private key (Bech32) |
| `EMBEDDING_MODEL` | `minishlab/potion-multilingual-128M` | Embedding model |
| `ORT_DYLIB_PATH` | (required for ONNX) | Path to ONNX Runtime library |
| `BASE_CHUNK_FEE_PER_1K_TOKENS_USDC` | `400` | Base fee per 1K tokens (0.0004 USDC) |
| `PLATFORM_USDC_ADDRESS` | (required) | Platform's Sui USDC address |
| `PLATFORM_SHARE_BPS` | `1000` | Platform share in basis points (10%) |
| `MIN_CONTENT_FEE_PER_1K_TOKENS_USDC` | `100` | Min creator fee per 1K tokens (0.0001 USDC) |
| `MAX_CONTENT_FEE_PER_1K_TOKENS_USDC` | `2000` | Max creator fee per 1K tokens (0.002 USDC) |
| `DEFAULT_CONTENT_FEE_PER_1K_TOKENS_USDC` | `800` | Default creator fee when no DB config exists |
| `MIN_PAYMENT_USDC` | `10000` | Minimum payment per request (0.01 USDC) |
| `APP_API_KEY` | (required for free endpoints) | Secret key for internal app access |
| `FLAT_SEARCH_FEE_USDC` | `500` | Flat fee per search (0.0005 USDC) |
---
---
## Architecture Notes
### Search Pipeline
```
Query → Vector Search (potion-base-128M) → Top-N × 5 candidates
│
▼
Lexical Search (pg_bigm) → Top-N × 5 candidates
│
▼
RRF Merge (rrf_k=60, raw_weight=0.5) → Top-N
│
▼
Reranker (cross-encoder / embedding / heuristic) → Top-K
│
▼
Top-K Results (default 20)
```
**Pipeline details:**
| 1 | Vector retrieval | N×5 | Cosine similarity on `minishlab/potion-base-128M` embeddings (256 dims) via pgvector |
| 2 | Lexical retrieval | N×5 | `ts_rank_cd` + pg_bigm bigram frequency on PostgreSQL |
| 3 | RRF merge | N | Reciprocal Rank Fusion: `1/(rrf_k + rank + 1) + raw_score × 0.5` per list, summed |
| 4 | Reranker | N | Cross-encoder (`bge-reranker-v2-m3` ONNX) or embedding heuristic |
| 5 | Final results | 10-50 | Returns top-K with `relevance_score` (default 20) |
**Reranker modes (configurable via `rerank` parameter):**
- `cross-encoder-onnx` (default): Full cross-encoder inference on (query, document) pairs. Weights: `score × 0.2 + ce_score × 0.8`.
- `embedding`: Embedding-based reranking with token coverage heuristic. Weights: `score × 0.2 + (embedding_sim × 0.8 + heuristic × 0.2) × 0.8`.
- `heuristic`: Token coverage heuristic only. Weights: `score × 0.35 + heuristic × 0.65`.
**Chunk search (document-scored):**
1. Document search with cross-encoder reranker → top-N docs with `doc_score`
2. Chunk search (RRF only, no rerank) → all candidate chunks with `passage_score`
3. Blend: `final_score = doc_score × 0.6 + passage_score × 0.4`
4. Sort desc, truncate to limit
Inherits cross-encoder scores from document search. No per-chunk cross-encoder for performance.
### Scoring Constants
All scoring weights are defined as named constants in `src/scoring.rs` and exposed via `/search/formula` and `/chunk/formula`. See those endpoints for the complete weight manifest.
### Chunking
- **Target:** ~200 tokens per chunk with ~25 token overlap between adjacent chunks
- **Content types:** `prose` (paragraphs/headings), `code` (fenced blocks), `table` (markdown tables)
- **Atomic blocks:** Code and table chunks are never split or merged — each is one chunk
- **Token estimation:** Custom estimator — alphanumeric words = 1 token, CJK = `len-1` formula
- **Excerpts (per-type):**
- Prose: `strip_markdown`, truncate at sentence boundary (280 chars max)
- Code: line-boundary truncation with `...` suffix (400 chars max)
- Table: header + first 3 data rows + `(N more rows)` note
- **Truncation info:** Computed at query time from `chunk_text` vs `excerpt` — no DB column needed
- **Expand endpoint:** `POST /chunk/expand` returns full `chunk_text` for truncated chunks (paid, up to 20 IDs)
### Tag Extraction
- **Taxonomy:** 36 fixed topics across 6 parent categories (society, markets, technology, lifestyle, entertainment, intellectual)
- **Assignment:** Cosine similarity between content embedding and each topic's embedding
- **Storage:** `file_tags` (per-file, tag, score, algorithm="topic") → aggregated in `wiki_tags` (per-owner, frequency, avg_score)
- **Topics include:** Multilingual descriptions (English + Chinese) for each topic
---
### Payment Flow
```
Client
│
▼
Sign ApiAccessMessage + send USDC to Platform
│
▼
POST /search or POST /chunks or POST /chunk/expand
│
▼
Server verifies signature + on-chain payment
│
▼
Runs search (expensive: cross-encoder inference)
│
▼
Calculates per-result/per-chunk cost
│
▼
Greedily accumulates within budget (highest score first)
│
▼
Records payment + creator earnings in DB
│
▼
Returns results + budget breakdown
```
### Settlement
A background scheduler runs every `settlement_interval_secs` (default: 300s) to batch-payout accumulated creator earnings via a single Sui PTB.
---
## Health Check
### `GET /health`
**Example:**
```bash
curl "$BASE/health"
```
**Response:** `OK` (plain text, HTTP 200)
---
## Cost Formulas
**Flat fees:**
```
flat_fee = BASE_FLAT_FEE (if limit ≤ 20)
flat_fee = ceil(BASE_FLAT_FEE × limit / 20) (if limit > 20)
```
**Chunk costs:**
```
tokens = chunk.chunk_token_count (from DB, estimated at indexing time)
base_fee = tokens × BASE_CHUNK_FEE_PER_1K_TOKENS_USDC / 1000
level = article.subscription_level (fetched on-chain from Article object)
creator_rate = content_fee_levels[creator][level] ?? DEFAULT_CONTENT_FEE_PER_1K_TOKENS_USDC
content_fee = tokens × creator_rate / 1000 (0 if subscriber)
platform_share = content_fee × PLATFORM_SHARE_BPS / 10000
item_cost = base_fee + content_fee
total_consumed = flat_fee + sum(item_cost)
```
**Per-creator aggregate (used in `/chunks` and `/chunk/estimate`):**
```
for each creator:
total_tokens = sum(chunk.chunk_token_count for that creator)
level = max(chunk.subscription_level for that creator)
rate = get_content_fee_for_level(creator, level) ?? DEFAULT_CONTENT_FEE_PER_1K_TOKENS_USDC
content_fee = total_tokens × rate / 1000 (0 if waived)
platform_share = content_fee × PLATFORM_SHARE_BPS / 10000
```
**Budget breakdown (same structure for paid and estimate):**
```
consumed_usdc = flat_fee + content_fee_total + base_fee_total
platform_fee_usdc = flat_fee + base_fee_total + platform_share_total
creators_fee_usdc = content_fee_total - platform_share_total
```
**Search costs:**
```
flat_fee = FLAT_SEARCH_FEE_USDC (covers cross-encoder, scales with limit > 20)
per_result = tokens × (BASE_CHUNK_FEE_PER_1K_TOKENS_USDC + creator_rate) / 1000
```
## Route Summary
| GET | `/search` | App Key | Global search |
| GET | `/query` | App Key | Search within a wiki |
| POST | `/chunks` | Signature | Paid chunk retrieval |
| POST | `/chunk/expand` | Signature | Expand chunks — full untruncated text (paid) |
| POST | `/chunk/expand/estimate` | App Key | Estimate expand cost (admin) |
| POST | `/search` | Signature | Paid search |
| POST | `/search/estimate` | App Key | Estimate search cost (admin) |
| POST | `/chunk/estimate` | App Key | Estimate chunk cost (admin) |
| GET | `/search/formula` | None | Search scoring formula |
| GET | `/chunk/formula` | None | Chunk scoring formula |
| GET | `/chunk/config/{address}` | None | Creator fee config |
| PUT | `/chunk/config` | Signature | Set creator fee level |
| PUT | `/chunk/config/enabled` | Signature | Toggle search visibility |
| GET | `/tags/{owner}` | None | Wiki tags |
| POST | `/tags/batch` | None | Batch wiki tags |
| GET | `/tags/trending` | None | Trending tags |
| GET | `/tags/{tag}/articles` | None | Articles by tag |
| GET | `/creators/{tag}` | None | Creators by tag |
| POST | `/index/{owner}` | Admin key | Trigger full rebuild |
| POST | `/index/{owner}/articles` | Signature | Index specific articles |
| GET | `/index/{owner}/status` | None | Indexing status |
| DELETE | `/index/{owner}` | Admin key | Delete wiki index |
| DELETE | `/index/{owner}/articles/{article_id}` | Signature | Delete article index |
| POST | `/admin/clear-all-indexes` | Admin key | Clear all indexes |
| GET | `/earnings/summary` | None | Creator earnings summary |
| GET | `/earnings/monthly` | None | Monthly earnings breakdown |
| GET | `/earnings/history` | None | Earnings history |
| GET | `/articles/overview` | None | Batch article title + preview |
| GET | `/earnings/articles` | None | Earnings by article |
| GET | `/attributions/article` | None | Article attributions |
| GET | `/attributions/article/stats` | None | Attribution stats |
| GET | `/attributions/creator` | None | Creator attributions |
| GET | `/health` | None | Health check |