mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-27 09:46:25 +02:00
474 lines
12 KiB
Markdown
474 lines
12 KiB
Markdown
|
|
# DexScreener Connector - Implementation Plan
|
|||
|
|
|
|||
|
|
## 📋 Tổng Quan
|
|||
|
|
|
|||
|
|
Sau khi research kỹ [DexScreener API Documentation](https://docs.dexscreener.com/api/reference) và phân tích source code SurfSense, đây là phương án implementation chính xác nhất cho DexScreener Connector.
|
|||
|
|
|
|||
|
|
## 🔍 DexScreener API Research Findings
|
|||
|
|
|
|||
|
|
### Base Information
|
|||
|
|
- **Base URL**: `https://api.dexscreener.com`
|
|||
|
|
- **Authentication**: KHÔNG cần API key (public API)
|
|||
|
|
- **Rate Limits**:
|
|||
|
|
- Profile/Ads endpoints: 60 requests/minute
|
|||
|
|
- Pair/Token endpoints: **300 requests/minute**
|
|||
|
|
- **Max Results**: Search endpoint trả về tối đa 30 pairs
|
|||
|
|
|
|||
|
|
### Core Endpoints
|
|||
|
|
|
|||
|
|
#### 1. Search Pairs
|
|||
|
|
```
|
|||
|
|
GET /latest/dex/search?q={query}
|
|||
|
|
Rate Limit: 300 req/min
|
|||
|
|
Max Results: 30 pairs
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Use Case**: Tìm kiếm trading pairs theo token name, symbol, hoặc address
|
|||
|
|
|
|||
|
|
**Response Structure**:
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"schemaVersion": "1.0.0",
|
|||
|
|
"pairs": [
|
|||
|
|
{
|
|||
|
|
"chainId": "solana",
|
|||
|
|
"dexId": "raydium",
|
|||
|
|
"url": "https://dexscreener.com/solana/...",
|
|||
|
|
"pairAddress": "...",
|
|||
|
|
"baseToken": {
|
|||
|
|
"address": "...",
|
|||
|
|
"name": "Token Name",
|
|||
|
|
"symbol": "TKN"
|
|||
|
|
},
|
|||
|
|
"quoteToken": {
|
|||
|
|
"address": "...",
|
|||
|
|
"name": "USD Coin",
|
|||
|
|
"symbol": "USDC"
|
|||
|
|
},
|
|||
|
|
"priceNative": "0.00123",
|
|||
|
|
"priceUsd": "1.23",
|
|||
|
|
"txns": {
|
|||
|
|
"m5": { "buys": 10, "sells": 5 },
|
|||
|
|
"h1": { "buys": 100, "sells": 50 },
|
|||
|
|
"h6": { "buys": 500, "sells": 250 },
|
|||
|
|
"h24": { "buys": 2000, "sells": 1000 }
|
|||
|
|
},
|
|||
|
|
"volume": {
|
|||
|
|
"h24": 1000000,
|
|||
|
|
"h6": 250000,
|
|||
|
|
"h1": 50000,
|
|||
|
|
"m5": 5000
|
|||
|
|
},
|
|||
|
|
"priceChange": {
|
|||
|
|
"m5": 1.5,
|
|||
|
|
"h1": 5.2,
|
|||
|
|
"h6": 10.5,
|
|||
|
|
"h24": 25.3
|
|||
|
|
},
|
|||
|
|
"liquidity": {
|
|||
|
|
"usd": 500000,
|
|||
|
|
"base": 1000000,
|
|||
|
|
"quote": 500000
|
|||
|
|
},
|
|||
|
|
"fdv": 10000000,
|
|||
|
|
"marketCap": 5000000,
|
|||
|
|
"pairCreatedAt": 1640000000000
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. Get Token Pairs
|
|||
|
|
```
|
|||
|
|
GET /latest/dex/tokens/{chainId}/{tokenAddress}
|
|||
|
|
Rate Limit: 300 req/min
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Use Case**: Lấy tất cả pools/pairs của một token cụ thể
|
|||
|
|
|
|||
|
|
#### 3. Get Specific Pair
|
|||
|
|
```
|
|||
|
|
GET /latest/dex/pairs/{chainId}/{pairAddress}
|
|||
|
|
Rate Limit: 300 req/min
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Use Case**: Lấy thông tin chi tiết của một pair cụ thể
|
|||
|
|
|
|||
|
|
#### 4. Get Multiple Tokens
|
|||
|
|
```
|
|||
|
|
GET /tokens/v1/{chainId}/{tokenAddresses}
|
|||
|
|
Rate Limit: 300 req/min
|
|||
|
|
Max: 30 addresses (comma-separated)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**Use Case**: Batch query nhiều tokens cùng lúc
|
|||
|
|
|
|||
|
|
## 🏗️ SurfSense Architecture Analysis
|
|||
|
|
|
|||
|
|
### Pattern Đã Xác Định
|
|||
|
|
|
|||
|
|
#### 1. Connector Class Pattern
|
|||
|
|
**File**: `app/connectors/{name}_connector.py`
|
|||
|
|
|
|||
|
|
**Responsibilities**:
|
|||
|
|
- Initialize với API credentials (nếu cần)
|
|||
|
|
- Methods để fetch data từ external API
|
|||
|
|
- Methods để format data sang markdown
|
|||
|
|
- Error handling cho API calls
|
|||
|
|
|
|||
|
|
**Example từ LumaConnector**:
|
|||
|
|
```python
|
|||
|
|
class LumaConnector:
|
|||
|
|
def __init__(self, api_key: str | None = None):
|
|||
|
|
self.api_key = api_key
|
|||
|
|
self.base_url = "https://api.lu.ma"
|
|||
|
|
|
|||
|
|
def make_request(self, endpoint: str, params: dict | None = None):
|
|||
|
|
# Handle API calls with error handling
|
|||
|
|
|
|||
|
|
def get_events_by_date_range(self, start_date: str, end_date: str):
|
|||
|
|
# Fetch data from API
|
|||
|
|
|
|||
|
|
def format_event_to_markdown(self, event: dict) -> str:
|
|||
|
|
# Convert to markdown for indexing
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2. Indexer Pattern
|
|||
|
|
**File**: `app/tasks/connector_indexers/{name}_indexer.py`
|
|||
|
|
|
|||
|
|
**Responsibilities**:
|
|||
|
|
- Async function `index_{name}()`
|
|||
|
|
- Get connector từ database
|
|||
|
|
- Extract config (API keys, etc.)
|
|||
|
|
- Initialize connector class
|
|||
|
|
- Fetch data từ API
|
|||
|
|
- Loop qua items:
|
|||
|
|
- Generate `unique_identifier_hash` (để track duplicates)
|
|||
|
|
- Generate `content_hash` (để detect content changes)
|
|||
|
|
- Check existing documents
|
|||
|
|
- Create/Update `Document` objects với:
|
|||
|
|
- `chunks` (text chunks cho vector search)
|
|||
|
|
- `embedding` (vector embedding)
|
|||
|
|
- `metadata` (structured data)
|
|||
|
|
- Batch commit to database
|
|||
|
|
- Update `last_indexed_at` timestamp
|
|||
|
|
|
|||
|
|
**Key Functions Used**:
|
|||
|
|
```python
|
|||
|
|
from app.utils.document_converters import (
|
|||
|
|
create_document_chunks,
|
|||
|
|
generate_content_hash,
|
|||
|
|
generate_document_summary,
|
|||
|
|
generate_unique_identifier_hash,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 3. Routes Pattern
|
|||
|
|
**File**: `app/routes/{name}_add_connector_route.py`
|
|||
|
|
|
|||
|
|
**Endpoints**:
|
|||
|
|
- `POST /connectors/{name}/add` - Add/Update connector
|
|||
|
|
- `DELETE /connectors/{name}` - Delete connector
|
|||
|
|
- `GET /connectors/{name}/test` - Test connection
|
|||
|
|
|
|||
|
|
**Example từ luma_add_connector_route.py**:
|
|||
|
|
```python
|
|||
|
|
@router.post("/connectors/luma/add")
|
|||
|
|
async def add_luma_connector(
|
|||
|
|
request: AddLumaConnectorRequest,
|
|||
|
|
user: User = Depends(current_active_user),
|
|||
|
|
session: AsyncSession = Depends(get_async_session),
|
|||
|
|
):
|
|||
|
|
# Check existing connector
|
|||
|
|
# Create or update SearchSourceConnector
|
|||
|
|
# Store config in connector.config JSON field
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 4. Database Schema
|
|||
|
|
**File**: `app/db.py`
|
|||
|
|
|
|||
|
|
**SearchSourceConnectorType Enum**:
|
|||
|
|
```python
|
|||
|
|
class SearchSourceConnectorType(str, Enum):
|
|||
|
|
LUMA_CONNECTOR = "LUMA_CONNECTOR"
|
|||
|
|
SLACK_CONNECTOR = "SLACK_CONNECTOR"
|
|||
|
|
# ... thêm DEXSCREENER_CONNECTOR
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**SearchSourceConnector Model**:
|
|||
|
|
```python
|
|||
|
|
class SearchSourceConnector(Base):
|
|||
|
|
id: int
|
|||
|
|
name: str
|
|||
|
|
connector_type: SearchSourceConnectorType
|
|||
|
|
config: dict # JSON field để store API keys, settings
|
|||
|
|
search_space_id: int
|
|||
|
|
user_id: UUID
|
|||
|
|
is_indexable: bool
|
|||
|
|
last_indexed_at: datetime
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 5. Celery Tasks
|
|||
|
|
**File**: `app/tasks/celery_tasks/connector_tasks.py`
|
|||
|
|
|
|||
|
|
**Pattern**:
|
|||
|
|
```python
|
|||
|
|
@celery_app.task(name="index_luma_events", bind=True)
|
|||
|
|
def index_luma_events_task(
|
|||
|
|
self,
|
|||
|
|
connector_id: int,
|
|||
|
|
search_space_id: int,
|
|||
|
|
user_id: str,
|
|||
|
|
start_date: str | None = None,
|
|||
|
|
end_date: str | None = None,
|
|||
|
|
):
|
|||
|
|
# Wrapper cho async indexer function
|
|||
|
|
return asyncio.run(_index_luma_events(...))
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 6. Periodic Scheduler
|
|||
|
|
**File**: `app/utils/periodic_scheduler.py`
|
|||
|
|
|
|||
|
|
**Mapping**:
|
|||
|
|
```python
|
|||
|
|
CONNECTOR_TYPE_TO_TASK_NAME = {
|
|||
|
|
SearchSourceConnectorType.LUMA_CONNECTOR: "index_luma_events",
|
|||
|
|
# ... thêm mapping cho DexScreener
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
CONNECTOR_TYPE_TO_TASK = {
|
|||
|
|
SearchSourceConnectorType.LUMA_CONNECTOR: index_luma_events_task,
|
|||
|
|
# ... thêm task cho DexScreener
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 📝 Implementation Plan
|
|||
|
|
|
|||
|
|
### Phase 1: Core Components
|
|||
|
|
|
|||
|
|
#### 1.1. Database Schema Update
|
|||
|
|
|
|||
|
|
**File**: `app/db.py`
|
|||
|
|
|
|||
|
|
**Changes**:
|
|||
|
|
```python
|
|||
|
|
class SearchSourceConnectorType(str, Enum):
|
|||
|
|
# ... existing types
|
|||
|
|
DEXSCREENER_CONNECTOR = "DEXSCREENER_CONNECTOR"
|
|||
|
|
|
|||
|
|
class DocumentType(str, Enum):
|
|||
|
|
# ... existing types
|
|||
|
|
DEXSCREENER_CONNECTOR = "DEXSCREENER_CONNECTOR"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 1.2. Connector Class
|
|||
|
|
|
|||
|
|
**File**: `app/connectors/dexscreener_connector.py`
|
|||
|
|
|
|||
|
|
Xem full implementation trong artifacts.
|
|||
|
|
|
|||
|
|
#### 1.3. Indexer
|
|||
|
|
|
|||
|
|
**File**: `app/tasks/connector_indexers/dexscreener_indexer.py`
|
|||
|
|
|
|||
|
|
Xem full implementation trong artifacts.
|
|||
|
|
|
|||
|
|
### Phase 2: API Routes & Integration
|
|||
|
|
|
|||
|
|
#### 2.1. Routes
|
|||
|
|
|
|||
|
|
**File**: `app/routes/dexscreener_add_connector_route.py`
|
|||
|
|
|
|||
|
|
Xem full implementation trong artifacts.
|
|||
|
|
|
|||
|
|
#### 2.2. Celery Task
|
|||
|
|
|
|||
|
|
**File**: `app/tasks/celery_tasks/connector_tasks.py`
|
|||
|
|
|
|||
|
|
**Add to existing file**:
|
|||
|
|
```python
|
|||
|
|
# Add import
|
|||
|
|
from app.tasks.connector_indexers import index_dexscreener_pairs
|
|||
|
|
|
|||
|
|
# Add task
|
|||
|
|
@celery_app.task(name="index_dexscreener_pairs", bind=True)
|
|||
|
|
def index_dexscreener_pairs_task(
|
|||
|
|
self,
|
|||
|
|
connector_id: int,
|
|||
|
|
search_space_id: int,
|
|||
|
|
user_id: str,
|
|||
|
|
):
|
|||
|
|
"""Celery task for indexing DexScreener pairs."""
|
|||
|
|
try:
|
|||
|
|
return asyncio.run(
|
|||
|
|
_index_dexscreener_pairs(
|
|||
|
|
connector_id=connector_id,
|
|||
|
|
search_space_id=search_space_id,
|
|||
|
|
user_id=user_id,
|
|||
|
|
)
|
|||
|
|
)
|
|||
|
|
except Exception as e:
|
|||
|
|
logger.error(f"DexScreener indexing task failed: {str(e)}", exc_info=True)
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def _index_dexscreener_pairs(
|
|||
|
|
connector_id: int,
|
|||
|
|
search_space_id: int,
|
|||
|
|
user_id: str,
|
|||
|
|
):
|
|||
|
|
"""Async wrapper for DexScreener indexing."""
|
|||
|
|
async with get_async_session_context() as session:
|
|||
|
|
return await index_dexscreener_pairs(
|
|||
|
|
session=session,
|
|||
|
|
connector_id=connector_id,
|
|||
|
|
search_space_id=search_space_id,
|
|||
|
|
user_id=user_id,
|
|||
|
|
)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.3. Periodic Scheduler
|
|||
|
|
|
|||
|
|
**File**: `app/utils/periodic_scheduler.py`
|
|||
|
|
|
|||
|
|
**Add to existing mappings**:
|
|||
|
|
```python
|
|||
|
|
# Add to CONNECTOR_TYPE_TO_TASK_NAME
|
|||
|
|
CONNECTOR_TYPE_TO_TASK_NAME = {
|
|||
|
|
# ... existing mappings
|
|||
|
|
SearchSourceConnectorType.DEXSCREENER_CONNECTOR: "index_dexscreener_pairs",
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# Add import
|
|||
|
|
from app.tasks.celery_tasks.connector_tasks import index_dexscreener_pairs_task
|
|||
|
|
|
|||
|
|
# Add to CONNECTOR_TYPE_TO_TASK
|
|||
|
|
CONNECTOR_TYPE_TO_TASK = {
|
|||
|
|
# ... existing mappings
|
|||
|
|
SearchSourceConnectorType.DEXSCREENER_CONNECTOR: index_dexscreener_pairs_task,
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.4. Routes Registration
|
|||
|
|
|
|||
|
|
**File**: `app/routes/__init__.py`
|
|||
|
|
|
|||
|
|
**Add**:
|
|||
|
|
```python
|
|||
|
|
# Add import
|
|||
|
|
from app.routes.dexscreener_add_connector_route import router as dexscreener_add_connector_router
|
|||
|
|
|
|||
|
|
# Add to router includes (after other connector routes)
|
|||
|
|
router.include_router(dexscreener_add_connector_router)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 2.5. Indexer Export
|
|||
|
|
|
|||
|
|
**File**: `app/tasks/connector_indexers/__init__.py`
|
|||
|
|
|
|||
|
|
**Add**:
|
|||
|
|
```python
|
|||
|
|
# Add import
|
|||
|
|
from .dexscreener_indexer import index_dexscreener_pairs
|
|||
|
|
|
|||
|
|
# Add to __all__
|
|||
|
|
__all__ = [
|
|||
|
|
# ... existing exports
|
|||
|
|
"index_dexscreener_pairs",
|
|||
|
|
]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 🔄 Usage Flow
|
|||
|
|
|
|||
|
|
### 1. Add Connector via API
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -X POST "http://localhost:8000/api/connectors/dexscreener/add" \
|
|||
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{
|
|||
|
|
"space_id": 1,
|
|||
|
|
"tokens": [
|
|||
|
|
{
|
|||
|
|
"chain": "solana",
|
|||
|
|
"address": "So11111111111111111111111111111111111111112",
|
|||
|
|
"name": "Wrapped SOL"
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
"chain": "ethereum",
|
|||
|
|
"address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
|
|||
|
|
"name": "Wrapped ETH"
|
|||
|
|
}
|
|||
|
|
]
|
|||
|
|
}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2. Test Connection
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -X GET "http://localhost:8000/api/connectors/dexscreener/test?chain=solana&token_address=So11111111111111111111111111111111111111112" \
|
|||
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. Trigger Manual Indexing
|
|||
|
|
|
|||
|
|
Indexing sẽ được trigger tự động qua:
|
|||
|
|
- **Periodic scheduler**: Mỗi 60 phút (configurable)
|
|||
|
|
- **Manual trigger**: Qua search_source_connectors_routes.py endpoint
|
|||
|
|
|
|||
|
|
### 4. Search Indexed Data
|
|||
|
|
|
|||
|
|
Data được index sẽ tự động available trong:
|
|||
|
|
- AI Chat với context từ DexScreener
|
|||
|
|
- Search results
|
|||
|
|
- Document retrieval
|
|||
|
|
|
|||
|
|
## ⚠️ Important Considerations
|
|||
|
|
|
|||
|
|
### Rate Limiting
|
|||
|
|
- DexScreener API: 300 requests/minute
|
|||
|
|
- Với 50 tokens tracked, mỗi lần index = 50 requests
|
|||
|
|
- Recommended indexing interval: **60 minutes**
|
|||
|
|
- Implement exponential backoff nếu hit rate limit
|
|||
|
|
|
|||
|
|
### Data Freshness
|
|||
|
|
- Crypto market data thay đổi nhanh
|
|||
|
|
- Consider shorter intervals (15-30 min) cho high-priority tokens
|
|||
|
|
- Implement priority queue cho important tokens
|
|||
|
|
|
|||
|
|
### Storage Optimization
|
|||
|
|
- Mỗi pair = 1 document với chunks
|
|||
|
|
- 50 tokens × 5 pairs average = 250 documents
|
|||
|
|
- Monitor storage usage và implement cleanup cho old data
|
|||
|
|
|
|||
|
|
### Error Handling
|
|||
|
|
- Network failures: Retry với exponential backoff
|
|||
|
|
- API errors: Log và skip, không block toàn bộ indexing
|
|||
|
|
- Invalid data: Validate trước khi index
|
|||
|
|
|
|||
|
|
## 🎯 Next Steps
|
|||
|
|
|
|||
|
|
1. **Implement Phase 1**: Core components (connector, indexer, DB schema)
|
|||
|
|
2. **Test locally**: Verify API calls và data formatting
|
|||
|
|
3. **Implement Phase 2**: Routes và integration
|
|||
|
|
4. **Test end-to-end**: Add connector → Index → Search
|
|||
|
|
5. **Deploy**: Monitor performance và adjust intervals
|
|||
|
|
6. **Optimize**: Based on usage patterns và feedback
|
|||
|
|
|
|||
|
|
## 📊 Success Metrics
|
|||
|
|
|
|||
|
|
- ✅ Connector successfully fetches data from DexScreener API
|
|||
|
|
- ✅ Data được format chính xác sang markdown
|
|||
|
|
- ✅ Documents được index với proper chunks và embeddings
|
|||
|
|
- ✅ Search results include DexScreener data
|
|||
|
|
- ✅ AI Chat có context từ crypto market data
|
|||
|
|
- ✅ Periodic indexing runs without errors
|
|||
|
|
- ✅ Rate limits được respect
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**Note**: Implementation này dựa trên:
|
|||
|
|
- Official DexScreener API Documentation
|
|||
|
|
- Existing SurfSense connector patterns (Luma, Slack, etc.)
|
|||
|
|
- Best practices từ production connectors
|