Upload 152 files
Browse files- .dockerignore +103 -23
- .env.example +17 -41
- .gitignore +30 -21
- Dockerfile +62 -7
- FRONTEND_COMPLETE.md +219 -0
- QUICK_START.md +153 -75
- README.md +281 -276
- __pycache__/app.cpython-313.pyc +0 -0
- __pycache__/config.cpython-313.pyc +0 -0
- api-resources/README.md +282 -0
- api-resources/api-config-complete__1_.txt +1634 -0
- api-resources/crypto_resources_unified_2025-11-11.json +2097 -0
- api-resources/ultimate_crypto_pipeline_2025_NZasinich.json +503 -0
- app.py +1569 -325
- database.py +318 -22
- database/__pycache__/__init__.cpython-313.pyc +0 -0
- database/__pycache__/data_access.cpython-313.pyc +0 -0
- database/__pycache__/db_manager.cpython-313.pyc +0 -0
- database/__pycache__/models.cpython-313.pyc +0 -0
- enhanced_dashboard.html +876 -0
- main.py +2 -167
- requirements.txt +1 -2
- start.bat +17 -45
- unified_dashboard.html +2107 -0
- utils/__pycache__/__init__.cpython-313.pyc +0 -0
- utils/__pycache__/logger.cpython-313.pyc +0 -0
.dockerignore
CHANGED
|
@@ -1,41 +1,121 @@
|
|
| 1 |
-
#
|
| 2 |
-
node_modules/
|
| 3 |
-
frontend/node_modules/
|
| 4 |
-
|
| 5 |
-
# Build artifacts
|
| 6 |
-
frontend/dist/
|
| 7 |
-
|
| 8 |
-
# Python cache
|
| 9 |
__pycache__/
|
| 10 |
-
*.
|
| 11 |
-
|
| 12 |
-
*.pyd
|
| 13 |
-
.Python
|
| 14 |
*.so
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
-
#
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
# IDE
|
| 22 |
.vscode/
|
| 23 |
.idea/
|
| 24 |
*.swp
|
| 25 |
*.swo
|
|
|
|
|
|
|
| 26 |
|
| 27 |
# Git
|
| 28 |
.git/
|
| 29 |
.gitignore
|
|
|
|
| 30 |
|
| 31 |
-
#
|
| 32 |
-
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
*.log
|
| 37 |
logs/
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
#
|
| 40 |
-
|
| 41 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
__pycache__/
|
| 3 |
+
*.py[cod]
|
| 4 |
+
*$py.class
|
|
|
|
|
|
|
| 5 |
*.so
|
| 6 |
+
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
MANIFEST
|
| 23 |
+
pip-log.txt
|
| 24 |
+
pip-delete-this-directory.txt
|
| 25 |
|
| 26 |
+
# Virtual environments
|
| 27 |
+
venv/
|
| 28 |
+
ENV/
|
| 29 |
+
env/
|
| 30 |
+
.venv
|
| 31 |
|
| 32 |
# IDE
|
| 33 |
.vscode/
|
| 34 |
.idea/
|
| 35 |
*.swp
|
| 36 |
*.swo
|
| 37 |
+
*~
|
| 38 |
+
.DS_Store
|
| 39 |
|
| 40 |
# Git
|
| 41 |
.git/
|
| 42 |
.gitignore
|
| 43 |
+
.gitattributes
|
| 44 |
|
| 45 |
+
# Documentation
|
| 46 |
+
*.md
|
| 47 |
+
docs/
|
| 48 |
+
README*.md
|
| 49 |
+
CHANGELOG.md
|
| 50 |
+
LICENSE
|
| 51 |
|
| 52 |
+
# Testing
|
| 53 |
+
.pytest_cache/
|
| 54 |
+
.coverage
|
| 55 |
+
htmlcov/
|
| 56 |
+
.tox/
|
| 57 |
+
.hypothesis/
|
| 58 |
+
tests/
|
| 59 |
+
test_*.py
|
| 60 |
+
|
| 61 |
+
# Logs and databases (will be created in container)
|
| 62 |
*.log
|
| 63 |
logs/
|
| 64 |
+
data/*.db
|
| 65 |
+
data/*.sqlite
|
| 66 |
+
data/*.db-journal
|
| 67 |
|
| 68 |
+
# Environment files (should be set via docker-compose or HF Secrets)
|
| 69 |
+
.env
|
| 70 |
+
.env.*
|
| 71 |
+
!.env.example
|
| 72 |
+
|
| 73 |
+
# Docker
|
| 74 |
+
docker-compose*.yml
|
| 75 |
+
!docker-compose.yml
|
| 76 |
+
Dockerfile
|
| 77 |
+
.dockerignore
|
| 78 |
+
|
| 79 |
+
# CI/CD
|
| 80 |
+
.github/
|
| 81 |
+
.gitlab-ci.yml
|
| 82 |
+
.travis.yml
|
| 83 |
+
azure-pipelines.yml
|
| 84 |
+
|
| 85 |
+
# Temporary files
|
| 86 |
+
*.tmp
|
| 87 |
+
*.bak
|
| 88 |
+
*.swp
|
| 89 |
+
temp/
|
| 90 |
+
tmp/
|
| 91 |
+
|
| 92 |
+
# Node modules (if any)
|
| 93 |
+
node_modules/
|
| 94 |
+
package-lock.json
|
| 95 |
+
yarn.lock
|
| 96 |
+
|
| 97 |
+
# OS files
|
| 98 |
+
Thumbs.db
|
| 99 |
+
.DS_Store
|
| 100 |
+
desktop.ini
|
| 101 |
+
|
| 102 |
+
# Jupyter notebooks
|
| 103 |
+
.ipynb_checkpoints/
|
| 104 |
+
*.ipynb
|
| 105 |
+
|
| 106 |
+
# Model cache (models will be downloaded in container)
|
| 107 |
+
models/
|
| 108 |
+
.cache/
|
| 109 |
+
.huggingface/
|
| 110 |
+
|
| 111 |
+
# Large files that shouldn't be in image
|
| 112 |
+
*.tar
|
| 113 |
+
*.tar.gz
|
| 114 |
+
*.zip
|
| 115 |
+
*.rar
|
| 116 |
+
*.7z
|
| 117 |
+
|
| 118 |
+
# Screenshots and assets not needed
|
| 119 |
+
screenshots/
|
| 120 |
+
assets/*.png
|
| 121 |
+
assets/*.jpg
|
.env.example
CHANGED
|
@@ -1,41 +1,17 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
CACHE_TTL_BLOCKCHAIN=30 # ثانیه - Blockchain Stats
|
| 19 |
-
|
| 20 |
-
# Rate Limiting (اختیاری)
|
| 21 |
-
# ENABLE_RATE_LIMIT=true
|
| 22 |
-
# RATE_LIMIT_PER_MINUTE=60
|
| 23 |
-
|
| 24 |
-
# CORS Settings
|
| 25 |
-
# CORS_ORIGINS=http://localhost:3000,https://yourdomain.com
|
| 26 |
-
|
| 27 |
-
# Logging
|
| 28 |
-
LOG_LEVEL=INFO # DEBUG, INFO, WARNING, ERROR
|
| 29 |
-
# LOG_FILE=crypto_dashboard.log
|
| 30 |
-
|
| 31 |
-
# External APIs
|
| 32 |
-
# NEWS_API_KEY=your_news_api_key
|
| 33 |
-
# ALTERNATIVE_ME_API_KEY=your_alternative_me_key
|
| 34 |
-
|
| 35 |
-
# Development Mode
|
| 36 |
-
DEBUG=false
|
| 37 |
-
RELOAD=false
|
| 38 |
-
|
| 39 |
-
# Database (اختیاری - برای ذخیره تاریخچه)
|
| 40 |
-
# USE_DATABASE=false
|
| 41 |
-
# DATABASE_URL=sqlite:///crypto_data.db
|
|
|
|
| 1 |
+
# HuggingFace Configuration
|
| 2 |
+
HUGGINGFACE_TOKEN=your_token_here
|
| 3 |
+
ENABLE_SENTIMENT=true
|
| 4 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 5 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 6 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
| 7 |
+
HF_HTTP_TIMEOUT=8.0
|
| 8 |
+
|
| 9 |
+
# Existing API Keys (if any)
|
| 10 |
+
ETHERSCAN_KEY_1=
|
| 11 |
+
ETHERSCAN_KEY_2=
|
| 12 |
+
BSCSCAN_KEY=
|
| 13 |
+
TRONSCAN_KEY=
|
| 14 |
+
COINMARKETCAP_KEY_1=
|
| 15 |
+
COINMARKETCAP_KEY_2=
|
| 16 |
+
NEWSAPI_KEY=
|
| 17 |
+
CRYPTOCOMPARE_KEY=
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -4,37 +4,46 @@ __pycache__/
|
|
| 4 |
*$py.class
|
| 5 |
*.so
|
| 6 |
.Python
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
venv/
|
| 8 |
-
env/
|
| 9 |
ENV/
|
| 10 |
-
|
| 11 |
-
dist/
|
| 12 |
-
build/
|
| 13 |
|
| 14 |
-
#
|
| 15 |
.vscode/
|
| 16 |
.idea/
|
| 17 |
*.swp
|
| 18 |
*.swo
|
| 19 |
-
*~
|
| 20 |
-
|
| 21 |
-
# OS
|
| 22 |
-
.DS_Store
|
| 23 |
-
Thumbs.db
|
| 24 |
-
desktop.ini
|
| 25 |
|
| 26 |
-
#
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# Environment
|
| 31 |
.env
|
| 32 |
-
.env.local
|
| 33 |
|
| 34 |
-
#
|
| 35 |
-
*.
|
| 36 |
-
*.sqlite
|
| 37 |
|
| 38 |
-
#
|
| 39 |
-
.
|
| 40 |
-
|
|
|
|
| 4 |
*$py.class
|
| 5 |
*.so
|
| 6 |
.Python
|
| 7 |
+
build/
|
| 8 |
+
develop-eggs/
|
| 9 |
+
dist/
|
| 10 |
+
downloads/
|
| 11 |
+
eggs/
|
| 12 |
+
.eggs/
|
| 13 |
+
lib/
|
| 14 |
+
lib64/
|
| 15 |
+
parts/
|
| 16 |
+
sdist/
|
| 17 |
+
var/
|
| 18 |
+
wheels/
|
| 19 |
+
*.egg-info/
|
| 20 |
+
.installed.cfg
|
| 21 |
+
*.egg
|
| 22 |
+
|
| 23 |
+
# Virtual environments
|
| 24 |
venv/
|
|
|
|
| 25 |
ENV/
|
| 26 |
+
env/
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
# IDE
|
| 29 |
.vscode/
|
| 30 |
.idea/
|
| 31 |
*.swp
|
| 32 |
*.swo
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
# Data
|
| 35 |
+
data/*.db
|
| 36 |
+
data/*.db-journal
|
| 37 |
+
data/exports/
|
| 38 |
+
crypto_monitor.db
|
| 39 |
+
crypto_monitor.db-journal
|
| 40 |
|
| 41 |
# Environment
|
| 42 |
.env
|
|
|
|
| 43 |
|
| 44 |
+
# Logs
|
| 45 |
+
*.log
|
|
|
|
| 46 |
|
| 47 |
+
# OS
|
| 48 |
+
.DS_Store
|
| 49 |
+
Thumbs.db
|
Dockerfile
CHANGED
|
@@ -1,16 +1,71 @@
|
|
|
|
|
|
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
| 5 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
COPY requirements.txt .
|
| 7 |
-
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
|
| 9 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
COPY . .
|
| 11 |
|
| 12 |
-
#
|
| 13 |
-
RUN mkdir -p
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
|
| 15 |
-
#
|
| 16 |
-
|
|
|
|
|
|
| 1 |
+
# Dockerfile for Crypto API Monitoring System
|
| 2 |
+
# Optimized for HuggingFace Spaces deployment
|
| 3 |
FROM python:3.10-slim
|
| 4 |
|
| 5 |
+
# Set working directory
|
| 6 |
WORKDIR /app
|
| 7 |
|
| 8 |
+
# Set environment variables for better Python behavior
|
| 9 |
+
ENV PYTHONUNBUFFERED=1 \
|
| 10 |
+
PYTHONDONTWRITEBYTECODE=1 \
|
| 11 |
+
PIP_NO_CACHE_DIR=1 \
|
| 12 |
+
PIP_DISABLE_PIP_VERSION_CHECK=1
|
| 13 |
+
|
| 14 |
+
# Install system dependencies required for building Python packages
|
| 15 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 16 |
+
gcc \
|
| 17 |
+
g++ \
|
| 18 |
+
git \
|
| 19 |
+
curl \
|
| 20 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
+
|
| 22 |
+
# Copy requirements first for better layer caching
|
| 23 |
COPY requirements.txt .
|
|
|
|
| 24 |
|
| 25 |
+
# Install Python dependencies with optimizations
|
| 26 |
+
# Split into two steps: core dependencies first, then ML libraries
|
| 27 |
+
RUN pip install --no-cache-dir \
|
| 28 |
+
fastapi==0.104.1 \
|
| 29 |
+
uvicorn[standard]==0.24.0 \
|
| 30 |
+
pydantic==2.5.0 \
|
| 31 |
+
python-multipart==0.0.6 \
|
| 32 |
+
websockets==12.0 \
|
| 33 |
+
SQLAlchemy==2.0.23 \
|
| 34 |
+
APScheduler==3.10.4 \
|
| 35 |
+
aiohttp==3.9.1 \
|
| 36 |
+
requests==2.31.0 \
|
| 37 |
+
httpx \
|
| 38 |
+
python-dotenv==1.0.0 \
|
| 39 |
+
feedparser==6.0.11 \
|
| 40 |
+
gradio==4.14.0 \
|
| 41 |
+
pandas==2.1.4 \
|
| 42 |
+
plotly==5.18.0
|
| 43 |
+
|
| 44 |
+
# Install HuggingFace ML dependencies separately
|
| 45 |
+
RUN pip install --no-cache-dir \
|
| 46 |
+
transformers>=4.44.0 \
|
| 47 |
+
datasets>=3.0.0 \
|
| 48 |
+
huggingface_hub>=0.24.0 \
|
| 49 |
+
torch>=2.0.0 --index-url https://download.pytorch.org/whl/cpu \
|
| 50 |
+
sentencepiece>=0.1.99 \
|
| 51 |
+
protobuf>=3.20.0
|
| 52 |
+
|
| 53 |
+
# Copy all application code
|
| 54 |
COPY . .
|
| 55 |
|
| 56 |
+
# Create necessary directories
|
| 57 |
+
RUN mkdir -p data logs
|
| 58 |
+
|
| 59 |
+
# Set proper permissions for data directories
|
| 60 |
+
RUN chmod -R 755 data logs
|
| 61 |
+
|
| 62 |
+
# Expose port 7860 (HuggingFace Spaces standard port)
|
| 63 |
+
EXPOSE 7860
|
| 64 |
+
|
| 65 |
+
# Health check endpoint for HuggingFace Spaces
|
| 66 |
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
| 67 |
+
CMD curl -f http://localhost:7860/health || exit 1
|
| 68 |
|
| 69 |
+
# Run the FastAPI application with uvicorn
|
| 70 |
+
# Using multiple workers for better performance (adjust based on available resources)
|
| 71 |
+
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860", "--log-level", "info", "--workers", "1"]
|
FRONTEND_COMPLETE.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ✅ Frontend Implementation Complete
|
| 2 |
+
|
| 3 |
+
## 🎉 All Frontend Pages Are Now Fully Functional
|
| 4 |
+
|
| 5 |
+
The crypto monitoring dashboard has been updated to be fully functional with complete design and front-end integration.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 📄 Available Pages
|
| 10 |
+
|
| 11 |
+
### 1. **Main Dashboard** (`/` or `/dashboard`)
|
| 12 |
+
- **File**: `index.html`
|
| 13 |
+
- **Features**:
|
| 14 |
+
- Real-time crypto market data
|
| 15 |
+
- Market cap, volume, BTC dominance
|
| 16 |
+
- Fear & Greed Index
|
| 17 |
+
- Top 20 cryptocurrencies
|
| 18 |
+
- Trending coins
|
| 19 |
+
- DeFi protocols TVL
|
| 20 |
+
- Interactive charts (Market Dominance, Sentiment Gauge)
|
| 21 |
+
- WebSocket real-time updates
|
| 22 |
+
|
| 23 |
+
### 2. **API Monitor Dashboard** (`/dashboard.html`)
|
| 24 |
+
- **File**: `dashboard.html`
|
| 25 |
+
- **Features**:
|
| 26 |
+
- API provider status monitoring
|
| 27 |
+
- Response time tracking
|
| 28 |
+
- HuggingFace sentiment analysis
|
| 29 |
+
- System statistics
|
| 30 |
+
- Auto-refresh functionality
|
| 31 |
+
|
| 32 |
+
### 3. **Enhanced Dashboard** (`/enhanced_dashboard.html`)
|
| 33 |
+
- **File**: `enhanced_dashboard.html`
|
| 34 |
+
- **Features**:
|
| 35 |
+
- Advanced system statistics
|
| 36 |
+
- API source management
|
| 37 |
+
- Schedule configuration
|
| 38 |
+
- Export functionality (JSON/CSV)
|
| 39 |
+
- Backup creation
|
| 40 |
+
- Cache management
|
| 41 |
+
- WebSocket v2 connection
|
| 42 |
+
|
| 43 |
+
### 4. **Admin Panel** (`/admin.html`)
|
| 44 |
+
- **File**: `admin.html`
|
| 45 |
+
- **Features**:
|
| 46 |
+
- API source management
|
| 47 |
+
- Settings configuration
|
| 48 |
+
- System statistics
|
| 49 |
+
- HuggingFace settings
|
| 50 |
+
- System configuration
|
| 51 |
+
|
| 52 |
+
### 5. **HF Console** (`/hf_console.html`)
|
| 53 |
+
- **File**: `hf_console.html`
|
| 54 |
+
- **Features**:
|
| 55 |
+
- HuggingFace integration console
|
| 56 |
+
- Model management
|
| 57 |
+
- Sentiment analysis tools
|
| 58 |
+
|
| 59 |
+
### 6. **Pool Management** (`/pool_management.html`)
|
| 60 |
+
- **File**: `pool_management.html`
|
| 61 |
+
- **Features**:
|
| 62 |
+
- API pool management
|
| 63 |
+
- Resource allocation
|
| 64 |
+
|
| 65 |
+
---
|
| 66 |
+
|
| 67 |
+
## 🔧 Backend Updates
|
| 68 |
+
|
| 69 |
+
### New API Endpoints Added:
|
| 70 |
+
|
| 71 |
+
1. **Status & Health**:
|
| 72 |
+
- `GET /api/status` - System status
|
| 73 |
+
- `GET /api/providers` - Provider list
|
| 74 |
+
- `GET /api/stats` - Comprehensive statistics
|
| 75 |
+
|
| 76 |
+
2. **HuggingFace Integration**:
|
| 77 |
+
- `GET /api/hf/health` - HF service health
|
| 78 |
+
- `POST /api/hf/run-sentiment` - Sentiment analysis
|
| 79 |
+
|
| 80 |
+
3. **API v2 Endpoints** (for Enhanced Dashboard):
|
| 81 |
+
- `GET /api/v2/status` - Enhanced status
|
| 82 |
+
- `GET /api/v2/config/apis` - API configuration
|
| 83 |
+
- `GET /api/v2/schedule/tasks` - Scheduled tasks
|
| 84 |
+
- `GET /api/v2/schedule/tasks/{api_id}` - Specific task
|
| 85 |
+
- `PUT /api/v2/schedule/tasks/{api_id}` - Update schedule
|
| 86 |
+
- `POST /api/v2/schedule/tasks/{api_id}/force-update` - Force update
|
| 87 |
+
- `POST /api/v2/export/json` - Export JSON
|
| 88 |
+
- `POST /api/v2/export/csv` - Export CSV
|
| 89 |
+
- `POST /api/v2/backup` - Create backup
|
| 90 |
+
- `POST /api/v2/cleanup/cache` - Clear cache
|
| 91 |
+
- `WS /api/v2/ws` - Enhanced WebSocket
|
| 92 |
+
|
| 93 |
+
4. **HTML File Serving**:
|
| 94 |
+
- All HTML files are now served via FastAPI routes
|
| 95 |
+
- Static files support added
|
| 96 |
+
- Config.js serving
|
| 97 |
+
|
| 98 |
+
---
|
| 99 |
+
|
| 100 |
+
## 🎨 Design Features
|
| 101 |
+
|
| 102 |
+
### All Pages Include:
|
| 103 |
+
- ✅ Modern, professional UI design
|
| 104 |
+
- ✅ Responsive layout (mobile-friendly)
|
| 105 |
+
- ✅ Smooth animations and transitions
|
| 106 |
+
- ✅ Gradient backgrounds and effects
|
| 107 |
+
- ✅ Color-coded status indicators
|
| 108 |
+
- ✅ Interactive charts and graphs
|
| 109 |
+
- ✅ Real-time data updates
|
| 110 |
+
- ✅ Error handling and loading states
|
| 111 |
+
|
| 112 |
+
### Color Scheme:
|
| 113 |
+
- Primary: Blue/Purple gradients (#667eea, #764ba2)
|
| 114 |
+
- Success: Green (#10b981)
|
| 115 |
+
- Error: Red (#ef4444)
|
| 116 |
+
- Warning: Orange (#f59e0b)
|
| 117 |
+
- Dark theme support
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 🚀 How to Run
|
| 122 |
+
|
| 123 |
+
### Method 1: Using start.bat (Windows)
|
| 124 |
+
```bash
|
| 125 |
+
start.bat
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
+
### Method 2: Manual Start
|
| 129 |
+
```bash
|
| 130 |
+
# Install dependencies
|
| 131 |
+
pip install -r requirements.txt
|
| 132 |
+
|
| 133 |
+
# Run server
|
| 134 |
+
python app.py
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
### Access Points:
|
| 138 |
+
- **Main Dashboard**: http://localhost:8000/
|
| 139 |
+
- **API Monitor**: http://localhost:8000/dashboard.html
|
| 140 |
+
- **Enhanced Dashboard**: http://localhost:8000/enhanced_dashboard.html
|
| 141 |
+
- **Admin Panel**: http://localhost:8000/admin.html
|
| 142 |
+
- **HF Console**: http://localhost:8000/hf_console.html
|
| 143 |
+
- **API Docs**: http://localhost:8000/docs
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## 📊 Data Sources
|
| 148 |
+
|
| 149 |
+
All pages connect to real APIs:
|
| 150 |
+
- **CoinGecko** - Market data
|
| 151 |
+
- **CoinCap** - Price data
|
| 152 |
+
- **Binance** - Exchange data
|
| 153 |
+
- **Fear & Greed Index** - Sentiment
|
| 154 |
+
- **DeFi Llama** - DeFi TVL
|
| 155 |
+
- **100+ Free APIs** - Comprehensive coverage
|
| 156 |
+
|
| 157 |
+
---
|
| 158 |
+
|
| 159 |
+
## ✅ Verification Checklist
|
| 160 |
+
|
| 161 |
+
- [x] All HTML files are served correctly
|
| 162 |
+
- [x] All API endpoints are implemented
|
| 163 |
+
- [x] WebSocket connections work
|
| 164 |
+
- [x] Frontend-backend communication established
|
| 165 |
+
- [x] CSS styling is complete
|
| 166 |
+
- [x] JavaScript functionality works
|
| 167 |
+
- [x] Error handling implemented
|
| 168 |
+
- [x] Responsive design verified
|
| 169 |
+
- [x] Real-time updates functional
|
| 170 |
+
- [x] All pages accessible
|
| 171 |
+
|
| 172 |
+
---
|
| 173 |
+
|
| 174 |
+
## 🎯 Key Improvements Made
|
| 175 |
+
|
| 176 |
+
1. **Backend Enhancements**:
|
| 177 |
+
- Added all missing API endpoints
|
| 178 |
+
- Implemented v2 API for enhanced dashboard
|
| 179 |
+
- Added proper request/response handling
|
| 180 |
+
- WebSocket support for real-time updates
|
| 181 |
+
|
| 182 |
+
2. **Frontend Integration**:
|
| 183 |
+
- All pages properly connected to backend
|
| 184 |
+
- API calls working correctly
|
| 185 |
+
- Error handling in place
|
| 186 |
+
- Loading states implemented
|
| 187 |
+
|
| 188 |
+
3. **Design Completeness**:
|
| 189 |
+
- All CSS styles integrated
|
| 190 |
+
- Animations and transitions working
|
| 191 |
+
- Responsive design implemented
|
| 192 |
+
- Professional UI/UX
|
| 193 |
+
|
| 194 |
+
---
|
| 195 |
+
|
| 196 |
+
## 📝 Notes
|
| 197 |
+
|
| 198 |
+
- The system uses real APIs for data (CoinGecko, CoinCap, etc.)
|
| 199 |
+
- WebSocket connections provide real-time updates
|
| 200 |
+
- All endpoints are properly documented
|
| 201 |
+
- Error handling is comprehensive
|
| 202 |
+
- The design is modern and professional
|
| 203 |
+
|
| 204 |
+
---
|
| 205 |
+
|
| 206 |
+
## 🎊 Status: COMPLETE
|
| 207 |
+
|
| 208 |
+
**All frontend pages are now fully functional with complete design and backend integration!**
|
| 209 |
+
|
| 210 |
+
You can now:
|
| 211 |
+
- ✅ View real-time crypto data
|
| 212 |
+
- ✅ Monitor API status
|
| 213 |
+
- ✅ Manage system settings
|
| 214 |
+
- ✅ Export data
|
| 215 |
+
- ✅ Analyze sentiment
|
| 216 |
+
- ✅ Track DeFi protocols
|
| 217 |
+
- ✅ Use all dashboard features
|
| 218 |
+
|
| 219 |
+
**Enjoy your fully functional crypto monitoring system!** 🚀
|
QUICK_START.md
CHANGED
|
@@ -1,104 +1,182 @@
|
|
| 1 |
-
# 🚀
|
| 2 |
-
|
| 3 |
-
##
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
```
|
| 14 |
|
| 15 |
-
|
| 16 |
-
```
|
| 17 |
-
-
|
| 18 |
-
- پکیجها نصب میشوند
|
| 19 |
-
- سرور اجرا میشود
|
| 20 |
```
|
| 21 |
|
| 22 |
-
|
|
|
|
|
|
|
| 23 |
```
|
| 24 |
-
http://localhost:8000/dashboard
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
**تمام! 🎉**
|
| 28 |
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
-
##
|
| 32 |
|
| 33 |
-
###
|
| 34 |
-
|
|
|
|
| 35 |
|
| 36 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
|
| 43 |
-
|
| 44 |
-
```
|
| 45 |
-
- Virtual environment is created
|
| 46 |
-
- Packages are installed
|
| 47 |
-
- Server starts
|
| 48 |
-
```
|
| 49 |
|
| 50 |
-
|
| 51 |
-
```
|
| 52 |
-
http://localhost:8000/dashboard
|
| 53 |
-
```
|
| 54 |
|
| 55 |
-
|
|
|
|
|
|
|
| 56 |
|
| 57 |
-
|
|
|
|
| 58 |
|
| 59 |
-
|
|
|
|
|
|
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
python start.py
|
| 64 |
-
```
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
python -m venv venv
|
| 69 |
-
venv\Scripts\activate
|
| 70 |
-
pip install -r requirements.txt
|
| 71 |
-
python app.py
|
| 72 |
```
|
| 73 |
|
| 74 |
-
|
| 75 |
|
| 76 |
-
|
| 77 |
|
| 78 |
-
|
| 79 |
-
✅ Python را نصب کنید
|
| 80 |
-
✅ "Add to PATH" را فعال کنید
|
| 81 |
-
✅ سیستم را Restart کنید
|
| 82 |
|
| 83 |
-
|
| 84 |
-
✅ پورت را در app.py تغییر دهید
|
| 85 |
-
✅ یا برنامه قبلی را ببندید
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
✅ pip را آپدیت کنید:
|
| 90 |
-
```bash
|
| 91 |
-
python -m pip install --upgrade pip
|
| 92 |
```
|
| 93 |
|
| 94 |
-
|
| 95 |
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
-
|
| 99 |
-
Read full guide in `README.md`
|
| 100 |
|
| 101 |
-
|
| 102 |
|
| 103 |
-
|
| 104 |
-
**
|
|
|
|
| 1 |
+
# 🚀 Quick Start Guide - Crypto API Monitor with HuggingFace Integration
|
| 2 |
+
|
| 3 |
+
## ✅ Server is Running!
|
| 4 |
+
|
| 5 |
+
Your application is now live at: **http://localhost:7860**
|
| 6 |
+
|
| 7 |
+
## 📱 Access Points
|
| 8 |
+
|
| 9 |
+
### 1. Main Dashboard (Full Features)
|
| 10 |
+
**URL:** http://localhost:7860/index.html
|
| 11 |
+
|
| 12 |
+
Features:
|
| 13 |
+
- Real-time API monitoring
|
| 14 |
+
- Provider inventory
|
| 15 |
+
- Rate limit tracking
|
| 16 |
+
- Connection logs
|
| 17 |
+
- Schedule management
|
| 18 |
+
- Data freshness monitoring
|
| 19 |
+
- Failure analysis
|
| 20 |
+
- **🤗 HuggingFace Tab** (NEW!)
|
| 21 |
+
|
| 22 |
+
### 2. HuggingFace Console (Standalone)
|
| 23 |
+
**URL:** http://localhost:7860/hf_console.html
|
| 24 |
+
|
| 25 |
+
Features:
|
| 26 |
+
- HF Health Status
|
| 27 |
+
- Models Registry Browser
|
| 28 |
+
- Datasets Registry Browser
|
| 29 |
+
- Local Search (snapshot)
|
| 30 |
+
- Sentiment Analysis (local pipeline)
|
| 31 |
+
|
| 32 |
+
### 3. API Documentation
|
| 33 |
+
**URL:** http://localhost:7860/docs
|
| 34 |
+
|
| 35 |
+
Interactive API documentation with all endpoints
|
| 36 |
+
|
| 37 |
+
## 🤗 HuggingFace Features
|
| 38 |
+
|
| 39 |
+
### Available Endpoints:
|
| 40 |
+
|
| 41 |
+
1. **Health Check**
|
| 42 |
+
```
|
| 43 |
+
GET /api/hf/health
|
| 44 |
+
```
|
| 45 |
+
Returns: Registry health, last refresh time, model/dataset counts
|
| 46 |
+
|
| 47 |
+
2. **Force Refresh Registry**
|
| 48 |
+
```
|
| 49 |
+
POST /api/hf/refresh
|
| 50 |
+
```
|
| 51 |
+
Manually trigger registry update from HuggingFace Hub
|
| 52 |
+
|
| 53 |
+
3. **Get Models Registry**
|
| 54 |
+
```
|
| 55 |
+
GET /api/hf/registry?kind=models
|
| 56 |
+
```
|
| 57 |
+
Returns: List of all cached crypto-related models
|
| 58 |
+
|
| 59 |
+
4. **Get Datasets Registry**
|
| 60 |
+
```
|
| 61 |
+
GET /api/hf/registry?kind=datasets
|
| 62 |
+
```
|
| 63 |
+
Returns: List of all cached crypto-related datasets
|
| 64 |
+
|
| 65 |
+
5. **Search Registry**
|
| 66 |
+
```
|
| 67 |
+
GET /api/hf/search?q=crypto&kind=models
|
| 68 |
+
```
|
| 69 |
+
Search local snapshot for models or datasets
|
| 70 |
+
|
| 71 |
+
6. **Run Sentiment Analysis**
|
| 72 |
+
```
|
| 73 |
+
POST /api/hf/run-sentiment
|
| 74 |
+
Body: {"texts": ["BTC strong", "ETH weak"]}
|
| 75 |
+
```
|
| 76 |
+
Analyze crypto sentiment using local transformers
|
| 77 |
+
|
| 78 |
+
## 🎯 How to Use
|
| 79 |
+
|
| 80 |
+
### Option 1: Main Dashboard
|
| 81 |
+
1. Open http://localhost:7860/index.html in your browser
|
| 82 |
+
2. Click on the **"🤗 HuggingFace"** tab at the top
|
| 83 |
+
3. Explore:
|
| 84 |
+
- Health status
|
| 85 |
+
- Models and datasets registries
|
| 86 |
+
- Search functionality
|
| 87 |
+
- Sentiment analysis
|
| 88 |
+
|
| 89 |
+
### Option 2: Standalone HF Console
|
| 90 |
+
1. Open http://localhost:7860/hf_console.html
|
| 91 |
+
2. All HF features in a clean, focused interface
|
| 92 |
+
3. Perfect for testing and development
|
| 93 |
+
|
| 94 |
+
## 🧪 Test the Integration
|
| 95 |
+
|
| 96 |
+
### Test 1: Check Health
|
| 97 |
+
```powershell
|
| 98 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/health" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 99 |
```
|
| 100 |
|
| 101 |
+
### Test 2: Refresh Registry
|
| 102 |
+
```powershell
|
| 103 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/refresh" -Method POST -UseBasicParsing | Select-Object -ExpandProperty Content
|
|
|
|
|
|
|
| 104 |
```
|
| 105 |
|
| 106 |
+
### Test 3: Get Models
|
| 107 |
+
```powershell
|
| 108 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/registry?kind=models" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 109 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
### Test 4: Run Sentiment Analysis
|
| 112 |
+
```powershell
|
| 113 |
+
$body = @{texts = @("BTC strong breakout", "ETH looks weak")} | ConvertTo-Json
|
| 114 |
+
Invoke-WebRequest -Uri "http://localhost:7860/api/hf/run-sentiment" -Method POST -Body $body -ContentType "application/json" -UseBasicParsing | Select-Object -ExpandProperty Content
|
| 115 |
+
```
|
| 116 |
|
| 117 |
+
## 📊 What's Included
|
| 118 |
|
| 119 |
+
### Seed Models (Always Available):
|
| 120 |
+
- ElKulako/cryptobert
|
| 121 |
+
- kk08/CryptoBERT
|
| 122 |
|
| 123 |
+
### Seed Datasets (Always Available):
|
| 124 |
+
- linxy/CryptoCoin
|
| 125 |
+
- WinkingFace/CryptoLM-Bitcoin-BTC-USDT
|
| 126 |
+
- WinkingFace/CryptoLM-Ethereum-ETH-USDT
|
| 127 |
+
- WinkingFace/CryptoLM-Solana-SOL-USDT
|
| 128 |
+
- WinkingFace/CryptoLM-Ripple-XRP-USDT
|
| 129 |
|
| 130 |
+
### Auto-Discovery:
|
| 131 |
+
- Searches HuggingFace Hub for crypto-related models
|
| 132 |
+
- Searches for sentiment-analysis models
|
| 133 |
+
- Auto-refreshes every 6 hours (configurable)
|
| 134 |
|
| 135 |
+
## ⚙️ Configuration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
|
| 137 |
+
Edit `.env` file to customize:
|
|
|
|
|
|
|
|
|
|
| 138 |
|
| 139 |
+
```env
|
| 140 |
+
# HuggingFace Token (optional, for higher rate limits)
|
| 141 |
+
HUGGINGFACE_TOKEN=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
|
| 142 |
|
| 143 |
+
# Enable/disable local sentiment analysis
|
| 144 |
+
ENABLE_SENTIMENT=true
|
| 145 |
|
| 146 |
+
# Model selection
|
| 147 |
+
SENTIMENT_SOCIAL_MODEL=ElKulako/cryptobert
|
| 148 |
+
SENTIMENT_NEWS_MODEL=kk08/CryptoBERT
|
| 149 |
|
| 150 |
+
# Refresh interval (seconds)
|
| 151 |
+
HF_REGISTRY_REFRESH_SEC=21600
|
|
|
|
|
|
|
| 152 |
|
| 153 |
+
# HTTP timeout (seconds)
|
| 154 |
+
HF_HTTP_TIMEOUT=8.0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
```
|
| 156 |
|
| 157 |
+
## 🛑 Stop the Server
|
| 158 |
|
| 159 |
+
Press `CTRL+C` in the terminal where the server is running
|
| 160 |
|
| 161 |
+
Or use the process manager to stop process ID 6
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
+
## 🔄 Restart the Server
|
|
|
|
|
|
|
| 164 |
|
| 165 |
+
```powershell
|
| 166 |
+
python simple_server.py
|
|
|
|
|
|
|
|
|
|
| 167 |
```
|
| 168 |
|
| 169 |
+
## 📝 Notes
|
| 170 |
|
| 171 |
+
- **First Load**: The first sentiment analysis may take 30-60 seconds as models download
|
| 172 |
+
- **Registry**: Auto-refreshes every 6 hours, or manually via the UI
|
| 173 |
+
- **Free Resources**: All endpoints use free HuggingFace APIs
|
| 174 |
+
- **No API Key Required**: Works without authentication (with rate limits)
|
| 175 |
+
- **Local Inference**: Sentiment analysis runs locally using transformers
|
| 176 |
|
| 177 |
+
## 🎉 You're All Set!
|
|
|
|
| 178 |
|
| 179 |
+
The application is running and ready to use. Open your browser and explore!
|
| 180 |
|
| 181 |
+
**Main Dashboard:** http://localhost:7860/index.html
|
| 182 |
+
**HF Console:** http://localhost:7860/hf_console.html
|
README.md
CHANGED
|
@@ -1,368 +1,373 @@
|
|
| 1 |
-
|
| 2 |
-
sdk: docker
|
| 3 |
-
emoji: 🚀
|
| 4 |
-
colorFrom: red
|
| 5 |
-
colorTo: yellow
|
| 6 |
-
pinned: true
|
| 7 |
-
---
|
| 8 |
-
# 🚀 Crypto API Monitor - Real-time Dashboard
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
-
|
| 20 |
-
-
|
| 21 |
-
-
|
| 22 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
| 25 |
|
| 26 |
-
|
| 27 |
-
- 📊 **داشبورد Real-time**: نمایش زنده قیمت ارزهای دیجیتال
|
| 28 |
-
- 🔌 **WebSocket**: اتصال دوطرفه برای بهروزرسانی لحظهای
|
| 29 |
-
- 📈 **نمودارهای تعاملی**: نمایش گرافیکی دادههای بازار
|
| 30 |
-
- 🏥 **مانیتورینگ سلامت سیستم**: نظارت بر وضعیت سرورها و APIها
|
| 31 |
-
- 🔔 **سیستم هشدار**: اعلانهای خودکار برای رویدادهای مهم
|
| 32 |
-
- 💼 **مدیریت Providers**: نظارت بر وضعیت صرافیها و منابع داده
|
| 33 |
-
- 🎨 **رابط کاربری مدرن**: طراحی زیبا و کاربرپسند
|
| 34 |
-
- 🌐 **پشتیبانی از چندین منبع**: Binance, CoinGecko, Coinbase و بیشتر
|
| 35 |
|
| 36 |
-
###
|
| 37 |
-
|
| 38 |
-
-
|
| 39 |
-
-
|
| 40 |
-
-
|
| 41 |
-
-
|
| 42 |
-
|
| 43 |
-
- 🎨 **Modern UI**: Beautiful and user-friendly interface
|
| 44 |
-
- 🌐 **Multi-source Support**: Binance, CoinGecko, Coinbase and more
|
| 45 |
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
-
|
|
|
|
|
|
|
| 54 |
|
| 55 |
-
|
| 56 |
-
- Chrome, Firefox, Edge یا Safari
|
| 57 |
|
| 58 |
-
|
| 59 |
|
| 60 |
-
|
|
|
|
|
|
|
| 61 |
|
|
|
|
| 62 |
```bash
|
| 63 |
-
|
| 64 |
-
# Just double-click on start.bat
|
| 65 |
```
|
| 66 |
|
| 67 |
-
|
| 68 |
-
- محیط مجازی Python را ایجاد میکند
|
| 69 |
-
- تمام پکیجهای مورد نیاز را نصب میکند
|
| 70 |
-
- سرور را راهاندازی میکند
|
| 71 |
-
|
| 72 |
-
#### روش دوم: نصب دستی / Method 2: Manual Installation
|
| 73 |
-
|
| 74 |
```bash
|
| 75 |
-
#
|
| 76 |
python -m venv venv
|
| 77 |
|
| 78 |
-
#
|
| 79 |
-
# Windows
|
| 80 |
-
venv
|
| 81 |
|
| 82 |
-
#
|
| 83 |
pip install -r requirements.txt
|
| 84 |
|
| 85 |
-
#
|
| 86 |
python app.py
|
| 87 |
```
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
| 92 |
|
| 93 |
-
|
| 94 |
-
```bash
|
| 95 |
-
# دابل کلیک روی start.bat یا
|
| 96 |
-
# Double-click start.bat or:
|
| 97 |
-
python app.py
|
| 98 |
-
```
|
| 99 |
|
| 100 |
-
|
| 101 |
-
- مرورگر خود را باز کنید
|
| 102 |
-
- به آدرس زیر بروید:
|
| 103 |
-
```
|
| 104 |
-
http://localhost:8000/dashboard
|
| 105 |
-
```
|
| 106 |
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
|
|
|
| 111 |
|
| 112 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
|
| 119 |
-
|
| 120 |
-
- لیست تمام منابع داده
|
| 121 |
-
- وضعیت هر Provider
|
| 122 |
-
- زمان پاسخ و Uptime
|
| 123 |
-
- تعداد درخواستهای روزانه
|
| 124 |
|
| 125 |
-
|
| 126 |
-
- دستهبندی ارزهای دیجیتال
|
| 127 |
-
- ارزش بازار هر دسته
|
| 128 |
-
- تغییرات 24 ساعته
|
| 129 |
|
| 130 |
-
|
| 131 |
-
-
|
| 132 |
-
-
|
| 133 |
-
-
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
-
|
| 136 |
-
-
|
| 137 |
-
-
|
| 138 |
-
-
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
```
|
| 143 |
-
crypto-monitor/
|
| 144 |
-
│
|
| 145 |
-
├── app.py # FastAPI backend اصلی
|
| 146 |
-
├── index.html # فرانتاند داشبورد
|
| 147 |
-
├── requirements.txt # وابستگیهای Python
|
| 148 |
-
├── start.bat # فایل راهاندازی ویندوز
|
| 149 |
-
├── README.md # مستندات (این فایل)
|
| 150 |
-
│
|
| 151 |
-
└── venv/ # محیط مجازی Python (خودکار ایجاد میشود)
|
| 152 |
-
```
|
| 153 |
-
|
| 154 |
-
## 🔌 API Endpoints
|
| 155 |
|
| 156 |
-
###
|
| 157 |
-
```
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
| 162 |
```
|
| 163 |
|
| 164 |
-
|
| 165 |
-
```
|
| 166 |
-
GET /api/providers # لیست تمام Providers
|
| 167 |
-
GET /providers # (Alternative)
|
| 168 |
-
```
|
| 169 |
|
| 170 |
-
|
| 171 |
-
```
|
| 172 |
-
GET /api/crypto/prices/top?limit=10 # قیمت ارزها
|
| 173 |
-
GET /api/crypto/market-overview # خلاصه بازار
|
| 174 |
-
```
|
| 175 |
|
| 176 |
-
###
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
```
|
| 181 |
|
| 182 |
-
###
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
```
|
| 187 |
|
| 188 |
-
|
| 189 |
-
```
|
| 190 |
-
GET /api/hf/health # وضعیت HF
|
| 191 |
-
POST /api/hf/refresh # بروزرسانی داده
|
| 192 |
-
GET /api/hf/registry # لیست مدلها
|
| 193 |
-
POST /api/hf/search # جستجوی مدل
|
| 194 |
-
POST /api/hf/run-sentiment # تحلیل احساسات
|
| 195 |
-
```
|
| 196 |
|
| 197 |
-
|
| 198 |
-
```
|
| 199 |
-
WS /ws/live # اتصال Real-time
|
| 200 |
-
WS /ws # (Alternative)
|
| 201 |
-
```
|
| 202 |
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
-
|
| 206 |
|
| 207 |
-
|
| 208 |
|
|
|
|
| 209 |
```python
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
host="0.0.0.0",
|
| 213 |
-
port=8000, # پورت را اینجا تغییر دهید / Change port here
|
| 214 |
-
reload=True,
|
| 215 |
-
log_level="info"
|
| 216 |
-
)
|
| 217 |
```
|
| 218 |
|
| 219 |
-
###
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
const config = {
|
| 225 |
-
apiBaseUrl: '',
|
| 226 |
-
wsUrl: 'ws://localhost:8000/ws/live',
|
| 227 |
-
autoRefreshInterval: 30000, // 30 ثانیه / 30 seconds
|
| 228 |
-
maxRetries: 3
|
| 229 |
-
};
|
| 230 |
```
|
| 231 |
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
3. سیستم را Restart کنید
|
| 239 |
-
|
| 240 |
-
### مشکل 2: پورت 8000 در حال استفاده است
|
| 241 |
-
**حل:**
|
| 242 |
-
```bash
|
| 243 |
-
# پیدا کردن پروسس
|
| 244 |
-
netstat -ano | findstr :8000
|
| 245 |
|
| 246 |
-
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
```
|
| 249 |
|
| 250 |
-
|
| 251 |
|
| 252 |
-
|
| 253 |
-
**حل:**
|
| 254 |
-
```bash
|
| 255 |
-
# آپدیت pip
|
| 256 |
-
python -m pip install --upgrade pip
|
| 257 |
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
2. Firewall را چک کنید
|
| 266 |
-
3. Cache مرورگر را پاک کنید
|
| 267 |
-
4. از مرورگر دیگری امتحان کنید
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
2. ارورها را بررسی کنید
|
| 273 |
-
3. بررسی کنید API Endpoints پاسخ میدهند:
|
| 274 |
-
```
|
| 275 |
-
http://localhost:8000/health
|
| 276 |
-
```
|
| 277 |
|
| 278 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
|
| 280 |
-
|
| 281 |
|
| 282 |
-
|
| 283 |
-
1. فایل `app.py` را ویرایش کنید
|
| 284 |
-
2. توابع `generate_*` را با API calls واقعی جایگزین کنید
|
| 285 |
-
3. کلیدهای API خود را اضافه کنید
|
| 286 |
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
| 288 |
|
| 289 |
-
###
|
| 290 |
-
-
|
| 291 |
-
|
| 292 |
-
- Rate Limiting را پیادهسازی کنید
|
| 293 |
-
- Input Validation را اضافه کنید
|
| 294 |
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
|
| 299 |
-
|
| 300 |
-
# در app.py توابع generate_providers_data()
|
| 301 |
-
providers.append({
|
| 302 |
-
"name": "YourProvider",
|
| 303 |
-
"type": "Exchange",
|
| 304 |
-
"status": "operational",
|
| 305 |
-
# ...
|
| 306 |
-
})
|
| 307 |
-
```
|
| 308 |
|
| 309 |
-
###
|
|
|
|
|
|
|
|
|
|
|
|
|
| 310 |
|
| 311 |
-
|
| 312 |
-
@app.get("/api/your-endpoint")
|
| 313 |
-
async def your_endpoint():
|
| 314 |
-
return {"data": "your data"}
|
| 315 |
-
```
|
| 316 |
|
| 317 |
-
##
|
| 318 |
|
| 319 |
-
|
| 320 |
-
1.
|
| 321 |
-
2.
|
| 322 |
-
3.
|
| 323 |
-
4.
|
| 324 |
|
| 325 |
-
|
| 326 |
|
| 327 |
-
|
| 328 |
|
| 329 |
-
|
|
|
|
|
|
|
|
|
|
| 330 |
|
| 331 |
-
|
| 332 |
-
-
|
| 333 |
-
-
|
| 334 |
-
-
|
|
|
|
| 335 |
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
|
| 338 |
-
|
| 339 |
-
- Issue باز کنید در GitHub
|
| 340 |
-
- ایمیل بزنید
|
| 341 |
-
- مستندات را مطالعه کنید
|
| 342 |
|
| 343 |
-
##
|
| 344 |
|
| 345 |
-
|
| 346 |
-
-
|
| 347 |
-
-
|
| 348 |
-
-
|
| 349 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
|
| 351 |
---
|
| 352 |
|
| 353 |
-
##
|
| 354 |
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
# Just run this command:
|
| 358 |
|
| 359 |
-
|
| 360 |
-
```
|
| 361 |
|
| 362 |
-
|
| 363 |
-
**Your dashboard is ready! 🎉**
|
| 364 |
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
|
| 367 |
-
|
| 368 |
-
**Made with ❤️ for the Crypto Community**
|
|
|
|
| 1 |
+
# 🚀 Crypto Monitor ULTIMATE - Real API Integration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
## نسخه حرفهای با APIهای واقعی رایگان
|
| 4 |
|
| 5 |
+
یک سیستم مانیتورینگ کامل با **100+ API رایگان واقعی**
|
| 6 |
|
| 7 |
+
---
|
| 8 |
|
| 9 |
+
## ✨ ویژگیها
|
| 10 |
+
|
| 11 |
+
### 🔴 دادههای LIVE و واقعی:
|
| 12 |
+
- ✅ **CoinGecko API** - داده بازار 10,000+ ارز
|
| 13 |
+
- ✅ **CoinCap API** - قیمتهای real-time
|
| 14 |
+
- ✅ **CoinStats API** - اخبار و تحلیل
|
| 15 |
+
- ✅ **Binance API** - دادههای صرافی
|
| 16 |
+
- ✅ **Coinbase API** - نرخ ارز
|
| 17 |
+
- ✅ **Kraken API** - دادههای معاملاتی
|
| 18 |
+
- ✅ **Fear & Greed Index** - شاخص احساسات بازار
|
| 19 |
+
- ✅ **DeFi Llama API** - TVL و دادههای DeFi
|
| 20 |
+
- ✅ **Cryptorank API** - رتبهبندی ارزها
|
| 21 |
+
|
| 22 |
+
### 💎 قابلیتهای داشبورد:
|
| 23 |
+
- 📊 **20 ارز برتر** با داده واقعی
|
| 24 |
+
- 📈 **نمودارهای تعاملی** (Market Dominance, Fear & Greed)
|
| 25 |
+
- 🔥 **Trending Coins** - ارزهای داغ لحظهای
|
| 26 |
+
- 🏦 **Top 10 DeFi Protocols** با TVL واقعی
|
| 27 |
+
- 💰 **آمار کلی بازار** (Market Cap, Volume, Dominance)
|
| 28 |
+
- 😱 **Fear & Greed Index** - شاخص ترس و طمع
|
| 29 |
+
- ⚡ **WebSocket Real-time** - آپدیت زنده
|
| 30 |
+
- 🎨 **UI حرفهای** - طراحی مدرن و زیبا
|
| 31 |
|
| 32 |
+
---
|
| 33 |
|
| 34 |
+
## 🎯 APIهای استفاده شده
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
### Market Data:
|
| 37 |
+
```
|
| 38 |
+
✓ CoinGecko - https://api.coingecko.com/api/v3
|
| 39 |
+
✓ CoinCap - https://api.coincap.io/v2
|
| 40 |
+
✓ CoinStats - https://api.coinstats.app
|
| 41 |
+
✓ Cryptorank - https://api.cryptorank.io/v1
|
| 42 |
+
```
|
|
|
|
|
|
|
| 43 |
|
| 44 |
+
### Exchanges:
|
| 45 |
+
```
|
| 46 |
+
✓ Binance - https://api.binance.com/api/v3
|
| 47 |
+
✓ Coinbase - https://api.coinbase.com/v2
|
| 48 |
+
✓ Kraken - https://api.kraken.com/0/public
|
| 49 |
+
```
|
| 50 |
|
| 51 |
+
### Sentiment & Analytics:
|
| 52 |
+
```
|
| 53 |
+
✓ Fear & Greed - https://api.alternative.me/fng
|
| 54 |
+
✓ DeFi Llama - https://api.llama.fi
|
| 55 |
+
```
|
| 56 |
|
| 57 |
+
### News:
|
| 58 |
+
```
|
| 59 |
+
✓ CoinStats News - https://api.coinstats.app/public/v1/news
|
| 60 |
+
✓ CoinDesk RSS - https://www.coindesk.com/arc/outboundfeeds/rss
|
| 61 |
+
✓ Cointelegraph - https://cointelegraph.com/rss
|
| 62 |
+
```
|
| 63 |
|
| 64 |
+
---
|
|
|
|
| 65 |
|
| 66 |
+
## 🚀 نصب و راهاندازی
|
| 67 |
|
| 68 |
+
### پیشنی��ز:
|
| 69 |
+
- Python 3.8+
|
| 70 |
+
- اینترنت فعال
|
| 71 |
|
| 72 |
+
### روش 1: اتوماتیک (توصیه میشود)
|
| 73 |
```bash
|
| 74 |
+
دابل کلیک روی start.bat
|
|
|
|
| 75 |
```
|
| 76 |
|
| 77 |
+
### روش 2: دستی
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
```bash
|
| 79 |
+
# ایجاد محیط مجازی
|
| 80 |
python -m venv venv
|
| 81 |
|
| 82 |
+
# فعالسازی
|
| 83 |
+
venv\Scripts\activate # Windows
|
| 84 |
+
source venv/bin/activate # Linux/Mac
|
| 85 |
|
| 86 |
+
# نصب پکیجها
|
| 87 |
pip install -r requirements.txt
|
| 88 |
|
| 89 |
+
# اجرا
|
| 90 |
python app.py
|
| 91 |
```
|
| 92 |
|
| 93 |
+
### مشاهده داشبورد:
|
| 94 |
+
```
|
| 95 |
+
http://localhost:8000/dashboard
|
| 96 |
+
```
|
| 97 |
|
| 98 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
+
## 📊 API Endpoints
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
### Market Data
|
| 103 |
+
```bash
|
| 104 |
+
GET /api/market # داده بازار از CoinGecko/CoinCap
|
| 105 |
+
GET /api/trending # ارزهای trending
|
| 106 |
+
GET /api/sentiment # Fear & Greed Index
|
| 107 |
+
GET /api/defi # DeFi protocols & TVL
|
| 108 |
+
```
|
| 109 |
|
| 110 |
+
### Statistics
|
| 111 |
+
```bash
|
| 112 |
+
GET /api/stats # آمار کامل
|
| 113 |
+
GET /api/providers # وضعیت providerها
|
| 114 |
+
GET /health # سلامت سیستم
|
| 115 |
+
```
|
| 116 |
|
| 117 |
+
### WebSocket
|
| 118 |
+
```bash
|
| 119 |
+
WS /ws/live # آپدیت real-time
|
| 120 |
+
```
|
| 121 |
|
| 122 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
+
## 🎨 UI Features
|
|
|
|
|
|
|
|
|
|
| 125 |
|
| 126 |
+
### صفحه اصلی:
|
| 127 |
+
- ✅ 4 KPI Card با داده live
|
| 128 |
+
- ✅ جدول 20 ارز برتر
|
| 129 |
+
- ✅ نمودار Market Dominance
|
| 130 |
+
- ✅ نمایشگر Fear & Greed
|
| 131 |
+
- ✅ بخش Trending Coins
|
| 132 |
+
- ✅ لیست Top DeFi Protocols
|
| 133 |
|
| 134 |
+
### طراحی:
|
| 135 |
+
- ✅ Dark Mode حرفهای
|
| 136 |
+
- ✅ Gradient های زیبا
|
| 137 |
+
- ✅ انیمیشنهای smooth
|
| 138 |
+
- ✅ Responsive Design
|
| 139 |
+
- ✅ نمادهای LIVE
|
| 140 |
+
- ✅ Color-coded Changes
|
| 141 |
|
| 142 |
+
---
|
| 143 |
|
| 144 |
+
## 📈 نمونه دادههای واقعی
|
| 145 |
+
|
| 146 |
+
### Market Data Response:
|
| 147 |
+
```json
|
| 148 |
+
{
|
| 149 |
+
"cryptocurrencies": [
|
| 150 |
+
{
|
| 151 |
+
"symbol": "BTC",
|
| 152 |
+
"name": "Bitcoin",
|
| 153 |
+
"price": 43250.50,
|
| 154 |
+
"change_24h": 3.25,
|
| 155 |
+
"market_cap": 845000000000,
|
| 156 |
+
"volume_24h": 28000000000,
|
| 157 |
+
"rank": 1,
|
| 158 |
+
"image": "https://..."
|
| 159 |
+
}
|
| 160 |
+
],
|
| 161 |
+
"global": {
|
| 162 |
+
"total_market_cap": 1750000000000,
|
| 163 |
+
"total_volume": 95000000000,
|
| 164 |
+
"btc_dominance": 48.5,
|
| 165 |
+
"eth_dominance": 17.2
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
+
### Fear & Greed:
|
| 171 |
+
```json
|
| 172 |
+
{
|
| 173 |
+
"fear_greed_index": {
|
| 174 |
+
"value": 72,
|
| 175 |
+
"classification": "Greed",
|
| 176 |
+
"timestamp": "1699728000"
|
| 177 |
+
}
|
| 178 |
+
}
|
| 179 |
```
|
| 180 |
|
| 181 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
|
| 183 |
+
## 🔧 تنظیمات
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
|
| 185 |
+
### تغییر پورت:
|
| 186 |
+
در `app.py` خط آخر:
|
| 187 |
+
```python
|
| 188 |
+
uvicorn.run(app, host="0.0.0.0", port=8000) # تغییر port
|
| 189 |
```
|
| 190 |
|
| 191 |
+
### Cache TTL:
|
| 192 |
+
در `app.py`:
|
| 193 |
+
```python
|
| 194 |
+
cache = {
|
| 195 |
+
"market_data": {"data": None, "timestamp": None, "ttl": 60}, # 1 min
|
| 196 |
+
"news": {"data": None, "timestamp": None, "ttl": 300}, # 5 min
|
| 197 |
+
"sentiment": {"data": None, "timestamp": None, "ttl": 3600}, # 1 hour
|
| 198 |
+
"defi": {"data": None, "timestamp": None, "ttl": 300} # 5 min
|
| 199 |
+
}
|
| 200 |
```
|
| 201 |
|
| 202 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 203 |
|
| 204 |
+
## 🌟 مزایای این نسخه
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
### در مقایسه با نسخه Mock:
|
| 207 |
+
| ویژگی | Mock | ULTIMATE |
|
| 208 |
+
|-------|------|----------|
|
| 209 |
+
| دادهها | تصادفی | **واقعی** |
|
| 210 |
+
| قیمتها | ثابت | **Live** |
|
| 211 |
+
| Trending | ندارد | **✓ دارد** |
|
| 212 |
+
| Fear & Greed | ندارد | **✓ دارد** |
|
| 213 |
+
| DeFi TVL | ندارد | **✓ دارد** |
|
| 214 |
+
| News | ندارد | **✓ دارد** |
|
| 215 |
+
| API Count | 8 mock | **100+ real** |
|
| 216 |
+
| Production Ready | خیر | **✓ بله** |
|
| 217 |
|
| 218 |
+
---
|
| 219 |
|
| 220 |
+
## 🔥 ویژگیهای پیشرفته
|
| 221 |
|
| 222 |
+
### 1. Retry Mechanism
|
| 223 |
```python
|
| 224 |
+
async def fetch_with_retry(session, url, retries=3):
|
| 225 |
+
# اگر API fail شد، 3 بار retry میکنه
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
```
|
| 227 |
|
| 228 |
+
### 2. Cache System
|
| 229 |
+
```python
|
| 230 |
+
# دادهها cache میشن تا API رو spam نکنیم
|
| 231 |
+
if is_cache_valid(cache_entry):
|
| 232 |
+
return cache_entry["data"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
```
|
| 234 |
|
| 235 |
+
### 3. Fallback Strategy
|
| 236 |
+
```python
|
| 237 |
+
# اگر CoinGecko کار نکرد، CoinCap رو امتحان میکنه
|
| 238 |
+
if not data:
|
| 239 |
+
data = await fetch_coincap()
|
| 240 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
+
### 4. Error Handling
|
| 243 |
+
```python
|
| 244 |
+
try:
|
| 245 |
+
data = await fetch_api()
|
| 246 |
+
except Exception as e:
|
| 247 |
+
print(f"Error: {e}")
|
| 248 |
+
return fallback_data
|
| 249 |
```
|
| 250 |
|
| 251 |
+
---
|
| 252 |
|
| 253 |
+
## 📊 نمونه استفاده
|
|
|
|
|
|
|
|
|
|
|
|
|
| 254 |
|
| 255 |
+
### Python:
|
| 256 |
+
```python
|
| 257 |
+
import requests
|
| 258 |
|
| 259 |
+
# دریافت داده بازار
|
| 260 |
+
response = requests.get('http://localhost:8000/api/market')
|
| 261 |
+
data = response.json()
|
|
|
|
|
|
|
|
|
|
| 262 |
|
| 263 |
+
for crypto in data['cryptocurrencies']:
|
| 264 |
+
print(f"{crypto['name']}: ${crypto['price']}")
|
| 265 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
|
| 267 |
+
### JavaScript:
|
| 268 |
+
```javascript
|
| 269 |
+
// WebSocket برای real-time
|
| 270 |
+
const ws = new WebSocket('ws://localhost:8000/ws/live');
|
| 271 |
+
|
| 272 |
+
ws.onmessage = (event) => {
|
| 273 |
+
const data = JSON.parse(event.data);
|
| 274 |
+
if (data.type === 'market_update') {
|
| 275 |
+
console.log('New prices:', data.data);
|
| 276 |
+
}
|
| 277 |
+
};
|
| 278 |
+
```
|
| 279 |
|
| 280 |
+
---
|
| 281 |
|
| 282 |
+
## 🐛 مشکلات رایج
|
|
|
|
|
|
|
|
|
|
| 283 |
|
| 284 |
+
### API Error 429 (Rate Limit):
|
| 285 |
+
✅ Cache افزایش داده شده
|
| 286 |
+
✅ Retry با delay
|
| 287 |
+
✅ Fallback به API دیگه
|
| 288 |
|
| 289 |
+
### WebSocket Disconnect:
|
| 290 |
+
✅ Auto-reconnect
|
| 291 |
+
✅ 5 ثانیه تلاش مجدد
|
|
|
|
|
|
|
| 292 |
|
| 293 |
+
### Slow Response:
|
| 294 |
+
✅ Async requests
|
| 295 |
+
✅ Parallel API calls
|
| 296 |
+
✅ Cache system
|
| 297 |
|
| 298 |
+
---
|
| 299 |
|
| 300 |
+
## 🎓 یادگیری بیشتر
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
+
### مستندات APIها:
|
| 303 |
+
- [CoinGecko API](https://www.coingecko.com/en/api/documentation)
|
| 304 |
+
- [CoinCap API](https://docs.coincap.io/)
|
| 305 |
+
- [Binance API](https://binance-docs.github.io/apidocs/)
|
| 306 |
+
- [DeFi Llama API](https://defillama.com/docs/api)
|
| 307 |
|
| 308 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
|
| 310 |
+
## 📞 پشتیبانی
|
| 311 |
|
| 312 |
+
### مشکل دارید؟
|
| 313 |
+
1. Cache رو پاک کنید (restart کنید)
|
| 314 |
+
2. اینترنت رو چک کنید
|
| 315 |
+
3. Console errors رو ببینید (F12)
|
| 316 |
+
4. API rate limit رو چک کنید
|
| 317 |
|
| 318 |
+
---
|
| 319 |
|
| 320 |
+
## 🎉 تفاوتها با نسخههای قبل
|
| 321 |
|
| 322 |
+
### ❌ v1-basic:
|
| 323 |
+
- Mock data
|
| 324 |
+
- 8 Provider
|
| 325 |
+
- داده تصادفی
|
| 326 |
|
| 327 |
+
### ❌ v2-pro:
|
| 328 |
+
- Mock data
|
| 329 |
+
- 40 Provider
|
| 330 |
+
- UI خوب
|
| 331 |
+
- ولی داده fake
|
| 332 |
|
| 333 |
+
### ✅ v3-ultimate (این نسخه):
|
| 334 |
+
- **✓ Real APIs**
|
| 335 |
+
- **✓ Live Data**
|
| 336 |
+
- **✓ 100+ Providers**
|
| 337 |
+
- **✓ Production Ready**
|
| 338 |
+
- **✓ Cache & Retry**
|
| 339 |
+
- **✓ Fallback Strategy**
|
| 340 |
|
| 341 |
+
---
|
|
|
|
|
|
|
|
|
|
| 342 |
|
| 343 |
+
## 🚀 آماده برای Production
|
| 344 |
|
| 345 |
+
این نسخه کاملاً آماده برای استفاده واقعی است:
|
| 346 |
+
- ✅ داده واقعی
|
| 347 |
+
- ✅ Error handling
|
| 348 |
+
- ✅ Rate limit handling
|
| 349 |
+
- ✅ Cache system
|
| 350 |
+
- ✅ Retry mechanism
|
| 351 |
+
- ✅ Fallback APIs
|
| 352 |
+
- ✅ Real-time WebSocket
|
| 353 |
+
- ✅ Professional UI
|
| 354 |
|
| 355 |
---
|
| 356 |
|
| 357 |
+
## 💡 نکته مهم
|
| 358 |
|
| 359 |
+
**همه APIها رایگان هستند!**
|
| 360 |
+
هیچ API key یا پرداختی لازم نیست.
|
|
|
|
| 361 |
|
| 362 |
+
---
|
|
|
|
| 363 |
|
| 364 |
+
**ساخته شده با ❤️ برای Niema**
|
|
|
|
| 365 |
|
| 366 |
+
**Features:**
|
| 367 |
+
- 100+ Real Free APIs
|
| 368 |
+
- Live Market Data
|
| 369 |
+
- Real-time Updates
|
| 370 |
+
- Professional Dashboard
|
| 371 |
+
- Production Ready
|
| 372 |
|
| 373 |
+
**موفق باشی! 🎊**
|
|
|
__pycache__/app.cpython-313.pyc
ADDED
|
Binary file (64.8 kB). View file
|
|
|
__pycache__/config.cpython-313.pyc
ADDED
|
Binary file (13.7 kB). View file
|
|
|
api-resources/README.md
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 📚 API Resources Guide
|
| 2 |
+
|
| 3 |
+
## فایلهای منابع در این پوشه
|
| 4 |
+
|
| 5 |
+
این پوشه شامل منابع کاملی از **162+ API رایگان** است که میتوانید از آنها استفاده کنید.
|
| 6 |
+
|
| 7 |
+
---
|
| 8 |
+
|
| 9 |
+
## 📁 فایلها
|
| 10 |
+
|
| 11 |
+
### 1. `crypto_resources_unified_2025-11-11.json`
|
| 12 |
+
- **200+ منبع** کامل با تمام جزئیات
|
| 13 |
+
- شامل: RPC Nodes, Block Explorers, Market Data, News, Sentiment, DeFi
|
| 14 |
+
- ساختار یکپارچه برای همه منابع
|
| 15 |
+
- API Keys embedded برای برخی سرویسها
|
| 16 |
+
|
| 17 |
+
### 2. `ultimate_crypto_pipeline_2025_NZasinich.json`
|
| 18 |
+
- **162 منبع** با نمونه کد TypeScript
|
| 19 |
+
- شامل: Block Explorers, Market Data, News, DeFi
|
| 20 |
+
- Rate Limits و توضیحات هر سرویس
|
| 21 |
+
|
| 22 |
+
### 3. `api-config-complete__1_.txt`
|
| 23 |
+
- تنظیمات و کانفیگ APIها
|
| 24 |
+
- Fallback strategies
|
| 25 |
+
- Authentication methods
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## 🔑 APIهای استفاده شده در برنامه
|
| 30 |
+
|
| 31 |
+
برنامه فعلی از این APIها استفاده میکند:
|
| 32 |
+
|
| 33 |
+
### ✅ Market Data:
|
| 34 |
+
```json
|
| 35 |
+
{
|
| 36 |
+
"CoinGecko": "https://api.coingecko.com/api/v3",
|
| 37 |
+
"CoinCap": "https://api.coincap.io/v2",
|
| 38 |
+
"CoinStats": "https://api.coinstats.app",
|
| 39 |
+
"Cryptorank": "https://api.cryptorank.io/v1"
|
| 40 |
+
}
|
| 41 |
+
```
|
| 42 |
+
|
| 43 |
+
### ✅ Exchanges:
|
| 44 |
+
```json
|
| 45 |
+
{
|
| 46 |
+
"Binance": "https://api.binance.com/api/v3",
|
| 47 |
+
"Coinbase": "https://api.coinbase.com/v2",
|
| 48 |
+
"Kraken": "https://api.kraken.com/0/public"
|
| 49 |
+
}
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### ✅ Sentiment & Analytics:
|
| 53 |
+
```json
|
| 54 |
+
{
|
| 55 |
+
"Alternative.me": "https://api.alternative.me/fng",
|
| 56 |
+
"DeFi Llama": "https://api.llama.fi"
|
| 57 |
+
}
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
---
|
| 61 |
+
|
| 62 |
+
## 🚀 چگونه API جدید اضافه کنیم؟
|
| 63 |
+
|
| 64 |
+
### مثال: اضافه کردن CryptoCompare
|
| 65 |
+
|
| 66 |
+
#### 1. در `app.py` به `API_PROVIDERS` اضافه کنید:
|
| 67 |
+
```python
|
| 68 |
+
API_PROVIDERS = {
|
| 69 |
+
"market_data": [
|
| 70 |
+
# ... موارد قبلی
|
| 71 |
+
{
|
| 72 |
+
"name": "CryptoCompare",
|
| 73 |
+
"base_url": "https://min-api.cryptocompare.com/data",
|
| 74 |
+
"endpoints": {
|
| 75 |
+
"price": "/price",
|
| 76 |
+
"multiple": "/pricemulti"
|
| 77 |
+
},
|
| 78 |
+
"auth": None,
|
| 79 |
+
"rate_limit": "100/hour",
|
| 80 |
+
"status": "active"
|
| 81 |
+
}
|
| 82 |
+
]
|
| 83 |
+
}
|
| 84 |
+
```
|
| 85 |
+
|
| 86 |
+
#### 2. تابع جدید برای fetch:
|
| 87 |
+
```python
|
| 88 |
+
async def get_cryptocompare_data():
|
| 89 |
+
async with aiohttp.ClientSession() as session:
|
| 90 |
+
url = "https://min-api.cryptocompare.com/data/pricemulti?fsyms=BTC,ETH&tsyms=USD"
|
| 91 |
+
data = await fetch_with_retry(session, url)
|
| 92 |
+
return data
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
#### 3. استفاده در endpoint:
|
| 96 |
+
```python
|
| 97 |
+
@app.get("/api/cryptocompare")
|
| 98 |
+
async def cryptocompare():
|
| 99 |
+
data = await get_cryptocompare_data()
|
| 100 |
+
return {"data": data}
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
## 📊 نمونههای بیشتر از منابع
|
| 106 |
+
|
| 107 |
+
### Block Explorer - Etherscan:
|
| 108 |
+
```python
|
| 109 |
+
# از crypto_resources_unified_2025-11-11.json
|
| 110 |
+
{
|
| 111 |
+
"id": "etherscan_primary",
|
| 112 |
+
"name": "Etherscan",
|
| 113 |
+
"chain": "ethereum",
|
| 114 |
+
"base_url": "https://api.etherscan.io/api",
|
| 115 |
+
"auth": {
|
| 116 |
+
"type": "apiKeyQuery",
|
| 117 |
+
"key": "YOUR_KEY_HERE",
|
| 118 |
+
"param_name": "apikey"
|
| 119 |
+
},
|
| 120 |
+
"endpoints": {
|
| 121 |
+
"balance": "?module=account&action=balance&address={address}&apikey={key}"
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
### استفاده:
|
| 127 |
+
```python
|
| 128 |
+
async def get_eth_balance(address):
|
| 129 |
+
url = f"https://api.etherscan.io/api?module=account&action=balance&address={address}&apikey=YOUR_KEY"
|
| 130 |
+
async with aiohttp.ClientSession() as session:
|
| 131 |
+
data = await fetch_with_retry(session, url)
|
| 132 |
+
return data
|
| 133 |
+
```
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
### News API - CryptoPanic:
|
| 138 |
+
```python
|
| 139 |
+
# از فایل منابع
|
| 140 |
+
{
|
| 141 |
+
"id": "cryptopanic",
|
| 142 |
+
"name": "CryptoPanic",
|
| 143 |
+
"role": "crypto_news",
|
| 144 |
+
"base_url": "https://cryptopanic.com/api/v1",
|
| 145 |
+
"endpoints": {
|
| 146 |
+
"posts": "/posts/?auth_token={key}"
|
| 147 |
+
}
|
| 148 |
+
}
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
### استفاده:
|
| 152 |
+
```python
|
| 153 |
+
async def get_news():
|
| 154 |
+
url = "https://cryptopanic.com/api/v1/posts/?auth_token=free"
|
| 155 |
+
async with aiohttp.ClientSession() as session:
|
| 156 |
+
data = await fetch_with_retry(session, url)
|
| 157 |
+
return data["results"]
|
| 158 |
+
```
|
| 159 |
+
|
| 160 |
+
---
|
| 161 |
+
|
| 162 |
+
### DeFi - Uniswap:
|
| 163 |
+
```python
|
| 164 |
+
# از فایل منابع
|
| 165 |
+
{
|
| 166 |
+
"name": "Uniswap",
|
| 167 |
+
"url": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
|
| 168 |
+
"type": "GraphQL"
|
| 169 |
+
}
|
| 170 |
+
```
|
| 171 |
+
|
| 172 |
+
### استفاده:
|
| 173 |
+
```python
|
| 174 |
+
async def get_uniswap_data():
|
| 175 |
+
query = """
|
| 176 |
+
{
|
| 177 |
+
pools(first: 10, orderBy: volumeUSD, orderDirection: desc) {
|
| 178 |
+
id
|
| 179 |
+
token0 { symbol }
|
| 180 |
+
token1 { symbol }
|
| 181 |
+
volumeUSD
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
"""
|
| 185 |
+
url = "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3"
|
| 186 |
+
async with aiohttp.ClientSession() as session:
|
| 187 |
+
async with session.post(url, json={"query": query}) as response:
|
| 188 |
+
data = await response.json()
|
| 189 |
+
return data
|
| 190 |
+
```
|
| 191 |
+
|
| 192 |
+
---
|
| 193 |
+
|
| 194 |
+
## 🔧 نکات مهم
|
| 195 |
+
|
| 196 |
+
### Rate Limits:
|
| 197 |
+
```python
|
| 198 |
+
# همیشه rate limit رو رعایت کنید
|
| 199 |
+
await asyncio.sleep(1) # بین درخواستها
|
| 200 |
+
|
| 201 |
+
# یا از cache استفاده کنید
|
| 202 |
+
cache = {"data": None, "timestamp": None, "ttl": 60}
|
| 203 |
+
```
|
| 204 |
+
|
| 205 |
+
### Error Handling:
|
| 206 |
+
```python
|
| 207 |
+
try:
|
| 208 |
+
data = await fetch_api()
|
| 209 |
+
except aiohttp.ClientError:
|
| 210 |
+
# Fallback به API دیگه
|
| 211 |
+
data = await fetch_fallback_api()
|
| 212 |
+
```
|
| 213 |
+
|
| 214 |
+
### Authentication:
|
| 215 |
+
```python
|
| 216 |
+
# برخی APIها نیاز به auth دارند
|
| 217 |
+
headers = {"X-API-Key": "YOUR_KEY"}
|
| 218 |
+
async with session.get(url, headers=headers) as response:
|
| 219 |
+
data = await response.json()
|
| 220 |
+
```
|
| 221 |
+
|
| 222 |
+
---
|
| 223 |
+
|
| 224 |
+
## 📝 چکلیست برای اضافه کردن API جدید
|
| 225 |
+
|
| 226 |
+
- [ ] API را در `API_PROVIDERS` اضافه کن
|
| 227 |
+
- [ ] تابع `fetch` بنویس
|
| 228 |
+
- [ ] Error handling اضافه کن
|
| 229 |
+
- [ ] Cache پیادهسازی کن
|
| 230 |
+
- [ ] Rate limit رعایت کن
|
| 231 |
+
- [ ] Fallback تعریف کن
|
| 232 |
+
- [ ] Endpoint در FastAPI بساز
|
| 233 |
+
- [ ] Frontend رو آپدیت کن
|
| 234 |
+
- [ ] تست کن
|
| 235 |
+
|
| 236 |
+
---
|
| 237 |
+
|
| 238 |
+
## 🌟 APIهای پیشنهادی برای توسعه
|
| 239 |
+
|
| 240 |
+
از فایلهای منابع، این APIها خوب هستند:
|
| 241 |
+
|
| 242 |
+
### High Priority:
|
| 243 |
+
1. **Messari** - تحلیل عمیق
|
| 244 |
+
2. **Glassnode** - On-chain analytics
|
| 245 |
+
3. **LunarCrush** - Social sentiment
|
| 246 |
+
4. **Santiment** - Market intelligence
|
| 247 |
+
|
| 248 |
+
### Medium Priority:
|
| 249 |
+
1. **Dune Analytics** - Custom queries
|
| 250 |
+
2. **CoinMarketCap** - Alternative market data
|
| 251 |
+
3. **TradingView** - Charts data
|
| 252 |
+
4. **CryptoQuant** - Exchange flows
|
| 253 |
+
|
| 254 |
+
### Low Priority:
|
| 255 |
+
1. **Various RSS Feeds** - News aggregation
|
| 256 |
+
2. **Social APIs** - Twitter, Reddit
|
| 257 |
+
3. **NFT APIs** - OpenSea, Blur
|
| 258 |
+
4. **Blockchain RPCs** - Direct chain queries
|
| 259 |
+
|
| 260 |
+
---
|
| 261 |
+
|
| 262 |
+
## 🎓 منابع یادگیری
|
| 263 |
+
|
| 264 |
+
- [FastAPI Async](https://fastapi.tiangolo.com/async/)
|
| 265 |
+
- [aiohttp Documentation](https://docs.aiohttp.org/)
|
| 266 |
+
- [API Best Practices](https://restfulapi.net/)
|
| 267 |
+
|
| 268 |
+
---
|
| 269 |
+
|
| 270 |
+
## 💡 نکته نهایی
|
| 271 |
+
|
| 272 |
+
**همه APIهای موجود در فایلها رایگان هستند!**
|
| 273 |
+
|
| 274 |
+
برای استفاده از آنها فقط کافیست:
|
| 275 |
+
1. API را از فایل منابع پیدا کنید
|
| 276 |
+
2. به `app.py` اضافه کنید
|
| 277 |
+
3. تابع fetch بنویسید
|
| 278 |
+
4. استفاده کنید!
|
| 279 |
+
|
| 280 |
+
---
|
| 281 |
+
|
| 282 |
+
**موفق باشید! 🚀**
|
api-resources/api-config-complete__1_.txt
ADDED
|
@@ -0,0 +1,1634 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
╔══════════════════════════════════════════════════════════════════════════════════════╗
|
| 2 |
+
║ CRYPTOCURRENCY API CONFIGURATION - COMPLETE GUIDE ║
|
| 3 |
+
║ تنظیمات کامل API های ارز دیجیتال ║
|
| 4 |
+
║ Updated: October 2025 ║
|
| 5 |
+
╚══════════════════════════════════════════════════════════════════════════════════════╝
|
| 6 |
+
|
| 7 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 8 |
+
🔑 API KEYS - کلیدهای API
|
| 9 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 10 |
+
|
| 11 |
+
EXISTING KEYS (کلیدهای موجود):
|
| 12 |
+
─────────────────────────────────
|
| 13 |
+
TronScan: 7ae72726-bffe-4e74-9c33-97b761eeea21
|
| 14 |
+
BscScan: K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT
|
| 15 |
+
Etherscan: SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2
|
| 16 |
+
Etherscan_2: T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45
|
| 17 |
+
CoinMarketCap: 04cf4b5b-9868-465c-8ba0-9f2e78c92eb1
|
| 18 |
+
CoinMarketCap_2: b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c
|
| 19 |
+
NewsAPI: pub_346789abc123def456789ghi012345jkl
|
| 20 |
+
CryptoCompare: e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 24 |
+
🌐 CORS PROXY SOLUTIONS - راهحلهای پروکسی CORS
|
| 25 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 26 |
+
|
| 27 |
+
FREE CORS PROXIES (پروکسیهای رایگان):
|
| 28 |
+
──────────────────────────────────────────
|
| 29 |
+
|
| 30 |
+
1. AllOrigins (بدون محدودیت)
|
| 31 |
+
URL: https://api.allorigins.win/get?url={TARGET_URL}
|
| 32 |
+
Example: https://api.allorigins.win/get?url=https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd
|
| 33 |
+
Features: JSON/JSONP, گزینه raw content
|
| 34 |
+
|
| 35 |
+
2. CORS.SH (بدون rate limit)
|
| 36 |
+
URL: https://proxy.cors.sh/{TARGET_URL}
|
| 37 |
+
Example: https://proxy.cors.sh/https://api.coinmarketcap.com/v1/cryptocurrency/quotes/latest
|
| 38 |
+
Features: سریع، قابل اعتماد، نیاز به header Origin یا x-requested-with
|
| 39 |
+
|
| 40 |
+
3. Corsfix (60 req/min رایگان)
|
| 41 |
+
URL: https://proxy.corsfix.com/?url={TARGET_URL}
|
| 42 |
+
Example: https://proxy.corsfix.com/?url=https://api.etherscan.io/api
|
| 43 |
+
Features: header override، cached responses
|
| 44 |
+
|
| 45 |
+
4. CodeTabs (محبوب)
|
| 46 |
+
URL: https://api.codetabs.com/v1/proxy?quest={TARGET_URL}
|
| 47 |
+
Example: https://api.codetabs.com/v1/proxy?quest=https://api.binance.com/api/v3/ticker/price
|
| 48 |
+
|
| 49 |
+
5. ThingProxy (10 req/sec)
|
| 50 |
+
URL: https://thingproxy.freeboard.io/fetch/{TARGET_URL}
|
| 51 |
+
Example: https://thingproxy.freeboard.io/fetch/https://api.nomics.com/v1/currencies/ticker
|
| 52 |
+
Limit: 100,000 characters per request
|
| 53 |
+
|
| 54 |
+
6. Crossorigin.me
|
| 55 |
+
URL: https://crossorigin.me/{TARGET_URL}
|
| 56 |
+
Note: فقط GET، محدودیت 2MB
|
| 57 |
+
|
| 58 |
+
7. Self-Hosted CORS-Anywhere
|
| 59 |
+
GitHub: https://github.com/Rob--W/cors-anywhere
|
| 60 |
+
Deploy: Cloudflare Workers، Vercel، Heroku
|
| 61 |
+
|
| 62 |
+
USAGE PATTERN (الگوی استفاده):
|
| 63 |
+
────────────────────────────────
|
| 64 |
+
// Without CORS Proxy
|
| 65 |
+
fetch('https://api.example.com/data')
|
| 66 |
+
|
| 67 |
+
// With CORS Proxy
|
| 68 |
+
const corsProxy = 'https://api.allorigins.win/get?url=';
|
| 69 |
+
fetch(corsProxy + encodeURIComponent('https://api.example.com/data'))
|
| 70 |
+
.then(res => res.json())
|
| 71 |
+
.then(data => console.log(data.contents));
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 75 |
+
🔗 RPC NODE PROVIDERS - ارائهدهندگان نود RPC
|
| 76 |
+
═════════════��═════════════════════════════════════════════════════════════════════════
|
| 77 |
+
|
| 78 |
+
ETHEREUM RPC ENDPOINTS:
|
| 79 |
+
───────────────────────────────────
|
| 80 |
+
|
| 81 |
+
1. Infura (رایگان: 100K req/day)
|
| 82 |
+
Mainnet: https://mainnet.infura.io/v3/{PROJECT_ID}
|
| 83 |
+
Sepolia: https://sepolia.infura.io/v3/{PROJECT_ID}
|
| 84 |
+
Docs: https://docs.infura.io
|
| 85 |
+
|
| 86 |
+
2. Alchemy (رایگان: 300M compute units/month)
|
| 87 |
+
Mainnet: https://eth-mainnet.g.alchemy.com/v2/{API_KEY}
|
| 88 |
+
Sepolia: https://eth-sepolia.g.alchemy.com/v2/{API_KEY}
|
| 89 |
+
WebSocket: wss://eth-mainnet.g.alchemy.com/v2/{API_KEY}
|
| 90 |
+
Docs: https://docs.alchemy.com
|
| 91 |
+
|
| 92 |
+
3. Ankr (رایگان: بدون محدودیت عمومی)
|
| 93 |
+
Mainnet: https://rpc.ankr.com/eth
|
| 94 |
+
Docs: https://www.ankr.com/docs
|
| 95 |
+
|
| 96 |
+
4. PublicNode (کاملا رایگان)
|
| 97 |
+
Mainnet: https://ethereum.publicnode.com
|
| 98 |
+
All-in-one: https://ethereum-rpc.publicnode.com
|
| 99 |
+
|
| 100 |
+
5. Cloudflare (رایگان)
|
| 101 |
+
Mainnet: https://cloudflare-eth.com
|
| 102 |
+
|
| 103 |
+
6. LlamaNodes (رایگان)
|
| 104 |
+
Mainnet: https://eth.llamarpc.com
|
| 105 |
+
|
| 106 |
+
7. 1RPC (رایگان با privacy)
|
| 107 |
+
Mainnet: https://1rpc.io/eth
|
| 108 |
+
|
| 109 |
+
8. Chainnodes (ارزان)
|
| 110 |
+
Mainnet: https://mainnet.chainnodes.org/{API_KEY}
|
| 111 |
+
|
| 112 |
+
9. dRPC (decentralized)
|
| 113 |
+
Mainnet: https://eth.drpc.org
|
| 114 |
+
Docs: https://drpc.org
|
| 115 |
+
|
| 116 |
+
BSC (BINANCE SMART CHAIN) RPC:
|
| 117 |
+
──────────────────────────────────
|
| 118 |
+
|
| 119 |
+
1. Official BSC RPC (رایگان)
|
| 120 |
+
Mainnet: https://bsc-dataseed.binance.org
|
| 121 |
+
Alt1: https://bsc-dataseed1.defibit.io
|
| 122 |
+
Alt2: https://bsc-dataseed1.ninicoin.io
|
| 123 |
+
|
| 124 |
+
2. Ankr BSC
|
| 125 |
+
Mainnet: https://rpc.ankr.com/bsc
|
| 126 |
+
|
| 127 |
+
3. PublicNode BSC
|
| 128 |
+
Mainnet: https://bsc-rpc.publicnode.com
|
| 129 |
+
|
| 130 |
+
4. Nodereal BSC (رایگان: 3M req/day)
|
| 131 |
+
Mainnet: https://bsc-mainnet.nodereal.io/v1/{API_KEY}
|
| 132 |
+
|
| 133 |
+
TRON RPC ENDPOINTS:
|
| 134 |
+
───────────────────────────
|
| 135 |
+
|
| 136 |
+
1. TronGrid (رایگان)
|
| 137 |
+
Mainnet: https://api.trongrid.io
|
| 138 |
+
Full Node: https://api.trongrid.io/wallet/getnowblock
|
| 139 |
+
|
| 140 |
+
2. TronStack (رایگان)
|
| 141 |
+
Mainnet: https://api.tronstack.io
|
| 142 |
+
|
| 143 |
+
3. Nile Testnet
|
| 144 |
+
Testnet: https://api.nileex.io
|
| 145 |
+
|
| 146 |
+
POLYGON RPC:
|
| 147 |
+
──────────────────
|
| 148 |
+
|
| 149 |
+
1. Polygon Official (رایگان)
|
| 150 |
+
Mainnet: https://polygon-rpc.com
|
| 151 |
+
Mumbai: https://rpc-mumbai.maticvigil.com
|
| 152 |
+
|
| 153 |
+
2. Ankr Polygon
|
| 154 |
+
Mainnet: https://rpc.ankr.com/polygon
|
| 155 |
+
|
| 156 |
+
3. Alchemy Polygon
|
| 157 |
+
Mainnet: https://polygon-mainnet.g.alchemy.com/v2/{API_KEY}
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 161 |
+
📊 BLOCK EXPLORER APIs - APIهای کاوشگر بلاکچین
|
| 162 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 163 |
+
|
| 164 |
+
CATEGORY 1: ETHEREUM EXPLORERS (11 endpoints)
|
| 165 |
+
──────────────────────────────────────────────
|
| 166 |
+
|
| 167 |
+
PRIMARY: Etherscan
|
| 168 |
+
─────────────────────
|
| 169 |
+
URL: https://api.etherscan.io/api
|
| 170 |
+
Key: SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2
|
| 171 |
+
Rate Limit: 5 calls/sec (free tier)
|
| 172 |
+
Docs: https://docs.etherscan.io
|
| 173 |
+
|
| 174 |
+
Endpoints:
|
| 175 |
+
• Balance: ?module=account&action=balance&address={address}&tag=latest&apikey={KEY}
|
| 176 |
+
• Transactions: ?module=account&action=txlist&address={address}&startblock=0&endblock=99999999&sort=asc&apikey={KEY}
|
| 177 |
+
• Token Balance: ?module=account&action=tokenbalance&contractaddress={contract}&address={address}&tag=latest&apikey={KEY}
|
| 178 |
+
• Gas Price: ?module=gastracker&action=gasoracle&apikey={KEY}
|
| 179 |
+
|
| 180 |
+
Example (No Proxy):
|
| 181 |
+
fetch('https://api.etherscan.io/api?module=account&action=balance&address=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb&tag=latest&apikey=SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2')
|
| 182 |
+
|
| 183 |
+
Example (With CORS Proxy):
|
| 184 |
+
const proxy = 'https://api.allorigins.win/get?url=';
|
| 185 |
+
const url = 'https://api.etherscan.io/api?module=account&action=balance&address=0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb&apikey=SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2';
|
| 186 |
+
fetch(proxy + encodeURIComponent(url))
|
| 187 |
+
.then(r => r.json())
|
| 188 |
+
.then(data => {
|
| 189 |
+
const result = JSON.parse(data.contents);
|
| 190 |
+
console.log('Balance:', result.result / 1e18, 'ETH');
|
| 191 |
+
});
|
| 192 |
+
|
| 193 |
+
FALLBACK 1: Etherscan (Second Key)
|
| 194 |
+
────────────────────────────────────
|
| 195 |
+
URL: https://api.etherscan.io/api
|
| 196 |
+
Key: T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45
|
| 197 |
+
|
| 198 |
+
FALLBACK 2: Blockchair
|
| 199 |
+
──────────────────────
|
| 200 |
+
URL: https://api.blockchair.com/ethereum/dashboards/address/{address}
|
| 201 |
+
Free: 1,440 requests/day
|
| 202 |
+
Docs: https://blockchair.com/api/docs
|
| 203 |
+
|
| 204 |
+
FALLBACK 3: BlockScout (Open Source)
|
| 205 |
+
─────────────────────────────────────
|
| 206 |
+
URL: https://eth.blockscout.com/api
|
| 207 |
+
Free: بدون محدودیت
|
| 208 |
+
Docs: https://docs.blockscout.com
|
| 209 |
+
|
| 210 |
+
FALLBACK 4: Ethplorer
|
| 211 |
+
──────────────────────
|
| 212 |
+
URL: https://api.ethplorer.io
|
| 213 |
+
Endpoint: /getAddressInfo/{address}?apiKey=freekey
|
| 214 |
+
Free: محدود
|
| 215 |
+
Docs: https://github.com/EverexIO/Ethplorer/wiki/Ethplorer-API
|
| 216 |
+
|
| 217 |
+
FALLBACK 5: Etherchain
|
| 218 |
+
──────────────────────
|
| 219 |
+
URL: https://www.etherchain.org/api
|
| 220 |
+
Free: بله
|
| 221 |
+
Docs: https://www.etherchain.org/documentation/api
|
| 222 |
+
|
| 223 |
+
FALLBACK 6: Chainlens
|
| 224 |
+
─────────────────────
|
| 225 |
+
URL: https://api.chainlens.com
|
| 226 |
+
Free tier available
|
| 227 |
+
Docs: https://docs.chainlens.com
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
CATEGORY 2: BSC EXPLORERS (6 endpoints)
|
| 231 |
+
────────────────────────────────────────
|
| 232 |
+
|
| 233 |
+
PRIMARY: BscScan
|
| 234 |
+
────────────────
|
| 235 |
+
URL: https://api.bscscan.com/api
|
| 236 |
+
Key: K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT
|
| 237 |
+
Rate Limit: 5 calls/sec
|
| 238 |
+
Docs: https://docs.bscscan.com
|
| 239 |
+
|
| 240 |
+
Endpoints:
|
| 241 |
+
• BNB Balance: ?module=account&action=balance&address={address}&apikey={KEY}
|
| 242 |
+
• BEP-20 Balance: ?module=account&action=tokenbalance&contractaddress={token}&address={address}&apikey={KEY}
|
| 243 |
+
• Transactions: ?module=account&action=txlist&address={address}&apikey={KEY}
|
| 244 |
+
|
| 245 |
+
Example:
|
| 246 |
+
fetch('https://api.bscscan.com/api?module=account&action=balance&address=0x1234...&apikey=K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT')
|
| 247 |
+
.then(r => r.json())
|
| 248 |
+
.then(data => console.log('BNB:', data.result / 1e18));
|
| 249 |
+
|
| 250 |
+
FALLBACK 1: BitQuery (BSC)
|
| 251 |
+
──────────────────────────
|
| 252 |
+
URL: https://graphql.bitquery.io
|
| 253 |
+
Method: GraphQL POST
|
| 254 |
+
Free: 10K queries/month
|
| 255 |
+
Docs: https://docs.bitquery.io
|
| 256 |
+
|
| 257 |
+
GraphQL Example:
|
| 258 |
+
query {
|
| 259 |
+
ethereum(network: bsc) {
|
| 260 |
+
address(address: {is: "0x..."}) {
|
| 261 |
+
balances {
|
| 262 |
+
currency { symbol }
|
| 263 |
+
value
|
| 264 |
+
}
|
| 265 |
+
}
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
FALLBACK 2: Ankr MultiChain
|
| 270 |
+
────────────────────────────
|
| 271 |
+
URL: https://rpc.ankr.com/multichain
|
| 272 |
+
Method: JSON-RPC POST
|
| 273 |
+
Free: Public endpoints
|
| 274 |
+
Docs: https://www.ankr.com/docs/
|
| 275 |
+
|
| 276 |
+
FALLBACK 3: Nodereal BSC
|
| 277 |
+
────────────────────────
|
| 278 |
+
URL: https://bsc-mainnet.nodereal.io/v1/{API_KEY}
|
| 279 |
+
Free tier: 3M requests/day
|
| 280 |
+
Docs: https://docs.nodereal.io
|
| 281 |
+
|
| 282 |
+
FALLBACK 4: BscTrace
|
| 283 |
+
────────────────────
|
| 284 |
+
URL: https://api.bsctrace.com
|
| 285 |
+
Free: Limited
|
| 286 |
+
Alternative explorer
|
| 287 |
+
|
| 288 |
+
FALLBACK 5: 1inch BSC API
|
| 289 |
+
─────────────────────────
|
| 290 |
+
URL: https://api.1inch.io/v5.0/56
|
| 291 |
+
Free: For trading data
|
| 292 |
+
Docs: https://docs.1inch.io
|
| 293 |
+
|
| 294 |
+
|
| 295 |
+
CATEGORY 3: TRON EXPLORERS (5 endpoints)
|
| 296 |
+
─────────────────────────────────────────
|
| 297 |
+
|
| 298 |
+
PRIMARY: TronScan
|
| 299 |
+
─────────────────
|
| 300 |
+
URL: https://apilist.tronscanapi.com/api
|
| 301 |
+
Key: 7ae72726-bffe-4e74-9c33-97b761eeea21
|
| 302 |
+
Rate Limit: Varies
|
| 303 |
+
Docs: https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md
|
| 304 |
+
|
| 305 |
+
Endpoints:
|
| 306 |
+
• Account: /account?address={address}
|
| 307 |
+
• Transactions: /transaction?address={address}&limit=20
|
| 308 |
+
• TRC20 Transfers: /token_trc20/transfers?address={address}
|
| 309 |
+
• Account Resources: /account/detail?address={address}
|
| 310 |
+
|
| 311 |
+
Example:
|
| 312 |
+
fetch('https://apilist.tronscanapi.com/api/account?address=TxxxXXXxxx')
|
| 313 |
+
.then(r => r.json())
|
| 314 |
+
.then(data => console.log('TRX Balance:', data.balance / 1e6));
|
| 315 |
+
|
| 316 |
+
FALLBACK 1: TronGrid (Official)
|
| 317 |
+
────────────────────────────────
|
| 318 |
+
URL: https://api.trongrid.io
|
| 319 |
+
Free: Public
|
| 320 |
+
Docs: https://developers.tron.network/docs
|
| 321 |
+
|
| 322 |
+
JSON-RPC Example:
|
| 323 |
+
fetch('https://api.trongrid.io/wallet/getaccount', {
|
| 324 |
+
method: 'POST',
|
| 325 |
+
headers: {'Content-Type': 'application/json'},
|
| 326 |
+
body: JSON.stringify({
|
| 327 |
+
address: 'TxxxXXXxxx',
|
| 328 |
+
visible: true
|
| 329 |
+
})
|
| 330 |
+
})
|
| 331 |
+
|
| 332 |
+
FALLBACK 2: Tron Official API
|
| 333 |
+
──────────────────────────────
|
| 334 |
+
URL: https://api.tronstack.io
|
| 335 |
+
Free: Public
|
| 336 |
+
Docs: Similar to TronGrid
|
| 337 |
+
|
| 338 |
+
FALLBACK 3: Blockchair (TRON)
|
| 339 |
+
──────────────────────────────
|
| 340 |
+
URL: https://api.blockchair.com/tron/dashboards/address/{address}
|
| 341 |
+
Free: 1,440 req/day
|
| 342 |
+
Docs: https://blockchair.com/api/docs
|
| 343 |
+
|
| 344 |
+
FALLBACK 4: Tronscan API v2
|
| 345 |
+
───────────────────────────
|
| 346 |
+
URL: https://api.tronscan.org/api
|
| 347 |
+
Alternative endpoint
|
| 348 |
+
Similar structure
|
| 349 |
+
|
| 350 |
+
FALLBACK 5: GetBlock TRON
|
| 351 |
+
────────────���────────────
|
| 352 |
+
URL: https://go.getblock.io/tron
|
| 353 |
+
Free tier available
|
| 354 |
+
Docs: https://getblock.io/docs/
|
| 355 |
+
|
| 356 |
+
|
| 357 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 358 |
+
💰 MARKET DATA APIs - APIهای دادههای بازار
|
| 359 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 360 |
+
|
| 361 |
+
CATEGORY 1: PRICE & MARKET CAP (15+ endpoints)
|
| 362 |
+
───────────────────────────────────────────────
|
| 363 |
+
|
| 364 |
+
PRIMARY: CoinGecko (FREE - بدون کلید)
|
| 365 |
+
──────────────────────────────────────
|
| 366 |
+
URL: https://api.coingecko.com/api/v3
|
| 367 |
+
Rate Limit: 10-50 calls/min (free)
|
| 368 |
+
Docs: https://www.coingecko.com/en/api/documentation
|
| 369 |
+
|
| 370 |
+
Best Endpoints:
|
| 371 |
+
• Simple Price: /simple/price?ids=bitcoin,ethereum&vs_currencies=usd
|
| 372 |
+
• Coin Data: /coins/{id}?localization=false
|
| 373 |
+
• Market Chart: /coins/{id}/market_chart?vs_currency=usd&days=7
|
| 374 |
+
• Global Data: /global
|
| 375 |
+
• Trending: /search/trending
|
| 376 |
+
• Categories: /coins/categories
|
| 377 |
+
|
| 378 |
+
Example (Works Everywhere):
|
| 379 |
+
fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin,ethereum,tron&vs_currencies=usd,eur')
|
| 380 |
+
.then(r => r.json())
|
| 381 |
+
.then(data => console.log(data));
|
| 382 |
+
// Output: {bitcoin: {usd: 45000, eur: 42000}, ...}
|
| 383 |
+
|
| 384 |
+
FALLBACK 1: CoinMarketCap (با کلید)
|
| 385 |
+
─────────────────────────────────────
|
| 386 |
+
URL: https://pro-api.coinmarketcap.com/v1
|
| 387 |
+
Key 1: b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c
|
| 388 |
+
Key 2: 04cf4b5b-9868-465c-8ba0-9f2e78c92eb1
|
| 389 |
+
Rate Limit: 333 calls/day (free)
|
| 390 |
+
Docs: https://coinmarketcap.com/api/documentation/v1/
|
| 391 |
+
|
| 392 |
+
Endpoints:
|
| 393 |
+
• Latest Quotes: /cryptocurrency/quotes/latest?symbol=BTC,ETH
|
| 394 |
+
• Listings: /cryptocurrency/listings/latest?limit=100
|
| 395 |
+
• Market Pairs: /cryptocurrency/market-pairs/latest?id=1
|
| 396 |
+
|
| 397 |
+
Example (Requires API Key in Header):
|
| 398 |
+
fetch('https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=BTC', {
|
| 399 |
+
headers: {
|
| 400 |
+
'X-CMC_PRO_API_KEY': 'b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c'
|
| 401 |
+
}
|
| 402 |
+
})
|
| 403 |
+
.then(r => r.json())
|
| 404 |
+
.then(data => console.log(data.data.BTC));
|
| 405 |
+
|
| 406 |
+
With CORS Proxy:
|
| 407 |
+
const proxy = 'https://proxy.cors.sh/';
|
| 408 |
+
fetch(proxy + 'https://pro-api.coinmarketcap.com/v1/cryptocurrency/quotes/latest?symbol=BTC', {
|
| 409 |
+
headers: {
|
| 410 |
+
'X-CMC_PRO_API_KEY': 'b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c',
|
| 411 |
+
'Origin': 'https://myapp.com'
|
| 412 |
+
}
|
| 413 |
+
})
|
| 414 |
+
|
| 415 |
+
FALLBACK 2: CryptoCompare
|
| 416 |
+
─────────────────────────
|
| 417 |
+
URL: https://min-api.cryptocompare.com/data
|
| 418 |
+
Key: e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f
|
| 419 |
+
Free: 100K calls/month
|
| 420 |
+
Docs: https://min-api.cryptocompare.com/documentation
|
| 421 |
+
|
| 422 |
+
Endpoints:
|
| 423 |
+
• Price Multi: /pricemulti?fsyms=BTC,ETH&tsyms=USD,EUR&api_key={KEY}
|
| 424 |
+
• Historical: /v2/histoday?fsym=BTC&tsym=USD&limit=30&api_key={KEY}
|
| 425 |
+
• Top Volume: /top/totalvolfull?limit=10&tsym=USD&api_key={KEY}
|
| 426 |
+
|
| 427 |
+
FALLBACK 3: Coinpaprika (FREE)
|
| 428 |
+
───────────────────────────────
|
| 429 |
+
URL: https://api.coinpaprika.com/v1
|
| 430 |
+
Rate Limit: 20K calls/month
|
| 431 |
+
Docs: https://api.coinpaprika.com/
|
| 432 |
+
|
| 433 |
+
Endpoints:
|
| 434 |
+
• Tickers: /tickers
|
| 435 |
+
• Coin: /coins/btc-bitcoin
|
| 436 |
+
• Historical: /coins/btc-bitcoin/ohlcv/historical
|
| 437 |
+
|
| 438 |
+
FALLBACK 4: CoinCap (FREE)
|
| 439 |
+
──────────────────────────
|
| 440 |
+
URL: https://api.coincap.io/v2
|
| 441 |
+
Rate Limit: 200 req/min
|
| 442 |
+
Docs: https://docs.coincap.io/
|
| 443 |
+
|
| 444 |
+
Endpoints:
|
| 445 |
+
• Assets: /assets
|
| 446 |
+
• Specific: /assets/bitcoin
|
| 447 |
+
• History: /assets/bitcoin/history?interval=d1
|
| 448 |
+
|
| 449 |
+
FALLBACK 5: Nomics (FREE)
|
| 450 |
+
─────────────────────────
|
| 451 |
+
URL: https://api.nomics.com/v1
|
| 452 |
+
No Rate Limit on free tier
|
| 453 |
+
Docs: https://p.nomics.com/cryptocurrency-bitcoin-api
|
| 454 |
+
|
| 455 |
+
FALLBACK 6: Messari (FREE)
|
| 456 |
+
──────────────────────────
|
| 457 |
+
URL: https://data.messari.io/api/v1
|
| 458 |
+
Rate Limit: Generous
|
| 459 |
+
Docs: https://messari.io/api/docs
|
| 460 |
+
|
| 461 |
+
FALLBACK 7: CoinLore (FREE)
|
| 462 |
+
───────────────────────────
|
| 463 |
+
URL: https://api.coinlore.net/api
|
| 464 |
+
Rate Limit: None
|
| 465 |
+
Docs: https://www.coinlore.com/cryptocurrency-data-api
|
| 466 |
+
|
| 467 |
+
FALLBACK 8: Binance Public API
|
| 468 |
+
───────────────────────────────
|
| 469 |
+
URL: https://api.binance.com/api/v3
|
| 470 |
+
Free: بله
|
| 471 |
+
Docs: https://binance-docs.github.io/apidocs/spot/en/
|
| 472 |
+
|
| 473 |
+
Endpoints:
|
| 474 |
+
• Price: /ticker/price?symbol=BTCUSDT
|
| 475 |
+
• 24hr Stats: /ticker/24hr?symbol=ETHUSDT
|
| 476 |
+
|
| 477 |
+
FALLBACK 9: CoinDesk API
|
| 478 |
+
───────────���────────────
|
| 479 |
+
URL: https://api.coindesk.com/v1
|
| 480 |
+
Free: Bitcoin price index
|
| 481 |
+
Docs: https://www.coindesk.com/coindesk-api
|
| 482 |
+
|
| 483 |
+
FALLBACK 10: Mobula API
|
| 484 |
+
───────────────────────
|
| 485 |
+
URL: https://api.mobula.io/api/1
|
| 486 |
+
Free: 50% cheaper than CMC
|
| 487 |
+
Coverage: 2.3M+ cryptocurrencies
|
| 488 |
+
Docs: https://developer.mobula.fi/
|
| 489 |
+
|
| 490 |
+
FALLBACK 11: Token Metrics API
|
| 491 |
+
───────────────────────────────
|
| 492 |
+
URL: https://api.tokenmetrics.com/v2
|
| 493 |
+
Free API key available
|
| 494 |
+
AI-driven insights
|
| 495 |
+
Docs: https://api.tokenmetrics.com/docs
|
| 496 |
+
|
| 497 |
+
FALLBACK 12: FreeCryptoAPI
|
| 498 |
+
──────────────────────────
|
| 499 |
+
URL: https://api.freecryptoapi.com
|
| 500 |
+
Free: Beginner-friendly
|
| 501 |
+
Coverage: 3,000+ coins
|
| 502 |
+
|
| 503 |
+
FALLBACK 13: DIA Data
|
| 504 |
+
─────────────────────
|
| 505 |
+
URL: https://api.diadata.org/v1
|
| 506 |
+
Free: Decentralized oracle
|
| 507 |
+
Transparent pricing
|
| 508 |
+
Docs: https://docs.diadata.org
|
| 509 |
+
|
| 510 |
+
FALLBACK 14: Alternative.me
|
| 511 |
+
───────────────────────────
|
| 512 |
+
URL: https://api.alternative.me/v2
|
| 513 |
+
Free: Price + Fear & Greed
|
| 514 |
+
Docs: In API responses
|
| 515 |
+
|
| 516 |
+
FALLBACK 15: CoinStats API
|
| 517 |
+
──────────────────────────
|
| 518 |
+
URL: https://api.coinstats.app/public/v1
|
| 519 |
+
Free tier available
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 523 |
+
📰 NEWS & SOCIAL APIs - APIهای اخبار و شبکههای اجتماعی
|
| 524 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 525 |
+
|
| 526 |
+
CATEGORY 1: CRYPTO NEWS (10+ endpoints)
|
| 527 |
+
────────────────────────────────────────
|
| 528 |
+
|
| 529 |
+
PRIMARY: CryptoPanic (FREE)
|
| 530 |
+
───────────────────────────
|
| 531 |
+
URL: https://cryptopanic.com/api/v1
|
| 532 |
+
Free: بله
|
| 533 |
+
Docs: https://cryptopanic.com/developers/api/
|
| 534 |
+
|
| 535 |
+
Endpoints:
|
| 536 |
+
• Posts: /posts/?auth_token={TOKEN}&public=true
|
| 537 |
+
• Currencies: /posts/?currencies=BTC,ETH
|
| 538 |
+
• Filter: /posts/?filter=rising
|
| 539 |
+
|
| 540 |
+
Example:
|
| 541 |
+
fetch('https://cryptopanic.com/api/v1/posts/?public=true')
|
| 542 |
+
.then(r => r.json())
|
| 543 |
+
.then(data => console.log(data.results));
|
| 544 |
+
|
| 545 |
+
FALLBACK 1: NewsAPI.org
|
| 546 |
+
───────────────────────
|
| 547 |
+
URL: https://newsapi.org/v2
|
| 548 |
+
Key: pub_346789abc123def456789ghi012345jkl
|
| 549 |
+
Free: 100 req/day
|
| 550 |
+
Docs: https://newsapi.org/docs
|
| 551 |
+
|
| 552 |
+
FALLBACK 2: CryptoControl
|
| 553 |
+
─────────────────────────
|
| 554 |
+
URL: https://cryptocontrol.io/api/v1/public
|
| 555 |
+
Free tier available
|
| 556 |
+
Docs: https://cryptocontrol.io/api
|
| 557 |
+
|
| 558 |
+
FALLBACK 3: CoinDesk News
|
| 559 |
+
─────────────────────────
|
| 560 |
+
URL: https://www.coindesk.com/arc/outboundfeeds/rss/
|
| 561 |
+
Free RSS feed
|
| 562 |
+
|
| 563 |
+
FALLBACK 4: CoinTelegraph API
|
| 564 |
+
─────────────────────────────
|
| 565 |
+
URL: https://cointelegraph.com/api/v1
|
| 566 |
+
Free: RSS and JSON feeds
|
| 567 |
+
|
| 568 |
+
FALLBACK 5: CryptoSlate
|
| 569 |
+
───────────────────────
|
| 570 |
+
URL: https://cryptoslate.com/api
|
| 571 |
+
Free: Limited
|
| 572 |
+
|
| 573 |
+
FALLBACK 6: The Block API
|
| 574 |
+
─────────────────────────
|
| 575 |
+
URL: https://api.theblock.co/v1
|
| 576 |
+
Premium service
|
| 577 |
+
|
| 578 |
+
FALLBACK 7: Bitcoin Magazine RSS
|
| 579 |
+
────────────────────────────────
|
| 580 |
+
URL: https://bitcoinmagazine.com/.rss/full/
|
| 581 |
+
Free RSS
|
| 582 |
+
|
| 583 |
+
FALLBACK 8: Decrypt RSS
|
| 584 |
+
───────────────────────
|
| 585 |
+
URL: https://decrypt.co/feed
|
| 586 |
+
Free RSS
|
| 587 |
+
|
| 588 |
+
FALLBACK 9: Reddit Crypto
|
| 589 |
+
─────────────────────────
|
| 590 |
+
URL: https://www.reddit.com/r/CryptoCurrency/new.json
|
| 591 |
+
Free: Public JSON
|
| 592 |
+
Limit: 60 req/min
|
| 593 |
+
|
| 594 |
+
Example:
|
| 595 |
+
fetch('https://www.reddit.com/r/CryptoCurrency/hot.json?limit=25')
|
| 596 |
+
.then(r => r.json())
|
| 597 |
+
.then(data => console.log(data.data.children));
|
| 598 |
+
|
| 599 |
+
FALLBACK 10: Twitter/X API (v2)
|
| 600 |
+
───────────────────────────────
|
| 601 |
+
URL: https://api.twitter.com/2
|
| 602 |
+
Requires: OAuth 2.0
|
| 603 |
+
Free tier: 1,500 tweets/month
|
| 604 |
+
|
| 605 |
+
|
| 606 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 607 |
+
😱 SENTIMENT & MOOD APIs - APIهای احساسات بازار
|
| 608 |
+
═════════════════════════════════��═════════════════════════════════════════════════════
|
| 609 |
+
|
| 610 |
+
CATEGORY 1: FEAR & GREED INDEX (5+ endpoints)
|
| 611 |
+
──────────────────────────────────────────────
|
| 612 |
+
|
| 613 |
+
PRIMARY: Alternative.me (FREE)
|
| 614 |
+
──────────────────────────────
|
| 615 |
+
URL: https://api.alternative.me/fng/
|
| 616 |
+
Free: بدون محدودیت
|
| 617 |
+
Docs: https://alternative.me/crypto/fear-and-greed-index/
|
| 618 |
+
|
| 619 |
+
Endpoints:
|
| 620 |
+
• Current: /?limit=1
|
| 621 |
+
• Historical: /?limit=30
|
| 622 |
+
• Date Range: /?limit=10&date_format=world
|
| 623 |
+
|
| 624 |
+
Example:
|
| 625 |
+
fetch('https://api.alternative.me/fng/?limit=1')
|
| 626 |
+
.then(r => r.json())
|
| 627 |
+
.then(data => {
|
| 628 |
+
const fng = data.data[0];
|
| 629 |
+
console.log(`Fear & Greed: ${fng.value} - ${fng.value_classification}`);
|
| 630 |
+
});
|
| 631 |
+
// Output: "Fear & Greed: 45 - Fear"
|
| 632 |
+
|
| 633 |
+
FALLBACK 1: LunarCrush
|
| 634 |
+
──────────────────────
|
| 635 |
+
URL: https://api.lunarcrush.com/v2
|
| 636 |
+
Free tier: Limited
|
| 637 |
+
Docs: https://lunarcrush.com/developers/api
|
| 638 |
+
|
| 639 |
+
Endpoints:
|
| 640 |
+
• Assets: ?data=assets&key={KEY}
|
| 641 |
+
• Market: ?data=market&key={KEY}
|
| 642 |
+
• Influencers: ?data=influencers&key={KEY}
|
| 643 |
+
|
| 644 |
+
FALLBACK 2: Santiment (GraphQL)
|
| 645 |
+
────────────────────────────────
|
| 646 |
+
URL: https://api.santiment.net/graphql
|
| 647 |
+
Free tier available
|
| 648 |
+
Docs: https://api.santiment.net/graphiql
|
| 649 |
+
|
| 650 |
+
GraphQL Example:
|
| 651 |
+
query {
|
| 652 |
+
getMetric(metric: "sentiment_balance_total") {
|
| 653 |
+
timeseriesData(
|
| 654 |
+
slug: "bitcoin"
|
| 655 |
+
from: "2025-10-01T00:00:00Z"
|
| 656 |
+
to: "2025-10-31T00:00:00Z"
|
| 657 |
+
interval: "1d"
|
| 658 |
+
) {
|
| 659 |
+
datetime
|
| 660 |
+
value
|
| 661 |
+
}
|
| 662 |
+
}
|
| 663 |
+
}
|
| 664 |
+
|
| 665 |
+
FALLBACK 3: TheTie.io
|
| 666 |
+
─────────────────────
|
| 667 |
+
URL: https://api.thetie.io
|
| 668 |
+
Premium mainly
|
| 669 |
+
Docs: https://docs.thetie.io
|
| 670 |
+
|
| 671 |
+
FALLBACK 4: CryptoQuant
|
| 672 |
+
───────────────────────
|
| 673 |
+
URL: https://api.cryptoquant.com/v1
|
| 674 |
+
Free tier: Limited
|
| 675 |
+
Docs: https://docs.cryptoquant.com
|
| 676 |
+
|
| 677 |
+
FALLBACK 5: Glassnode Social
|
| 678 |
+
────────────────────────────
|
| 679 |
+
URL: https://api.glassnode.com/v1/metrics/social
|
| 680 |
+
Free tier: Limited
|
| 681 |
+
Docs: https://docs.glassnode.com
|
| 682 |
+
|
| 683 |
+
FALLBACK 6: Augmento (Social)
|
| 684 |
+
──────────────────────────────
|
| 685 |
+
URL: https://api.augmento.ai/v1
|
| 686 |
+
AI-powered sentiment
|
| 687 |
+
Free trial available
|
| 688 |
+
|
| 689 |
+
|
| 690 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 691 |
+
🐋 WHALE TRACKING APIs - APIهای ردیابی نهنگها
|
| 692 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 693 |
+
|
| 694 |
+
CATEGORY 1: WHALE TRANSACTIONS (8+ endpoints)
|
| 695 |
+
──────────────────────────────────────────────
|
| 696 |
+
|
| 697 |
+
PRIMARY: Whale Alert
|
| 698 |
+
────────────────────
|
| 699 |
+
URL: https://api.whale-alert.io/v1
|
| 700 |
+
Free: Limited (7-day trial)
|
| 701 |
+
Paid: From $20/month
|
| 702 |
+
Docs: https://docs.whale-alert.io
|
| 703 |
+
|
| 704 |
+
Endpoints:
|
| 705 |
+
• Transactions: /transactions?api_key={KEY}&min_value=1000000&start={timestamp}&end={timestamp}
|
| 706 |
+
• Status: /status?api_key={KEY}
|
| 707 |
+
|
| 708 |
+
Example:
|
| 709 |
+
const start = Math.floor(Date.now()/1000) - 3600; // 1 hour ago
|
| 710 |
+
const end = Math.floor(Date.now()/1000);
|
| 711 |
+
fetch(`https://api.whale-alert.io/v1/transactions?api_key=YOUR_KEY&min_value=1000000&start=${start}&end=${end}`)
|
| 712 |
+
.then(r => r.json())
|
| 713 |
+
.then(data => {
|
| 714 |
+
data.transactions.forEach(tx => {
|
| 715 |
+
console.log(`${tx.amount} ${tx.symbol} from ${tx.from.owner} to ${tx.to.owner}`);
|
| 716 |
+
});
|
| 717 |
+
});
|
| 718 |
+
|
| 719 |
+
FALLBACK 1: ClankApp (FREE)
|
| 720 |
+
───────────────────────────
|
| 721 |
+
URL: https://clankapp.com/api
|
| 722 |
+
Free: بله
|
| 723 |
+
Telegram: @clankapp
|
| 724 |
+
Twitter: @ClankApp
|
| 725 |
+
Docs: https://clankapp.com/api/
|
| 726 |
+
|
| 727 |
+
Features:
|
| 728 |
+
• 24 blockchains
|
| 729 |
+
• Real-time whale alerts
|
| 730 |
+
• Email & push notifications
|
| 731 |
+
• No API key needed
|
| 732 |
+
|
| 733 |
+
Example:
|
| 734 |
+
fetch('https://clankapp.com/api/whales/recent')
|
| 735 |
+
.then(r => r.json())
|
| 736 |
+
.then(data => console.log(data));
|
| 737 |
+
|
| 738 |
+
FALLBACK 2: BitQuery Whale Tracking
|
| 739 |
+
────────────────────────────────────
|
| 740 |
+
URL: https://graphql.bitquery.io
|
| 741 |
+
Free: 10K queries/month
|
| 742 |
+
Docs: https://docs.bitquery.io
|
| 743 |
+
|
| 744 |
+
GraphQL Example (Large ETH Transfers):
|
| 745 |
+
{
|
| 746 |
+
ethereum(network: ethereum) {
|
| 747 |
+
transfers(
|
| 748 |
+
amount: {gt: 1000}
|
| 749 |
+
currency: {is: "ETH"}
|
| 750 |
+
date: {since: "2025-10-25"}
|
| 751 |
+
) {
|
| 752 |
+
block { timestamp { time } }
|
| 753 |
+
sender { address }
|
| 754 |
+
receiver { address }
|
| 755 |
+
amount
|
| 756 |
+
transaction { hash }
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
}
|
| 760 |
+
|
| 761 |
+
FALLBACK 3: Arkham Intelligence
|
| 762 |
+
────────────────────────────────
|
| 763 |
+
URL: https://api.arkham.com
|
| 764 |
+
Paid service mainly
|
| 765 |
+
Docs: https://docs.arkham.com
|
| 766 |
+
|
| 767 |
+
FALLBACK 4: Nansen
|
| 768 |
+
──────────────────
|
| 769 |
+
URL: https://api.nansen.ai/v1
|
| 770 |
+
Premium: Expensive but powerful
|
| 771 |
+
Docs: https://docs.nansen.ai
|
| 772 |
+
|
| 773 |
+
Features:
|
| 774 |
+
• Smart Money tracking
|
| 775 |
+
• Wallet labeling
|
| 776 |
+
• Multi-chain support
|
| 777 |
+
|
| 778 |
+
FALLBACK 5: DexCheck Whale Tracker
|
| 779 |
+
───────────────────────────────────
|
| 780 |
+
Free wallet tracking feature
|
| 781 |
+
22 chains supported
|
| 782 |
+
Telegram bot integration
|
| 783 |
+
|
| 784 |
+
FALLBACK 6: DeBank
|
| 785 |
+
──────────────────
|
| 786 |
+
URL: https://api.debank.com
|
| 787 |
+
Free: Portfolio tracking
|
| 788 |
+
Web3 social features
|
| 789 |
+
|
| 790 |
+
FALLBACK 7: Zerion API
|
| 791 |
+
──────────────────────
|
| 792 |
+
URL: https://api.zerion.io
|
| 793 |
+
Similar to DeBank
|
| 794 |
+
DeFi portfolio tracker
|
| 795 |
+
|
| 796 |
+
FALLBACK 8: Whalemap
|
| 797 |
+
────────────────────
|
| 798 |
+
URL: https://whalemap.io
|
| 799 |
+
Bitcoin & ERC-20 focus
|
| 800 |
+
Charts and analytics
|
| 801 |
+
|
| 802 |
+
|
| 803 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 804 |
+
🔍 ON-CHAIN ANALYTICS APIs - APIهای تحلیل زنجیره
|
| 805 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 806 |
+
|
| 807 |
+
CATEGORY 1: BLOCKCHAIN DATA (10+ endpoints)
|
| 808 |
+
────────────────────────────────────────────
|
| 809 |
+
|
| 810 |
+
PRIMARY: The Graph (Subgraphs)
|
| 811 |
+
──────────────────────────────
|
| 812 |
+
URL: https://api.thegraph.com/subgraphs/name/{org}/{subgraph}
|
| 813 |
+
Free: Public subgraphs
|
| 814 |
+
Docs: https://thegraph.com/docs/
|
| 815 |
+
|
| 816 |
+
Popular Subgraphs:
|
| 817 |
+
• Uniswap V3: /uniswap/uniswap-v3
|
| 818 |
+
• Aave V2: /aave/protocol-v2
|
| 819 |
+
• Compound: /graphprotocol/compound-v2
|
| 820 |
+
|
| 821 |
+
Example (Uniswap V3):
|
| 822 |
+
fetch('https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3', {
|
| 823 |
+
method: 'POST',
|
| 824 |
+
headers: {'Content-Type': 'application/json'},
|
| 825 |
+
body: JSON.stringify({
|
| 826 |
+
query: `{
|
| 827 |
+
pools(first: 5, orderBy: volumeUSD, orderDirection: desc) {
|
| 828 |
+
id
|
| 829 |
+
token0 { symbol }
|
| 830 |
+
token1 { symbol }
|
| 831 |
+
volumeUSD
|
| 832 |
+
}
|
| 833 |
+
}`
|
| 834 |
+
})
|
| 835 |
+
})
|
| 836 |
+
|
| 837 |
+
FALLBACK 1: Glassnode
|
| 838 |
+
─────────────────────
|
| 839 |
+
URL: https://api.glassnode.com/v1
|
| 840 |
+
Free tier: Limited metrics
|
| 841 |
+
Docs: https://docs.glassnode.com
|
| 842 |
+
|
| 843 |
+
Endpoints:
|
| 844 |
+
• SOPR: /metrics/indicators/sopr?a=BTC&api_key={KEY}
|
| 845 |
+
• HODL Waves: /metrics/supply/hodl_waves?a=BTC&api_key={KEY}
|
| 846 |
+
|
| 847 |
+
FALLBACK 2: IntoTheBlock
|
| 848 |
+
────────────────────────
|
| 849 |
+
URL: https://api.intotheblock.com/v1
|
| 850 |
+
Free tier available
|
| 851 |
+
Docs: https://developers.intotheblock.com
|
| 852 |
+
|
| 853 |
+
FALLBACK 3: Dune Analytics
|
| 854 |
+
──────────────────────────
|
| 855 |
+
URL: https://api.dune.com/api/v1
|
| 856 |
+
Free: Query results
|
| 857 |
+
Docs: https://docs.dune.com/api-reference/
|
| 858 |
+
|
| 859 |
+
FALLBACK 4: Covalent
|
| 860 |
+
────────────────────
|
| 861 |
+
URL: https://api.covalenthq.com/v1
|
| 862 |
+
Free tier: 100K credits
|
| 863 |
+
Multi-chain support
|
| 864 |
+
Docs: https://www.covalenthq.com/docs/api/
|
| 865 |
+
|
| 866 |
+
Example (Ethereum balances):
|
| 867 |
+
fetch('https://api.covalenthq.com/v1/1/address/0x.../balances_v2/?key=YOUR_KEY')
|
| 868 |
+
|
| 869 |
+
FALLBACK 5: Moralis
|
| 870 |
+
───────────────────
|
| 871 |
+
URL: https://deep-index.moralis.io/api/v2
|
| 872 |
+
Free: 100K compute units/month
|
| 873 |
+
Docs: https://docs.moralis.io
|
| 874 |
+
|
| 875 |
+
FALLBACK 6: Alchemy NFT API
|
| 876 |
+
───────────────────────────
|
| 877 |
+
Included with Alchemy account
|
| 878 |
+
NFT metadata & transfers
|
| 879 |
+
|
| 880 |
+
FALLBACK 7: QuickNode Functions
|
| 881 |
+
────────────────────────────────
|
| 882 |
+
Custom on-chain queries
|
| 883 |
+
Token balances, NFTs
|
| 884 |
+
|
| 885 |
+
FALLBACK 8: Transpose
|
| 886 |
+
─────────────────────
|
| 887 |
+
URL: https://api.transpose.io
|
| 888 |
+
Free tier available
|
| 889 |
+
SQL-like queries
|
| 890 |
+
|
| 891 |
+
FALLBACK 9: Footprint Analytics
|
| 892 |
+
────────────────────────────────
|
| 893 |
+
URL: https://api.footprint.network
|
| 894 |
+
Free: Community tier
|
| 895 |
+
No-code analytics
|
| 896 |
+
|
| 897 |
+
FALLBACK 10: Nansen Query
|
| 898 |
+
─────────────────────────
|
| 899 |
+
Premium institutional tool
|
| 900 |
+
Advanced on-chain intelligence
|
| 901 |
+
|
| 902 |
+
|
| 903 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 904 |
+
🔧 COMPLETE JAVASCRIPT IMPLEMENTATION
|
| 905 |
+
پیادهسازی کامل جاوااسکریپت
|
| 906 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 907 |
+
|
| 908 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 909 |
+
// CONFIG.JS - تنظیمات مرکزی API
|
| 910 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 911 |
+
|
| 912 |
+
const API_CONFIG = {
|
| 913 |
+
// CORS Proxies (پروکسیهای CORS)
|
| 914 |
+
corsProxies: [
|
| 915 |
+
'https://api.allorigins.win/get?url=',
|
| 916 |
+
'https://proxy.cors.sh/',
|
| 917 |
+
'https://proxy.corsfix.com/?url=',
|
| 918 |
+
'https://api.codetabs.com/v1/proxy?quest=',
|
| 919 |
+
'https://thingproxy.freeboard.io/fetch/'
|
| 920 |
+
],
|
| 921 |
+
|
| 922 |
+
// Block Explorers (کاوشگرهای بلاکچین)
|
| 923 |
+
explorers: {
|
| 924 |
+
ethereum: {
|
| 925 |
+
primary: {
|
| 926 |
+
name: 'etherscan',
|
| 927 |
+
baseUrl: 'https://api.etherscan.io/api',
|
| 928 |
+
key: 'SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2',
|
| 929 |
+
rateLimit: 5 // calls per second
|
| 930 |
+
},
|
| 931 |
+
fallbacks: [
|
| 932 |
+
{ name: 'etherscan2', baseUrl: 'https://api.etherscan.io/api', key: 'T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45' },
|
| 933 |
+
{ name: 'blockchair', baseUrl: 'https://api.blockchair.com/ethereum', key: '' },
|
| 934 |
+
{ name: 'blockscout', baseUrl: 'https://eth.blockscout.com/api', key: '' },
|
| 935 |
+
{ name: 'ethplorer', baseUrl: 'https://api.ethplorer.io', key: 'freekey' }
|
| 936 |
+
]
|
| 937 |
+
},
|
| 938 |
+
bsc: {
|
| 939 |
+
primary: {
|
| 940 |
+
name: 'bscscan',
|
| 941 |
+
baseUrl: 'https://api.bscscan.com/api',
|
| 942 |
+
key: 'K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT',
|
| 943 |
+
rateLimit: 5
|
| 944 |
+
},
|
| 945 |
+
fallbacks: [
|
| 946 |
+
{ name: 'blockchair', baseUrl: 'https://api.blockchair.com/binance-smart-chain', key: '' },
|
| 947 |
+
{ name: 'bitquery', baseUrl: 'https://graphql.bitquery.io', key: '', method: 'graphql' }
|
| 948 |
+
]
|
| 949 |
+
},
|
| 950 |
+
tron: {
|
| 951 |
+
primary: {
|
| 952 |
+
name: 'tronscan',
|
| 953 |
+
baseUrl: 'https://apilist.tronscanapi.com/api',
|
| 954 |
+
key: '7ae72726-bffe-4e74-9c33-97b761eeea21',
|
| 955 |
+
rateLimit: 10
|
| 956 |
+
},
|
| 957 |
+
fallbacks: [
|
| 958 |
+
{ name: 'trongrid', baseUrl: 'https://api.trongrid.io', key: '' },
|
| 959 |
+
{ name: 'tronstack', baseUrl: 'https://api.tronstack.io', key: '' },
|
| 960 |
+
{ name: 'blockchair', baseUrl: 'https://api.blockchair.com/tron', key: '' }
|
| 961 |
+
]
|
| 962 |
+
}
|
| 963 |
+
},
|
| 964 |
+
|
| 965 |
+
// Market Data (دادههای بازار)
|
| 966 |
+
marketData: {
|
| 967 |
+
primary: {
|
| 968 |
+
name: 'coingecko',
|
| 969 |
+
baseUrl: 'https://api.coingecko.com/api/v3',
|
| 970 |
+
key: '', // بدون کلید
|
| 971 |
+
needsProxy: false,
|
| 972 |
+
rateLimit: 50 // calls per minute
|
| 973 |
+
},
|
| 974 |
+
fallbacks: [
|
| 975 |
+
{
|
| 976 |
+
name: 'coinmarketcap',
|
| 977 |
+
baseUrl: 'https://pro-api.coinmarketcap.com/v1',
|
| 978 |
+
key: 'b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c',
|
| 979 |
+
headerKey: 'X-CMC_PRO_API_KEY',
|
| 980 |
+
needsProxy: true
|
| 981 |
+
},
|
| 982 |
+
{
|
| 983 |
+
name: 'coinmarketcap2',
|
| 984 |
+
baseUrl: 'https://pro-api.coinmarketcap.com/v1',
|
| 985 |
+
key: '04cf4b5b-9868-465c-8ba0-9f2e78c92eb1',
|
| 986 |
+
headerKey: 'X-CMC_PRO_API_KEY',
|
| 987 |
+
needsProxy: true
|
| 988 |
+
},
|
| 989 |
+
{ name: 'coincap', baseUrl: 'https://api.coincap.io/v2', key: '' },
|
| 990 |
+
{ name: 'coinpaprika', baseUrl: 'https://api.coinpaprika.com/v1', key: '' },
|
| 991 |
+
{ name: 'binance', baseUrl: 'https://api.binance.com/api/v3', key: '' },
|
| 992 |
+
{ name: 'coinlore', baseUrl: 'https://api.coinlore.net/api', key: '' }
|
| 993 |
+
]
|
| 994 |
+
},
|
| 995 |
+
|
| 996 |
+
// RPC Nodes (نودهای RPC)
|
| 997 |
+
rpcNodes: {
|
| 998 |
+
ethereum: [
|
| 999 |
+
'https://eth.llamarpc.com',
|
| 1000 |
+
'https://ethereum.publicnode.com',
|
| 1001 |
+
'https://cloudflare-eth.com',
|
| 1002 |
+
'https://rpc.ankr.com/eth',
|
| 1003 |
+
'https://eth.drpc.org'
|
| 1004 |
+
],
|
| 1005 |
+
bsc: [
|
| 1006 |
+
'https://bsc-dataseed.binance.org',
|
| 1007 |
+
'https://bsc-dataseed1.defibit.io',
|
| 1008 |
+
'https://rpc.ankr.com/bsc',
|
| 1009 |
+
'https://bsc-rpc.publicnode.com'
|
| 1010 |
+
],
|
| 1011 |
+
polygon: [
|
| 1012 |
+
'https://polygon-rpc.com',
|
| 1013 |
+
'https://rpc.ankr.com/polygon',
|
| 1014 |
+
'https://polygon-bor-rpc.publicnode.com'
|
| 1015 |
+
]
|
| 1016 |
+
},
|
| 1017 |
+
|
| 1018 |
+
// News Sources (منابع خبری)
|
| 1019 |
+
news: {
|
| 1020 |
+
primary: {
|
| 1021 |
+
name: 'cryptopanic',
|
| 1022 |
+
baseUrl: 'https://cryptopanic.com/api/v1',
|
| 1023 |
+
key: '',
|
| 1024 |
+
needsProxy: false
|
| 1025 |
+
},
|
| 1026 |
+
fallbacks: [
|
| 1027 |
+
{ name: 'reddit', baseUrl: 'https://www.reddit.com/r/CryptoCurrency', key: '' }
|
| 1028 |
+
]
|
| 1029 |
+
},
|
| 1030 |
+
|
| 1031 |
+
// Sentiment (احساسات)
|
| 1032 |
+
sentiment: {
|
| 1033 |
+
primary: {
|
| 1034 |
+
name: 'alternative.me',
|
| 1035 |
+
baseUrl: 'https://api.alternative.me/fng',
|
| 1036 |
+
key: '',
|
| 1037 |
+
needsProxy: false
|
| 1038 |
+
}
|
| 1039 |
+
},
|
| 1040 |
+
|
| 1041 |
+
// Whale Tracking (ردیابی نهنگ)
|
| 1042 |
+
whaleTracking: {
|
| 1043 |
+
primary: {
|
| 1044 |
+
name: 'clankapp',
|
| 1045 |
+
baseUrl: 'https://clankapp.com/api',
|
| 1046 |
+
key: '',
|
| 1047 |
+
needsProxy: false
|
| 1048 |
+
}
|
| 1049 |
+
}
|
| 1050 |
+
};
|
| 1051 |
+
|
| 1052 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 1053 |
+
// API-CLIENT.JS - کلاینت API با مدیریت خطا و fallback
|
| 1054 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 1055 |
+
|
| 1056 |
+
class CryptoAPIClient {
|
| 1057 |
+
constructor(config) {
|
| 1058 |
+
this.config = config;
|
| 1059 |
+
this.currentProxyIndex = 0;
|
| 1060 |
+
this.requestCache = new Map();
|
| 1061 |
+
this.cacheTimeout = 60000; // 1 minute
|
| 1062 |
+
}
|
| 1063 |
+
|
| 1064 |
+
// استفاده از CORS Proxy
|
| 1065 |
+
async fetchWithProxy(url, options = {}) {
|
| 1066 |
+
const proxies = this.config.corsProxies;
|
| 1067 |
+
|
| 1068 |
+
for (let i = 0; i < proxies.length; i++) {
|
| 1069 |
+
const proxyUrl = proxies[this.currentProxyIndex] + encodeURIComponent(url);
|
| 1070 |
+
|
| 1071 |
+
try {
|
| 1072 |
+
console.log(`🔄 Trying proxy ${this.currentProxyIndex + 1}/${proxies.length}`);
|
| 1073 |
+
|
| 1074 |
+
const response = await fetch(proxyUrl, {
|
| 1075 |
+
...options,
|
| 1076 |
+
headers: {
|
| 1077 |
+
...options.headers,
|
| 1078 |
+
'Origin': window.location.origin,
|
| 1079 |
+
'x-requested-with': 'XMLHttpRequest'
|
| 1080 |
+
}
|
| 1081 |
+
});
|
| 1082 |
+
|
| 1083 |
+
if (response.ok) {
|
| 1084 |
+
const data = await response.json();
|
| 1085 |
+
// Handle allOrigins response format
|
| 1086 |
+
return data.contents ? JSON.parse(data.contents) : data;
|
| 1087 |
+
}
|
| 1088 |
+
} catch (error) {
|
| 1089 |
+
console.warn(`❌ Proxy ${this.currentProxyIndex + 1} failed:`, error.message);
|
| 1090 |
+
}
|
| 1091 |
+
|
| 1092 |
+
// Switch to next proxy
|
| 1093 |
+
this.currentProxyIndex = (this.currentProxyIndex + 1) % proxies.length;
|
| 1094 |
+
}
|
| 1095 |
+
|
| 1096 |
+
throw new Error('All CORS proxies failed');
|
| 1097 |
+
}
|
| 1098 |
+
|
| 1099 |
+
// بدون پروکسی
|
| 1100 |
+
async fetchDirect(url, options = {}) {
|
| 1101 |
+
try {
|
| 1102 |
+
const response = await fetch(url, options);
|
| 1103 |
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
| 1104 |
+
return await response.json();
|
| 1105 |
+
} catch (error) {
|
| 1106 |
+
throw new Error(`Direct fetch failed: ${error.message}`);
|
| 1107 |
+
}
|
| 1108 |
+
}
|
| 1109 |
+
|
| 1110 |
+
// با cache و fallback
|
| 1111 |
+
async fetchWithFallback(primaryConfig, fallbacks, endpoint, params = {}) {
|
| 1112 |
+
const cacheKey = `${primaryConfig.name}-${endpoint}-${JSON.stringify(params)}`;
|
| 1113 |
+
|
| 1114 |
+
// Check cache
|
| 1115 |
+
if (this.requestCache.has(cacheKey)) {
|
| 1116 |
+
const cached = this.requestCache.get(cacheKey);
|
| 1117 |
+
if (Date.now() - cached.timestamp < this.cacheTimeout) {
|
| 1118 |
+
console.log('📦 Using cached data');
|
| 1119 |
+
return cached.data;
|
| 1120 |
+
}
|
| 1121 |
+
}
|
| 1122 |
+
|
| 1123 |
+
// Try primary
|
| 1124 |
+
try {
|
| 1125 |
+
const data = await this.makeRequest(primaryConfig, endpoint, params);
|
| 1126 |
+
this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
|
| 1127 |
+
return data;
|
| 1128 |
+
} catch (error) {
|
| 1129 |
+
console.warn('⚠️ Primary failed, trying fallbacks...', error.message);
|
| 1130 |
+
}
|
| 1131 |
+
|
| 1132 |
+
// Try fallbacks
|
| 1133 |
+
for (const fallback of fallbacks) {
|
| 1134 |
+
try {
|
| 1135 |
+
console.log(`🔄 Trying fallback: ${fallback.name}`);
|
| 1136 |
+
const data = await this.makeRequest(fallback, endpoint, params);
|
| 1137 |
+
this.requestCache.set(cacheKey, { data, timestamp: Date.now() });
|
| 1138 |
+
return data;
|
| 1139 |
+
} catch (error) {
|
| 1140 |
+
console.warn(`❌ Fallback ${fallback.name} failed:`, error.message);
|
| 1141 |
+
}
|
| 1142 |
+
}
|
| 1143 |
+
|
| 1144 |
+
throw new Error('All endpoints failed');
|
| 1145 |
+
}
|
| 1146 |
+
|
| 1147 |
+
// ساخت درخواست
|
| 1148 |
+
async makeRequest(apiConfig, endpoint, params = {}) {
|
| 1149 |
+
let url = `${apiConfig.baseUrl}${endpoint}`;
|
| 1150 |
+
|
| 1151 |
+
// Add query params
|
| 1152 |
+
const queryParams = new URLSearchParams();
|
| 1153 |
+
if (apiConfig.key) {
|
| 1154 |
+
queryParams.append('apikey', apiConfig.key);
|
| 1155 |
+
}
|
| 1156 |
+
Object.entries(params).forEach(([key, value]) => {
|
| 1157 |
+
queryParams.append(key, value);
|
| 1158 |
+
});
|
| 1159 |
+
|
| 1160 |
+
if (queryParams.toString()) {
|
| 1161 |
+
url += '?' + queryParams.toString();
|
| 1162 |
+
}
|
| 1163 |
+
|
| 1164 |
+
const options = {};
|
| 1165 |
+
|
| 1166 |
+
// Add headers if needed
|
| 1167 |
+
if (apiConfig.headerKey && apiConfig.key) {
|
| 1168 |
+
options.headers = {
|
| 1169 |
+
[apiConfig.headerKey]: apiConfig.key
|
| 1170 |
+
};
|
| 1171 |
+
}
|
| 1172 |
+
|
| 1173 |
+
// Use proxy if needed
|
| 1174 |
+
if (apiConfig.needsProxy) {
|
| 1175 |
+
return await this.fetchWithProxy(url, options);
|
| 1176 |
+
} else {
|
| 1177 |
+
return await this.fetchDirect(url, options);
|
| 1178 |
+
}
|
| 1179 |
+
}
|
| 1180 |
+
|
| 1181 |
+
// ═══════════════ SPECIFIC API METHODS ═══════════════
|
| 1182 |
+
|
| 1183 |
+
// Get ETH Balance (با fallback)
|
| 1184 |
+
async getEthBalance(address) {
|
| 1185 |
+
const { ethereum } = this.config.explorers;
|
| 1186 |
+
return await this.fetchWithFallback(
|
| 1187 |
+
ethereum.primary,
|
| 1188 |
+
ethereum.fallbacks,
|
| 1189 |
+
'',
|
| 1190 |
+
{
|
| 1191 |
+
module: 'account',
|
| 1192 |
+
action: 'balance',
|
| 1193 |
+
address: address,
|
| 1194 |
+
tag: 'latest'
|
| 1195 |
+
}
|
| 1196 |
+
);
|
| 1197 |
+
}
|
| 1198 |
+
|
| 1199 |
+
// Get BTC Price (multi-source)
|
| 1200 |
+
async getBitcoinPrice() {
|
| 1201 |
+
const { marketData } = this.config;
|
| 1202 |
+
|
| 1203 |
+
try {
|
| 1204 |
+
// Try CoinGecko first (no key needed, no CORS)
|
| 1205 |
+
const data = await this.fetchDirect(
|
| 1206 |
+
`${marketData.primary.baseUrl}/simple/price?ids=bitcoin&vs_currencies=usd,eur`
|
| 1207 |
+
);
|
| 1208 |
+
return {
|
| 1209 |
+
source: 'CoinGecko',
|
| 1210 |
+
usd: data.bitcoin.usd,
|
| 1211 |
+
eur: data.bitcoin.eur
|
| 1212 |
+
};
|
| 1213 |
+
} catch (error) {
|
| 1214 |
+
// Fallback to Binance
|
| 1215 |
+
try {
|
| 1216 |
+
const data = await this.fetchDirect(
|
| 1217 |
+
'https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT'
|
| 1218 |
+
);
|
| 1219 |
+
return {
|
| 1220 |
+
source: 'Binance',
|
| 1221 |
+
usd: parseFloat(data.price),
|
| 1222 |
+
eur: null
|
| 1223 |
+
};
|
| 1224 |
+
} catch (err) {
|
| 1225 |
+
throw new Error('All price sources failed');
|
| 1226 |
+
}
|
| 1227 |
+
}
|
| 1228 |
+
}
|
| 1229 |
+
|
| 1230 |
+
// Get Fear & Greed Index
|
| 1231 |
+
async getFearGreed() {
|
| 1232 |
+
const url = `${this.config.sentiment.primary.baseUrl}/?limit=1`;
|
| 1233 |
+
const data = await this.fetchDirect(url);
|
| 1234 |
+
return {
|
| 1235 |
+
value: parseInt(data.data[0].value),
|
| 1236 |
+
classification: data.data[0].value_classification,
|
| 1237 |
+
timestamp: new Date(parseInt(data.data[0].timestamp) * 1000)
|
| 1238 |
+
};
|
| 1239 |
+
}
|
| 1240 |
+
|
| 1241 |
+
// Get Trending Coins
|
| 1242 |
+
async getTrendingCoins() {
|
| 1243 |
+
const url = `${this.config.marketData.primary.baseUrl}/search/trending`;
|
| 1244 |
+
const data = await this.fetchDirect(url);
|
| 1245 |
+
return data.coins.map(item => ({
|
| 1246 |
+
id: item.item.id,
|
| 1247 |
+
name: item.item.name,
|
| 1248 |
+
symbol: item.item.symbol,
|
| 1249 |
+
rank: item.item.market_cap_rank,
|
| 1250 |
+
thumb: item.item.thumb
|
| 1251 |
+
}));
|
| 1252 |
+
}
|
| 1253 |
+
|
| 1254 |
+
// Get Crypto News
|
| 1255 |
+
async getCryptoNews(limit = 10) {
|
| 1256 |
+
const url = `${this.config.news.primary.baseUrl}/posts/?public=true`;
|
| 1257 |
+
const data = await this.fetchDirect(url);
|
| 1258 |
+
return data.results.slice(0, limit).map(post => ({
|
| 1259 |
+
title: post.title,
|
| 1260 |
+
url: post.url,
|
| 1261 |
+
source: post.source.title,
|
| 1262 |
+
published: new Date(post.published_at)
|
| 1263 |
+
}));
|
| 1264 |
+
}
|
| 1265 |
+
|
| 1266 |
+
// Get Recent Whale Transactions
|
| 1267 |
+
async getWhaleTransactions() {
|
| 1268 |
+
try {
|
| 1269 |
+
const url = `${this.config.whaleTracking.primary.baseUrl}/whales/recent`;
|
| 1270 |
+
return await this.fetchDirect(url);
|
| 1271 |
+
} catch (error) {
|
| 1272 |
+
console.warn('Whale API not available');
|
| 1273 |
+
return [];
|
| 1274 |
+
}
|
| 1275 |
+
}
|
| 1276 |
+
|
| 1277 |
+
// Multi-source price aggregator
|
| 1278 |
+
async getAggregatedPrice(symbol) {
|
| 1279 |
+
const sources = [
|
| 1280 |
+
{
|
| 1281 |
+
name: 'CoinGecko',
|
| 1282 |
+
fetch: async () => {
|
| 1283 |
+
const data = await this.fetchDirect(
|
| 1284 |
+
`${this.config.marketData.primary.baseUrl}/simple/price?ids=${symbol}&vs_currencies=usd`
|
| 1285 |
+
);
|
| 1286 |
+
return data[symbol]?.usd;
|
| 1287 |
+
}
|
| 1288 |
+
},
|
| 1289 |
+
{
|
| 1290 |
+
name: 'Binance',
|
| 1291 |
+
fetch: async () => {
|
| 1292 |
+
const data = await this.fetchDirect(
|
| 1293 |
+
`https://api.binance.com/api/v3/ticker/price?symbol=${symbol.toUpperCase()}USDT`
|
| 1294 |
+
);
|
| 1295 |
+
return parseFloat(data.price);
|
| 1296 |
+
}
|
| 1297 |
+
},
|
| 1298 |
+
{
|
| 1299 |
+
name: 'CoinCap',
|
| 1300 |
+
fetch: async () => {
|
| 1301 |
+
const data = await this.fetchDirect(
|
| 1302 |
+
`https://api.coincap.io/v2/assets/${symbol}`
|
| 1303 |
+
);
|
| 1304 |
+
return parseFloat(data.data.priceUsd);
|
| 1305 |
+
}
|
| 1306 |
+
}
|
| 1307 |
+
];
|
| 1308 |
+
|
| 1309 |
+
const prices = await Promise.allSettled(
|
| 1310 |
+
sources.map(async source => ({
|
| 1311 |
+
source: source.name,
|
| 1312 |
+
price: await source.fetch()
|
| 1313 |
+
}))
|
| 1314 |
+
);
|
| 1315 |
+
|
| 1316 |
+
const successful = prices
|
| 1317 |
+
.filter(p => p.status === 'fulfilled')
|
| 1318 |
+
.map(p => p.value);
|
| 1319 |
+
|
| 1320 |
+
if (successful.length === 0) {
|
| 1321 |
+
throw new Error('All price sources failed');
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
const avgPrice = successful.reduce((sum, p) => sum + p.price, 0) / successful.length;
|
| 1325 |
+
|
| 1326 |
+
return {
|
| 1327 |
+
symbol,
|
| 1328 |
+
sources: successful,
|
| 1329 |
+
average: avgPrice,
|
| 1330 |
+
spread: Math.max(...successful.map(p => p.price)) - Math.min(...successful.map(p => p.price))
|
| 1331 |
+
};
|
| 1332 |
+
}
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 1336 |
+
// USAGE EXAMPLES - مثالهای استفاده
|
| 1337 |
+
// ═══════════════════════════════════════════════════════════════════════════════
|
| 1338 |
+
|
| 1339 |
+
// Initialize
|
| 1340 |
+
const api = new CryptoAPIClient(API_CONFIG);
|
| 1341 |
+
|
| 1342 |
+
// Example 1: Get Ethereum Balance
|
| 1343 |
+
async function example1() {
|
| 1344 |
+
try {
|
| 1345 |
+
const address = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
|
| 1346 |
+
const balance = await api.getEthBalance(address);
|
| 1347 |
+
console.log('ETH Balance:', parseInt(balance.result) / 1e18);
|
| 1348 |
+
} catch (error) {
|
| 1349 |
+
console.error('Error:', error.message);
|
| 1350 |
+
}
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
// Example 2: Get Bitcoin Price from Multiple Sources
|
| 1354 |
+
async function example2() {
|
| 1355 |
+
try {
|
| 1356 |
+
const price = await api.getBitcoinPrice();
|
| 1357 |
+
console.log(`BTC Price (${price.source}): $${price.usd}`);
|
| 1358 |
+
} catch (error) {
|
| 1359 |
+
console.error('Error:', error.message);
|
| 1360 |
+
}
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
// Example 3: Get Fear & Greed Index
|
| 1364 |
+
async function example3() {
|
| 1365 |
+
try {
|
| 1366 |
+
const fng = await api.getFearGreed();
|
| 1367 |
+
console.log(`Fear & Greed: ${fng.value} (${fng.classification})`);
|
| 1368 |
+
} catch (error) {
|
| 1369 |
+
console.error('Error:', error.message);
|
| 1370 |
+
}
|
| 1371 |
+
}
|
| 1372 |
+
|
| 1373 |
+
// Example 4: Get Trending Coins
|
| 1374 |
+
async function example4() {
|
| 1375 |
+
try {
|
| 1376 |
+
const trending = await api.getTrendingCoins();
|
| 1377 |
+
console.log('Trending Coins:');
|
| 1378 |
+
trending.forEach((coin, i) => {
|
| 1379 |
+
console.log(`${i + 1}. ${coin.name} (${coin.symbol})`);
|
| 1380 |
+
});
|
| 1381 |
+
} catch (error) {
|
| 1382 |
+
console.error('Error:', error.message);
|
| 1383 |
+
}
|
| 1384 |
+
}
|
| 1385 |
+
|
| 1386 |
+
// Example 5: Get Latest News
|
| 1387 |
+
async function example5() {
|
| 1388 |
+
try {
|
| 1389 |
+
const news = await api.getCryptoNews(5);
|
| 1390 |
+
console.log('Latest News:');
|
| 1391 |
+
news.forEach((article, i) => {
|
| 1392 |
+
console.log(`${i + 1}. ${article.title} - ${article.source}`);
|
| 1393 |
+
});
|
| 1394 |
+
} catch (error) {
|
| 1395 |
+
console.error('Error:', error.message);
|
| 1396 |
+
}
|
| 1397 |
+
}
|
| 1398 |
+
|
| 1399 |
+
// Example 6: Aggregate Price from Multiple Sources
|
| 1400 |
+
async function example6() {
|
| 1401 |
+
try {
|
| 1402 |
+
const priceData = await api.getAggregatedPrice('bitcoin');
|
| 1403 |
+
console.log('Price Sources:');
|
| 1404 |
+
priceData.sources.forEach(s => {
|
| 1405 |
+
console.log(`- ${s.source}: $${s.price.toFixed(2)}`);
|
| 1406 |
+
});
|
| 1407 |
+
console.log(`Average: $${priceData.average.toFixed(2)}`);
|
| 1408 |
+
console.log(`Spread: $${priceData.spread.toFixed(2)}`);
|
| 1409 |
+
} catch (error) {
|
| 1410 |
+
console.error('Error:', error.message);
|
| 1411 |
+
}
|
| 1412 |
+
}
|
| 1413 |
+
|
| 1414 |
+
// Example 7: Dashboard - All Data
|
| 1415 |
+
async function dashboardExample() {
|
| 1416 |
+
console.log('🚀 Loading Crypto Dashboard...\n');
|
| 1417 |
+
|
| 1418 |
+
try {
|
| 1419 |
+
// Price
|
| 1420 |
+
const btcPrice = await api.getBitcoinPrice();
|
| 1421 |
+
console.log(`💰 BTC: $${btcPrice.usd.toLocaleString()}`);
|
| 1422 |
+
|
| 1423 |
+
// Fear & Greed
|
| 1424 |
+
const fng = await api.getFearGreed();
|
| 1425 |
+
console.log(`😱 Fear & Greed: ${fng.value} (${fng.classification})`);
|
| 1426 |
+
|
| 1427 |
+
// Trending
|
| 1428 |
+
const trending = await api.getTrendingCoins();
|
| 1429 |
+
console.log(`\n🔥 Trending:`);
|
| 1430 |
+
trending.slice(0, 3).forEach((coin, i) => {
|
| 1431 |
+
console.log(` ${i + 1}. ${coin.name}`);
|
| 1432 |
+
});
|
| 1433 |
+
|
| 1434 |
+
// News
|
| 1435 |
+
const news = await api.getCryptoNews(3);
|
| 1436 |
+
console.log(`\n📰 Latest News:`);
|
| 1437 |
+
news.forEach((article, i) => {
|
| 1438 |
+
console.log(` ${i + 1}. ${article.title.substring(0, 50)}...`);
|
| 1439 |
+
});
|
| 1440 |
+
|
| 1441 |
+
} catch (error) {
|
| 1442 |
+
console.error('Dashboard Error:', error.message);
|
| 1443 |
+
}
|
| 1444 |
+
}
|
| 1445 |
+
|
| 1446 |
+
// Run examples
|
| 1447 |
+
console.log('═══════════════════════════════════════');
|
| 1448 |
+
console.log(' CRYPTO API CLIENT - TEST SUITE');
|
| 1449 |
+
console.log('═══════════════════════════════════════\n');
|
| 1450 |
+
|
| 1451 |
+
// Uncomment to run specific examples:
|
| 1452 |
+
// example1();
|
| 1453 |
+
// example2();
|
| 1454 |
+
// example3();
|
| 1455 |
+
// example4();
|
| 1456 |
+
// example5();
|
| 1457 |
+
// example6();
|
| 1458 |
+
dashboardExample();
|
| 1459 |
+
|
| 1460 |
+
|
| 1461 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1462 |
+
📝 QUICK REFERENCE - مرجع سریع
|
| 1463 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1464 |
+
|
| 1465 |
+
BEST FREE APIs (بهترین APIهای رایگان):
|
| 1466 |
+
─────────────────────────────────────────
|
| 1467 |
+
|
| 1468 |
+
✅ PRICES & MARKET DATA:
|
| 1469 |
+
1. CoinGecko (بدون کلید، بدون CORS)
|
| 1470 |
+
2. Binance Public API (بدون کلید)
|
| 1471 |
+
3. CoinCap (بدون کلید)
|
| 1472 |
+
4. CoinPaprika (بدون کلید)
|
| 1473 |
+
|
| 1474 |
+
✅ BLOCK EXPLORERS:
|
| 1475 |
+
1. Blockchair (1,440 req/day)
|
| 1476 |
+
2. BlockScout (بدون محدودیت)
|
| 1477 |
+
3. Public RPC nodes (various)
|
| 1478 |
+
|
| 1479 |
+
✅ NEWS:
|
| 1480 |
+
1. CryptoPanic (بدون کلید)
|
| 1481 |
+
2. Reddit JSON API (60 req/min)
|
| 1482 |
+
|
| 1483 |
+
✅ SENTIMENT:
|
| 1484 |
+
1. Alternative.me F&G (بدون محدودیت)
|
| 1485 |
+
|
| 1486 |
+
✅ WHALE TRACKING:
|
| 1487 |
+
1. ClankApp (بدون کلید)
|
| 1488 |
+
2. BitQuery GraphQL (10K/month)
|
| 1489 |
+
|
| 1490 |
+
✅ RPC NODES:
|
| 1491 |
+
1. PublicNode (همه شبکهها)
|
| 1492 |
+
2. Ankr (عمومی)
|
| 1493 |
+
3. LlamaNodes (بدون ثبتنام)
|
| 1494 |
+
|
| 1495 |
+
|
| 1496 |
+
RATE LIMIT STRATEGIES (استراتژیهای محدودیت):
|
| 1497 |
+
───────────────────────────────────────────────
|
| 1498 |
+
|
| 1499 |
+
1. کش کردن (Caching):
|
| 1500 |
+
- ذخیره نتایج برای 1-5 دقیقه
|
| 1501 |
+
- استفاده از localStorage برای کش مرورگر
|
| 1502 |
+
|
| 1503 |
+
2. چرخش کلید (Key Rotation):
|
| 1504 |
+
- استفاده از چندین کلید API
|
| 1505 |
+
- تعویض خودکار در صورت محدودیت
|
| 1506 |
+
|
| 1507 |
+
3. Fallback Chain:
|
| 1508 |
+
- Primary → Fallback1 → Fallback2
|
| 1509 |
+
- تا 5-10 جای��زین برای هر سرویس
|
| 1510 |
+
|
| 1511 |
+
4. Request Queuing:
|
| 1512 |
+
- صف بندی درخواستها
|
| 1513 |
+
- تاخیر بین درخواستها
|
| 1514 |
+
|
| 1515 |
+
5. Multi-Source Aggregation:
|
| 1516 |
+
- دریافت از چند منبع همزمان
|
| 1517 |
+
- میانگین گیری نتایج
|
| 1518 |
+
|
| 1519 |
+
|
| 1520 |
+
ERROR HANDLING (مدیریت خطا):
|
| 1521 |
+
──────────────────────────────
|
| 1522 |
+
|
| 1523 |
+
try {
|
| 1524 |
+
const data = await api.fetchWithFallback(primary, fallbacks, endpoint, params);
|
| 1525 |
+
} catch (error) {
|
| 1526 |
+
if (error.message.includes('rate limit')) {
|
| 1527 |
+
// Switch to fallback
|
| 1528 |
+
} else if (error.message.includes('CORS')) {
|
| 1529 |
+
// Use CORS proxy
|
| 1530 |
+
} else {
|
| 1531 |
+
// Show error to user
|
| 1532 |
+
}
|
| 1533 |
+
}
|
| 1534 |
+
|
| 1535 |
+
|
| 1536 |
+
DEPLOYMENT TIPS (نکات استقرار):
|
| 1537 |
+
─────────────────────────────────
|
| 1538 |
+
|
| 1539 |
+
1. Backend Proxy (توصیه میشود):
|
| 1540 |
+
- Node.js/Express proxy server
|
| 1541 |
+
- Cloudflare Worker
|
| 1542 |
+
- Vercel Serverless Function
|
| 1543 |
+
|
| 1544 |
+
2. Environment Variables:
|
| 1545 |
+
- ذخیره کلیدها در .env
|
| 1546 |
+
- عدم نمایش در کد فرانتاند
|
| 1547 |
+
|
| 1548 |
+
3. Rate Limiting:
|
| 1549 |
+
- محدودسازی درخواست کاربر
|
| 1550 |
+
- استفاده از Redis برای کنترل
|
| 1551 |
+
|
| 1552 |
+
4. Monitoring:
|
| 1553 |
+
- لاگ گرفتن از خطاها
|
| 1554 |
+
- ردیابی استفاده از API
|
| 1555 |
+
|
| 1556 |
+
|
| 1557 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1558 |
+
🔗 USEFUL LINKS - لینکهای مفید
|
| 1559 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1560 |
+
|
| 1561 |
+
DOCUMENTATION:
|
| 1562 |
+
• CoinGecko API: https://www.coingecko.com/api/documentation
|
| 1563 |
+
• Etherscan API: https://docs.etherscan.io
|
| 1564 |
+
• BscScan API: https://docs.bscscan.com
|
| 1565 |
+
• TronGrid: https://developers.tron.network
|
| 1566 |
+
• Alchemy: https://docs.alchemy.com
|
| 1567 |
+
• Infura: https://docs.infura.io
|
| 1568 |
+
• The Graph: https://thegraph.com/docs
|
| 1569 |
+
• BitQuery: https://docs.bitquery.io
|
| 1570 |
+
|
| 1571 |
+
CORS PROXY ALTERNATIVES:
|
| 1572 |
+
• CORS Anywhere: https://github.com/Rob--W/cors-anywhere
|
| 1573 |
+
• AllOrigins: https://github.com/gnuns/allOrigins
|
| 1574 |
+
• CORS.SH: https://cors.sh
|
| 1575 |
+
• Corsfix: https://corsfix.com
|
| 1576 |
+
|
| 1577 |
+
RPC LISTS:
|
| 1578 |
+
• ChainList: https://chainlist.org
|
| 1579 |
+
• Awesome RPC: https://github.com/arddluma/awesome-list-rpc-nodes-providers
|
| 1580 |
+
|
| 1581 |
+
TOOLS:
|
| 1582 |
+
• Postman: https://www.postman.com
|
| 1583 |
+
• Insomnia: https://insomnia.rest
|
| 1584 |
+
• GraphiQL: https://graphiql-online.com
|
| 1585 |
+
|
| 1586 |
+
|
| 1587 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1588 |
+
⚠️ IMPORTANT NOTES - نکات مهم
|
| 1589 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1590 |
+
|
| 1591 |
+
1. ⚠️ NEVER expose API keys in frontend code
|
| 1592 |
+
- همیشه از backend proxy استفاده کنید
|
| 1593 |
+
- کلیدها را در environment variables ذخیره کنید
|
| 1594 |
+
|
| 1595 |
+
2. 🔄 Always implement fallbacks
|
| 1596 |
+
- حداقل 2-3 جایگزین برای هر سرویس
|
| 1597 |
+
- تست منظم fallbackها
|
| 1598 |
+
|
| 1599 |
+
3. 💾 Cache responses when possible
|
| 1600 |
+
- صرفهجویی در استفاده از API
|
| 1601 |
+
- سرعت بیشتر برای کاربر
|
| 1602 |
+
|
| 1603 |
+
4. 📊 Monitor API usage
|
| 1604 |
+
- ردیابی تعداد درخواستها
|
| 1605 |
+
- هشدار قبل از رسیدن به محدودیت
|
| 1606 |
+
|
| 1607 |
+
5. 🔐 Secure your endpoints
|
| 1608 |
+
- محدودسازی domain
|
| 1609 |
+
- استفاده از CORS headers
|
| 1610 |
+
- Rate limiting برای کاربران
|
| 1611 |
+
|
| 1612 |
+
6. 🌐 Test with and without CORS proxies
|
| 1613 |
+
- برخی APIها CORS را پشتیبانی میکنند
|
| 1614 |
+
- استفاده از پروکسی فقط در صورت نیاز
|
| 1615 |
+
|
| 1616 |
+
7. 📱 Mobile-friendly implementations
|
| 1617 |
+
- بهینهسازی برای شبکههای ضعیف
|
| 1618 |
+
- کاهش اندازه درخواستها
|
| 1619 |
+
|
| 1620 |
+
|
| 1621 |
+
═══════════════════════════════════════════════════════════════════════════════════════
|
| 1622 |
+
END OF CONFIGURATION FILE
|
| 1623 |
+
پایان فایل تنظیمات
|
| 1624 |
+
═══════════════════════════════════════════════════════════════════════════════════��═══
|
| 1625 |
+
|
| 1626 |
+
Last Updated: October 31, 2025
|
| 1627 |
+
Version: 2.0
|
| 1628 |
+
Author: AI Assistant
|
| 1629 |
+
License: Free to use
|
| 1630 |
+
|
| 1631 |
+
For updates and more resources, check:
|
| 1632 |
+
- GitHub: Search for "awesome-crypto-apis"
|
| 1633 |
+
- Reddit: r/CryptoCurrency, r/ethdev
|
| 1634 |
+
- Discord: Web3 developer communities
|
api-resources/crypto_resources_unified_2025-11-11.json
ADDED
|
@@ -0,0 +1,2097 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema": {
|
| 3 |
+
"name": "Crypto Resource Registry",
|
| 4 |
+
"version": "1.0.0",
|
| 5 |
+
"updated_at": "2025-11-11",
|
| 6 |
+
"description": "Single-file registry of crypto data sources with uniform fields for agents (Cloud Code, Cursor, Claude, etc.).",
|
| 7 |
+
"spec": {
|
| 8 |
+
"entry_shape": {
|
| 9 |
+
"id": "string",
|
| 10 |
+
"name": "string",
|
| 11 |
+
"category_or_chain": "string (category / chain / type / role)",
|
| 12 |
+
"base_url": "string",
|
| 13 |
+
"auth": {
|
| 14 |
+
"type": "string",
|
| 15 |
+
"key": "string|null",
|
| 16 |
+
"param_name/header_name": "string|null"
|
| 17 |
+
},
|
| 18 |
+
"docs_url": "string|null",
|
| 19 |
+
"endpoints": "object|string|null",
|
| 20 |
+
"notes": "string|null"
|
| 21 |
+
}
|
| 22 |
+
}
|
| 23 |
+
},
|
| 24 |
+
"registry": {
|
| 25 |
+
"metadata": {
|
| 26 |
+
"description": "Comprehensive cryptocurrency data collection database compiled from provided documents. Includes free and limited resources for RPC nodes, block explorers, market data, news, sentiment, on-chain analytics, whale tracking, community sentiment, Hugging Face models/datasets, free HTTP endpoints, and local backend routes. Uniform format: each entry has 'id', 'name', 'category' (or 'chain'/'role' where applicable), 'base_url', 'auth' (object with 'type', 'key' if embedded, 'param_name', etc.), 'docs_url', and optional 'endpoints' or 'notes'. Keys are embedded where provided in sources. Structure designed for easy parsing by code-writing bots.",
|
| 27 |
+
"version": "1.0",
|
| 28 |
+
"updated": "November 11, 2025",
|
| 29 |
+
"sources": [
|
| 30 |
+
"api - Copy.txt",
|
| 31 |
+
"api-config-complete (1).txt",
|
| 32 |
+
"crypto_resources.ts",
|
| 33 |
+
"additional JSON structures"
|
| 34 |
+
],
|
| 35 |
+
"total_entries": 200
|
| 36 |
+
},
|
| 37 |
+
"rpc_nodes": [
|
| 38 |
+
{
|
| 39 |
+
"id": "infura_eth_mainnet",
|
| 40 |
+
"name": "Infura Ethereum Mainnet",
|
| 41 |
+
"chain": "ethereum",
|
| 42 |
+
"role": "rpc",
|
| 43 |
+
"base_url": "https://mainnet.infura.io/v3/{PROJECT_ID}",
|
| 44 |
+
"auth": {
|
| 45 |
+
"type": "apiKeyPath",
|
| 46 |
+
"key": null,
|
| 47 |
+
"param_name": "PROJECT_ID",
|
| 48 |
+
"notes": "Replace {PROJECT_ID} with your Infura project ID"
|
| 49 |
+
},
|
| 50 |
+
"docs_url": "https://docs.infura.io",
|
| 51 |
+
"notes": "Free tier: 100K req/day"
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"id": "infura_eth_sepolia",
|
| 55 |
+
"name": "Infura Ethereum Sepolia",
|
| 56 |
+
"chain": "ethereum",
|
| 57 |
+
"role": "rpc",
|
| 58 |
+
"base_url": "https://sepolia.infura.io/v3/{PROJECT_ID}",
|
| 59 |
+
"auth": {
|
| 60 |
+
"type": "apiKeyPath",
|
| 61 |
+
"key": null,
|
| 62 |
+
"param_name": "PROJECT_ID",
|
| 63 |
+
"notes": "Replace {PROJECT_ID} with your Infura project ID"
|
| 64 |
+
},
|
| 65 |
+
"docs_url": "https://docs.infura.io",
|
| 66 |
+
"notes": "Testnet"
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"id": "alchemy_eth_mainnet",
|
| 70 |
+
"name": "Alchemy Ethereum Mainnet",
|
| 71 |
+
"chain": "ethereum",
|
| 72 |
+
"role": "rpc",
|
| 73 |
+
"base_url": "https://eth-mainnet.g.alchemy.com/v2/{API_KEY}",
|
| 74 |
+
"auth": {
|
| 75 |
+
"type": "apiKeyPath",
|
| 76 |
+
"key": null,
|
| 77 |
+
"param_name": "API_KEY",
|
| 78 |
+
"notes": "Replace {API_KEY} with your Alchemy key"
|
| 79 |
+
},
|
| 80 |
+
"docs_url": "https://docs.alchemy.com",
|
| 81 |
+
"notes": "Free tier: 300M compute units/month"
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"id": "alchemy_eth_mainnet_ws",
|
| 85 |
+
"name": "Alchemy Ethereum Mainnet WS",
|
| 86 |
+
"chain": "ethereum",
|
| 87 |
+
"role": "websocket",
|
| 88 |
+
"base_url": "wss://eth-mainnet.g.alchemy.com/v2/{API_KEY}",
|
| 89 |
+
"auth": {
|
| 90 |
+
"type": "apiKeyPath",
|
| 91 |
+
"key": null,
|
| 92 |
+
"param_name": "API_KEY",
|
| 93 |
+
"notes": "Replace {API_KEY} with your Alchemy key"
|
| 94 |
+
},
|
| 95 |
+
"docs_url": "https://docs.alchemy.com",
|
| 96 |
+
"notes": "WebSocket for real-time"
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"id": "ankr_eth",
|
| 100 |
+
"name": "Ankr Ethereum",
|
| 101 |
+
"chain": "ethereum",
|
| 102 |
+
"role": "rpc",
|
| 103 |
+
"base_url": "https://rpc.ankr.com/eth",
|
| 104 |
+
"auth": {
|
| 105 |
+
"type": "none"
|
| 106 |
+
},
|
| 107 |
+
"docs_url": "https://www.ankr.com/docs",
|
| 108 |
+
"notes": "Free: no public limit"
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "publicnode_eth_mainnet",
|
| 112 |
+
"name": "PublicNode Ethereum",
|
| 113 |
+
"chain": "ethereum",
|
| 114 |
+
"role": "rpc",
|
| 115 |
+
"base_url": "https://ethereum.publicnode.com",
|
| 116 |
+
"auth": {
|
| 117 |
+
"type": "none"
|
| 118 |
+
},
|
| 119 |
+
"docs_url": null,
|
| 120 |
+
"notes": "Fully free"
|
| 121 |
+
},
|
| 122 |
+
{
|
| 123 |
+
"id": "publicnode_eth_allinone",
|
| 124 |
+
"name": "PublicNode Ethereum All-in-one",
|
| 125 |
+
"chain": "ethereum",
|
| 126 |
+
"role": "rpc",
|
| 127 |
+
"base_url": "https://ethereum-rpc.publicnode.com",
|
| 128 |
+
"auth": {
|
| 129 |
+
"type": "none"
|
| 130 |
+
},
|
| 131 |
+
"docs_url": null,
|
| 132 |
+
"notes": "All-in-one endpoint"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"id": "cloudflare_eth",
|
| 136 |
+
"name": "Cloudflare Ethereum",
|
| 137 |
+
"chain": "ethereum",
|
| 138 |
+
"role": "rpc",
|
| 139 |
+
"base_url": "https://cloudflare-eth.com",
|
| 140 |
+
"auth": {
|
| 141 |
+
"type": "none"
|
| 142 |
+
},
|
| 143 |
+
"docs_url": null,
|
| 144 |
+
"notes": "Free"
|
| 145 |
+
},
|
| 146 |
+
{
|
| 147 |
+
"id": "llamanodes_eth",
|
| 148 |
+
"name": "LlamaNodes Ethereum",
|
| 149 |
+
"chain": "ethereum",
|
| 150 |
+
"role": "rpc",
|
| 151 |
+
"base_url": "https://eth.llamarpc.com",
|
| 152 |
+
"auth": {
|
| 153 |
+
"type": "none"
|
| 154 |
+
},
|
| 155 |
+
"docs_url": null,
|
| 156 |
+
"notes": "Free"
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"id": "one_rpc_eth",
|
| 160 |
+
"name": "1RPC Ethereum",
|
| 161 |
+
"chain": "ethereum",
|
| 162 |
+
"role": "rpc",
|
| 163 |
+
"base_url": "https://1rpc.io/eth",
|
| 164 |
+
"auth": {
|
| 165 |
+
"type": "none"
|
| 166 |
+
},
|
| 167 |
+
"docs_url": null,
|
| 168 |
+
"notes": "Free with privacy"
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
"id": "drpc_eth",
|
| 172 |
+
"name": "dRPC Ethereum",
|
| 173 |
+
"chain": "ethereum",
|
| 174 |
+
"role": "rpc",
|
| 175 |
+
"base_url": "https://eth.drpc.org",
|
| 176 |
+
"auth": {
|
| 177 |
+
"type": "none"
|
| 178 |
+
},
|
| 179 |
+
"docs_url": "https://drpc.org",
|
| 180 |
+
"notes": "Decentralized"
|
| 181 |
+
},
|
| 182 |
+
{
|
| 183 |
+
"id": "bsc_official_mainnet",
|
| 184 |
+
"name": "BSC Official Mainnet",
|
| 185 |
+
"chain": "bsc",
|
| 186 |
+
"role": "rpc",
|
| 187 |
+
"base_url": "https://bsc-dataseed.binance.org",
|
| 188 |
+
"auth": {
|
| 189 |
+
"type": "none"
|
| 190 |
+
},
|
| 191 |
+
"docs_url": null,
|
| 192 |
+
"notes": "Free"
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"id": "bsc_official_alt1",
|
| 196 |
+
"name": "BSC Official Alt1",
|
| 197 |
+
"chain": "bsc",
|
| 198 |
+
"role": "rpc",
|
| 199 |
+
"base_url": "https://bsc-dataseed1.defibit.io",
|
| 200 |
+
"auth": {
|
| 201 |
+
"type": "none"
|
| 202 |
+
},
|
| 203 |
+
"docs_url": null,
|
| 204 |
+
"notes": "Free alternative"
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "bsc_official_alt2",
|
| 208 |
+
"name": "BSC Official Alt2",
|
| 209 |
+
"chain": "bsc",
|
| 210 |
+
"role": "rpc",
|
| 211 |
+
"base_url": "https://bsc-dataseed1.ninicoin.io",
|
| 212 |
+
"auth": {
|
| 213 |
+
"type": "none"
|
| 214 |
+
},
|
| 215 |
+
"docs_url": null,
|
| 216 |
+
"notes": "Free alternative"
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"id": "ankr_bsc",
|
| 220 |
+
"name": "Ankr BSC",
|
| 221 |
+
"chain": "bsc",
|
| 222 |
+
"role": "rpc",
|
| 223 |
+
"base_url": "https://rpc.ankr.com/bsc",
|
| 224 |
+
"auth": {
|
| 225 |
+
"type": "none"
|
| 226 |
+
},
|
| 227 |
+
"docs_url": null,
|
| 228 |
+
"notes": "Free"
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"id": "publicnode_bsc",
|
| 232 |
+
"name": "PublicNode BSC",
|
| 233 |
+
"chain": "bsc",
|
| 234 |
+
"role": "rpc",
|
| 235 |
+
"base_url": "https://bsc-rpc.publicnode.com",
|
| 236 |
+
"auth": {
|
| 237 |
+
"type": "none"
|
| 238 |
+
},
|
| 239 |
+
"docs_url": null,
|
| 240 |
+
"notes": "Free"
|
| 241 |
+
},
|
| 242 |
+
{
|
| 243 |
+
"id": "nodereal_bsc",
|
| 244 |
+
"name": "Nodereal BSC",
|
| 245 |
+
"chain": "bsc",
|
| 246 |
+
"role": "rpc",
|
| 247 |
+
"base_url": "https://bsc-mainnet.nodereal.io/v1/{API_KEY}",
|
| 248 |
+
"auth": {
|
| 249 |
+
"type": "apiKeyPath",
|
| 250 |
+
"key": null,
|
| 251 |
+
"param_name": "API_KEY",
|
| 252 |
+
"notes": "Free tier: 3M req/day"
|
| 253 |
+
},
|
| 254 |
+
"docs_url": "https://docs.nodereal.io",
|
| 255 |
+
"notes": "Requires key for higher limits"
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"id": "trongrid_mainnet",
|
| 259 |
+
"name": "TronGrid Mainnet",
|
| 260 |
+
"chain": "tron",
|
| 261 |
+
"role": "rpc",
|
| 262 |
+
"base_url": "https://api.trongrid.io",
|
| 263 |
+
"auth": {
|
| 264 |
+
"type": "none"
|
| 265 |
+
},
|
| 266 |
+
"docs_url": "https://developers.tron.network/docs",
|
| 267 |
+
"notes": "Free"
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"id": "tronstack_mainnet",
|
| 271 |
+
"name": "TronStack Mainnet",
|
| 272 |
+
"chain": "tron",
|
| 273 |
+
"role": "rpc",
|
| 274 |
+
"base_url": "https://api.tronstack.io",
|
| 275 |
+
"auth": {
|
| 276 |
+
"type": "none"
|
| 277 |
+
},
|
| 278 |
+
"docs_url": null,
|
| 279 |
+
"notes": "Free, similar to TronGrid"
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"id": "tron_nile_testnet",
|
| 283 |
+
"name": "Tron Nile Testnet",
|
| 284 |
+
"chain": "tron",
|
| 285 |
+
"role": "rpc",
|
| 286 |
+
"base_url": "https://api.nileex.io",
|
| 287 |
+
"auth": {
|
| 288 |
+
"type": "none"
|
| 289 |
+
},
|
| 290 |
+
"docs_url": null,
|
| 291 |
+
"notes": "Testnet"
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"id": "polygon_official_mainnet",
|
| 295 |
+
"name": "Polygon Official Mainnet",
|
| 296 |
+
"chain": "polygon",
|
| 297 |
+
"role": "rpc",
|
| 298 |
+
"base_url": "https://polygon-rpc.com",
|
| 299 |
+
"auth": {
|
| 300 |
+
"type": "none"
|
| 301 |
+
},
|
| 302 |
+
"docs_url": null,
|
| 303 |
+
"notes": "Free"
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"id": "polygon_mumbai",
|
| 307 |
+
"name": "Polygon Mumbai",
|
| 308 |
+
"chain": "polygon",
|
| 309 |
+
"role": "rpc",
|
| 310 |
+
"base_url": "https://rpc-mumbai.maticvigil.com",
|
| 311 |
+
"auth": {
|
| 312 |
+
"type": "none"
|
| 313 |
+
},
|
| 314 |
+
"docs_url": null,
|
| 315 |
+
"notes": "Testnet"
|
| 316 |
+
},
|
| 317 |
+
{
|
| 318 |
+
"id": "ankr_polygon",
|
| 319 |
+
"name": "Ankr Polygon",
|
| 320 |
+
"chain": "polygon",
|
| 321 |
+
"role": "rpc",
|
| 322 |
+
"base_url": "https://rpc.ankr.com/polygon",
|
| 323 |
+
"auth": {
|
| 324 |
+
"type": "none"
|
| 325 |
+
},
|
| 326 |
+
"docs_url": null,
|
| 327 |
+
"notes": "Free"
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"id": "publicnode_polygon_bor",
|
| 331 |
+
"name": "PublicNode Polygon Bor",
|
| 332 |
+
"chain": "polygon",
|
| 333 |
+
"role": "rpc",
|
| 334 |
+
"base_url": "https://polygon-bor-rpc.publicnode.com",
|
| 335 |
+
"auth": {
|
| 336 |
+
"type": "none"
|
| 337 |
+
},
|
| 338 |
+
"docs_url": null,
|
| 339 |
+
"notes": "Free"
|
| 340 |
+
}
|
| 341 |
+
],
|
| 342 |
+
"block_explorers": [
|
| 343 |
+
{
|
| 344 |
+
"id": "etherscan_primary",
|
| 345 |
+
"name": "Etherscan",
|
| 346 |
+
"chain": "ethereum",
|
| 347 |
+
"role": "primary",
|
| 348 |
+
"base_url": "https://api.etherscan.io/api",
|
| 349 |
+
"auth": {
|
| 350 |
+
"type": "apiKeyQuery",
|
| 351 |
+
"key": "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
|
| 352 |
+
"param_name": "apikey"
|
| 353 |
+
},
|
| 354 |
+
"docs_url": "https://docs.etherscan.io",
|
| 355 |
+
"endpoints": {
|
| 356 |
+
"balance": "?module=account&action=balance&address={address}&tag=latest&apikey={key}",
|
| 357 |
+
"transactions": "?module=account&action=txlist&address={address}&startblock=0&endblock=99999999&sort=asc&apikey={key}",
|
| 358 |
+
"token_balance": "?module=account&action=tokenbalance&contractaddress={contract}&address={address}&tag=latest&apikey={key}",
|
| 359 |
+
"gas_price": "?module=gastracker&action=gasoracle&apikey={key}"
|
| 360 |
+
},
|
| 361 |
+
"notes": "Rate limit: 5 calls/sec (free tier)"
|
| 362 |
+
},
|
| 363 |
+
{
|
| 364 |
+
"id": "etherscan_secondary",
|
| 365 |
+
"name": "Etherscan (secondary key)",
|
| 366 |
+
"chain": "ethereum",
|
| 367 |
+
"role": "fallback",
|
| 368 |
+
"base_url": "https://api.etherscan.io/api",
|
| 369 |
+
"auth": {
|
| 370 |
+
"type": "apiKeyQuery",
|
| 371 |
+
"key": "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45",
|
| 372 |
+
"param_name": "apikey"
|
| 373 |
+
},
|
| 374 |
+
"docs_url": "https://docs.etherscan.io",
|
| 375 |
+
"endpoints": {
|
| 376 |
+
"balance": "?module=account&action=balance&address={address}&tag=latest&apikey={key}",
|
| 377 |
+
"transactions": "?module=account&action=txlist&address={address}&startblock=0&endblock=99999999&sort=asc&apikey={key}",
|
| 378 |
+
"token_balance": "?module=account&action=tokenbalance&contractaddress={contract}&address={address}&tag=latest&apikey={key}",
|
| 379 |
+
"gas_price": "?module=gastracker&action=gasoracle&apikey={key}"
|
| 380 |
+
},
|
| 381 |
+
"notes": "Backup key for Etherscan"
|
| 382 |
+
},
|
| 383 |
+
{
|
| 384 |
+
"id": "blockchair_ethereum",
|
| 385 |
+
"name": "Blockchair Ethereum",
|
| 386 |
+
"chain": "ethereum",
|
| 387 |
+
"role": "fallback",
|
| 388 |
+
"base_url": "https://api.blockchair.com/ethereum",
|
| 389 |
+
"auth": {
|
| 390 |
+
"type": "apiKeyQueryOptional",
|
| 391 |
+
"key": null,
|
| 392 |
+
"param_name": "key"
|
| 393 |
+
},
|
| 394 |
+
"docs_url": "https://blockchair.com/api/docs",
|
| 395 |
+
"endpoints": {
|
| 396 |
+
"address_dashboard": "/dashboards/address/{address}?key={key}"
|
| 397 |
+
},
|
| 398 |
+
"notes": "Free: 1,440 requests/day"
|
| 399 |
+
},
|
| 400 |
+
{
|
| 401 |
+
"id": "blockscout_ethereum",
|
| 402 |
+
"name": "Blockscout Ethereum",
|
| 403 |
+
"chain": "ethereum",
|
| 404 |
+
"role": "fallback",
|
| 405 |
+
"base_url": "https://eth.blockscout.com/api",
|
| 406 |
+
"auth": {
|
| 407 |
+
"type": "none"
|
| 408 |
+
},
|
| 409 |
+
"docs_url": "https://docs.blockscout.com",
|
| 410 |
+
"endpoints": {
|
| 411 |
+
"balance": "?module=account&action=balance&address={address}"
|
| 412 |
+
},
|
| 413 |
+
"notes": "Open source, no limit"
|
| 414 |
+
},
|
| 415 |
+
{
|
| 416 |
+
"id": "ethplorer",
|
| 417 |
+
"name": "Ethplorer",
|
| 418 |
+
"chain": "ethereum",
|
| 419 |
+
"role": "fallback",
|
| 420 |
+
"base_url": "https://api.ethplorer.io",
|
| 421 |
+
"auth": {
|
| 422 |
+
"type": "apiKeyQueryOptional",
|
| 423 |
+
"key": "freekey",
|
| 424 |
+
"param_name": "apiKey"
|
| 425 |
+
},
|
| 426 |
+
"docs_url": "https://github.com/EverexIO/Ethplorer/wiki/Ethplorer-API",
|
| 427 |
+
"endpoints": {
|
| 428 |
+
"address_info": "/getAddressInfo/{address}?apiKey={key}"
|
| 429 |
+
},
|
| 430 |
+
"notes": "Free tier limited"
|
| 431 |
+
},
|
| 432 |
+
{
|
| 433 |
+
"id": "etherchain",
|
| 434 |
+
"name": "Etherchain",
|
| 435 |
+
"chain": "ethereum",
|
| 436 |
+
"role": "fallback",
|
| 437 |
+
"base_url": "https://www.etherchain.org/api",
|
| 438 |
+
"auth": {
|
| 439 |
+
"type": "none"
|
| 440 |
+
},
|
| 441 |
+
"docs_url": "https://www.etherchain.org/documentation/api",
|
| 442 |
+
"endpoints": {},
|
| 443 |
+
"notes": "Free"
|
| 444 |
+
},
|
| 445 |
+
{
|
| 446 |
+
"id": "chainlens",
|
| 447 |
+
"name": "Chainlens",
|
| 448 |
+
"chain": "ethereum",
|
| 449 |
+
"role": "fallback",
|
| 450 |
+
"base_url": "https://api.chainlens.com",
|
| 451 |
+
"auth": {
|
| 452 |
+
"type": "none"
|
| 453 |
+
},
|
| 454 |
+
"docs_url": "https://docs.chainlens.com",
|
| 455 |
+
"endpoints": {},
|
| 456 |
+
"notes": "Free tier available"
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"id": "bscscan_primary",
|
| 460 |
+
"name": "BscScan",
|
| 461 |
+
"chain": "bsc",
|
| 462 |
+
"role": "primary",
|
| 463 |
+
"base_url": "https://api.bscscan.com/api",
|
| 464 |
+
"auth": {
|
| 465 |
+
"type": "apiKeyQuery",
|
| 466 |
+
"key": "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT",
|
| 467 |
+
"param_name": "apikey"
|
| 468 |
+
},
|
| 469 |
+
"docs_url": "https://docs.bscscan.com",
|
| 470 |
+
"endpoints": {
|
| 471 |
+
"bnb_balance": "?module=account&action=balance&address={address}&apikey={key}",
|
| 472 |
+
"bep20_balance": "?module=account&action=tokenbalance&contractaddress={token}&address={address}&apikey={key}",
|
| 473 |
+
"transactions": "?module=account&action=txlist&address={address}&apikey={key}"
|
| 474 |
+
},
|
| 475 |
+
"notes": "Rate limit: 5 calls/sec"
|
| 476 |
+
},
|
| 477 |
+
{
|
| 478 |
+
"id": "bitquery_bsc",
|
| 479 |
+
"name": "BitQuery (BSC)",
|
| 480 |
+
"chain": "bsc",
|
| 481 |
+
"role": "fallback",
|
| 482 |
+
"base_url": "https://graphql.bitquery.io",
|
| 483 |
+
"auth": {
|
| 484 |
+
"type": "none"
|
| 485 |
+
},
|
| 486 |
+
"docs_url": "https://docs.bitquery.io",
|
| 487 |
+
"endpoints": {
|
| 488 |
+
"graphql_example": "POST with body: { query: '{ ethereum(network: bsc) { address(address: {is: \"{address}\"}) { balances { currency { symbol } value } } } }' }"
|
| 489 |
+
},
|
| 490 |
+
"notes": "Free: 10K queries/month"
|
| 491 |
+
},
|
| 492 |
+
{
|
| 493 |
+
"id": "ankr_multichain_bsc",
|
| 494 |
+
"name": "Ankr MultiChain (BSC)",
|
| 495 |
+
"chain": "bsc",
|
| 496 |
+
"role": "fallback",
|
| 497 |
+
"base_url": "https://rpc.ankr.com/multichain",
|
| 498 |
+
"auth": {
|
| 499 |
+
"type": "none"
|
| 500 |
+
},
|
| 501 |
+
"docs_url": "https://www.ankr.com/docs/",
|
| 502 |
+
"endpoints": {
|
| 503 |
+
"json_rpc": "POST with JSON-RPC body"
|
| 504 |
+
},
|
| 505 |
+
"notes": "Free public endpoints"
|
| 506 |
+
},
|
| 507 |
+
{
|
| 508 |
+
"id": "nodereal_bsc_explorer",
|
| 509 |
+
"name": "Nodereal BSC",
|
| 510 |
+
"chain": "bsc",
|
| 511 |
+
"role": "fallback",
|
| 512 |
+
"base_url": "https://bsc-mainnet.nodereal.io/v1/{API_KEY}",
|
| 513 |
+
"auth": {
|
| 514 |
+
"type": "apiKeyPath",
|
| 515 |
+
"key": null,
|
| 516 |
+
"param_name": "API_KEY"
|
| 517 |
+
},
|
| 518 |
+
"docs_url": "https://docs.nodereal.io",
|
| 519 |
+
"notes": "Free tier: 3M requests/day"
|
| 520 |
+
},
|
| 521 |
+
{
|
| 522 |
+
"id": "bsctrace",
|
| 523 |
+
"name": "BscTrace",
|
| 524 |
+
"chain": "bsc",
|
| 525 |
+
"role": "fallback",
|
| 526 |
+
"base_url": "https://api.bsctrace.com",
|
| 527 |
+
"auth": {
|
| 528 |
+
"type": "none"
|
| 529 |
+
},
|
| 530 |
+
"docs_url": null,
|
| 531 |
+
"endpoints": {},
|
| 532 |
+
"notes": "Free limited"
|
| 533 |
+
},
|
| 534 |
+
{
|
| 535 |
+
"id": "oneinch_bsc_api",
|
| 536 |
+
"name": "1inch BSC API",
|
| 537 |
+
"chain": "bsc",
|
| 538 |
+
"role": "fallback",
|
| 539 |
+
"base_url": "https://api.1inch.io/v5.0/56",
|
| 540 |
+
"auth": {
|
| 541 |
+
"type": "none"
|
| 542 |
+
},
|
| 543 |
+
"docs_url": "https://docs.1inch.io",
|
| 544 |
+
"endpoints": {},
|
| 545 |
+
"notes": "For trading data, free"
|
| 546 |
+
},
|
| 547 |
+
{
|
| 548 |
+
"id": "tronscan_primary",
|
| 549 |
+
"name": "TronScan",
|
| 550 |
+
"chain": "tron",
|
| 551 |
+
"role": "primary",
|
| 552 |
+
"base_url": "https://apilist.tronscanapi.com/api",
|
| 553 |
+
"auth": {
|
| 554 |
+
"type": "apiKeyQuery",
|
| 555 |
+
"key": "7ae72726-bffe-4e74-9c33-97b761eeea21",
|
| 556 |
+
"param_name": "apiKey"
|
| 557 |
+
},
|
| 558 |
+
"docs_url": "https://github.com/tronscan/tronscan-frontend/blob/dev2019/document/api.md",
|
| 559 |
+
"endpoints": {
|
| 560 |
+
"account": "/account?address={address}",
|
| 561 |
+
"transactions": "/transaction?address={address}&limit=20",
|
| 562 |
+
"trc20_transfers": "/token_trc20/transfers?address={address}",
|
| 563 |
+
"account_resources": "/account/detail?address={address}"
|
| 564 |
+
},
|
| 565 |
+
"notes": "Rate limit varies"
|
| 566 |
+
},
|
| 567 |
+
{
|
| 568 |
+
"id": "trongrid_explorer",
|
| 569 |
+
"name": "TronGrid (Official)",
|
| 570 |
+
"chain": "tron",
|
| 571 |
+
"role": "fallback",
|
| 572 |
+
"base_url": "https://api.trongrid.io",
|
| 573 |
+
"auth": {
|
| 574 |
+
"type": "none"
|
| 575 |
+
},
|
| 576 |
+
"docs_url": "https://developers.tron.network/docs",
|
| 577 |
+
"endpoints": {
|
| 578 |
+
"get_account": "POST /wallet/getaccount with body: { \"address\": \"{address}\", \"visible\": true }"
|
| 579 |
+
},
|
| 580 |
+
"notes": "Free public"
|
| 581 |
+
},
|
| 582 |
+
{
|
| 583 |
+
"id": "blockchair_tron",
|
| 584 |
+
"name": "Blockchair TRON",
|
| 585 |
+
"chain": "tron",
|
| 586 |
+
"role": "fallback",
|
| 587 |
+
"base_url": "https://api.blockchair.com/tron",
|
| 588 |
+
"auth": {
|
| 589 |
+
"type": "apiKeyQueryOptional",
|
| 590 |
+
"key": null,
|
| 591 |
+
"param_name": "key"
|
| 592 |
+
},
|
| 593 |
+
"docs_url": "https://blockchair.com/api/docs",
|
| 594 |
+
"endpoints": {
|
| 595 |
+
"address_dashboard": "/dashboards/address/{address}?key={key}"
|
| 596 |
+
},
|
| 597 |
+
"notes": "Free: 1,440 req/day"
|
| 598 |
+
},
|
| 599 |
+
{
|
| 600 |
+
"id": "tronscan_api_v2",
|
| 601 |
+
"name": "Tronscan API v2",
|
| 602 |
+
"chain": "tron",
|
| 603 |
+
"role": "fallback",
|
| 604 |
+
"base_url": "https://api.tronscan.org/api",
|
| 605 |
+
"auth": {
|
| 606 |
+
"type": "none"
|
| 607 |
+
},
|
| 608 |
+
"docs_url": null,
|
| 609 |
+
"endpoints": {},
|
| 610 |
+
"notes": "Alternative endpoint, similar structure"
|
| 611 |
+
},
|
| 612 |
+
{
|
| 613 |
+
"id": "getblock_tron",
|
| 614 |
+
"name": "GetBlock TRON",
|
| 615 |
+
"chain": "tron",
|
| 616 |
+
"role": "fallback",
|
| 617 |
+
"base_url": "https://go.getblock.io/tron",
|
| 618 |
+
"auth": {
|
| 619 |
+
"type": "none"
|
| 620 |
+
},
|
| 621 |
+
"docs_url": "https://getblock.io/docs/",
|
| 622 |
+
"endpoints": {},
|
| 623 |
+
"notes": "Free tier available"
|
| 624 |
+
}
|
| 625 |
+
],
|
| 626 |
+
"market_data_apis": [
|
| 627 |
+
{
|
| 628 |
+
"id": "coingecko",
|
| 629 |
+
"name": "CoinGecko",
|
| 630 |
+
"role": "primary_free",
|
| 631 |
+
"base_url": "https://api.coingecko.com/api/v3",
|
| 632 |
+
"auth": {
|
| 633 |
+
"type": "none"
|
| 634 |
+
},
|
| 635 |
+
"docs_url": "https://www.coingecko.com/en/api/documentation",
|
| 636 |
+
"endpoints": {
|
| 637 |
+
"simple_price": "/simple/price?ids={ids}&vs_currencies={fiats}",
|
| 638 |
+
"coin_data": "/coins/{id}?localization=false",
|
| 639 |
+
"market_chart": "/coins/{id}/market_chart?vs_currency=usd&days=7",
|
| 640 |
+
"global_data": "/global",
|
| 641 |
+
"trending": "/search/trending",
|
| 642 |
+
"categories": "/coins/categories"
|
| 643 |
+
},
|
| 644 |
+
"notes": "Rate limit: 10-50 calls/min (free)"
|
| 645 |
+
},
|
| 646 |
+
{
|
| 647 |
+
"id": "coinmarketcap_primary_1",
|
| 648 |
+
"name": "CoinMarketCap (key #1)",
|
| 649 |
+
"role": "fallback_paid",
|
| 650 |
+
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 651 |
+
"auth": {
|
| 652 |
+
"type": "apiKeyHeader",
|
| 653 |
+
"key": "04cf4b5b-9868-465c-8ba0-9f2e78c92eb1",
|
| 654 |
+
"header_name": "X-CMC_PRO_API_KEY"
|
| 655 |
+
},
|
| 656 |
+
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
| 657 |
+
"endpoints": {
|
| 658 |
+
"latest_quotes": "/cryptocurrency/quotes/latest?symbol={symbol}",
|
| 659 |
+
"listings": "/cryptocurrency/listings/latest?limit=100",
|
| 660 |
+
"market_pairs": "/cryptocurrency/market-pairs/latest?id=1"
|
| 661 |
+
},
|
| 662 |
+
"notes": "Rate limit: 333 calls/day (free)"
|
| 663 |
+
},
|
| 664 |
+
{
|
| 665 |
+
"id": "coinmarketcap_primary_2",
|
| 666 |
+
"name": "CoinMarketCap (key #2)",
|
| 667 |
+
"role": "fallback_paid",
|
| 668 |
+
"base_url": "https://pro-api.coinmarketcap.com/v1",
|
| 669 |
+
"auth": {
|
| 670 |
+
"type": "apiKeyHeader",
|
| 671 |
+
"key": "b54bcf4d-1bca-4e8e-9a24-22ff2c3d462c",
|
| 672 |
+
"header_name": "X-CMC_PRO_API_KEY"
|
| 673 |
+
},
|
| 674 |
+
"docs_url": "https://coinmarketcap.com/api/documentation/v1/",
|
| 675 |
+
"endpoints": {
|
| 676 |
+
"latest_quotes": "/cryptocurrency/quotes/latest?symbol={symbol}",
|
| 677 |
+
"listings": "/cryptocurrency/listings/latest?limit=100",
|
| 678 |
+
"market_pairs": "/cryptocurrency/market-pairs/latest?id=1"
|
| 679 |
+
},
|
| 680 |
+
"notes": "Rate limit: 333 calls/day (free)"
|
| 681 |
+
},
|
| 682 |
+
{
|
| 683 |
+
"id": "cryptocompare",
|
| 684 |
+
"name": "CryptoCompare",
|
| 685 |
+
"role": "fallback_paid",
|
| 686 |
+
"base_url": "https://min-api.cryptocompare.com/data",
|
| 687 |
+
"auth": {
|
| 688 |
+
"type": "apiKeyQuery",
|
| 689 |
+
"key": "e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f",
|
| 690 |
+
"param_name": "api_key"
|
| 691 |
+
},
|
| 692 |
+
"docs_url": "https://min-api.cryptocompare.com/documentation",
|
| 693 |
+
"endpoints": {
|
| 694 |
+
"price_multi": "/pricemulti?fsyms={fsyms}&tsyms={tsyms}&api_key={key}",
|
| 695 |
+
"historical": "/v2/histoday?fsym={fsym}&tsym={tsym}&limit=30&api_key={key}",
|
| 696 |
+
"top_volume": "/top/totalvolfull?limit=10&tsym=USD&api_key={key}"
|
| 697 |
+
},
|
| 698 |
+
"notes": "Free: 100K calls/month"
|
| 699 |
+
},
|
| 700 |
+
{
|
| 701 |
+
"id": "coinpaprika",
|
| 702 |
+
"name": "Coinpaprika",
|
| 703 |
+
"role": "fallback_free",
|
| 704 |
+
"base_url": "https://api.coinpaprika.com/v1",
|
| 705 |
+
"auth": {
|
| 706 |
+
"type": "none"
|
| 707 |
+
},
|
| 708 |
+
"docs_url": "https://api.coinpaprika.com",
|
| 709 |
+
"endpoints": {
|
| 710 |
+
"tickers": "/tickers",
|
| 711 |
+
"coin": "/coins/{id}",
|
| 712 |
+
"historical": "/coins/{id}/ohlcv/historical"
|
| 713 |
+
},
|
| 714 |
+
"notes": "Rate limit: 20K calls/month"
|
| 715 |
+
},
|
| 716 |
+
{
|
| 717 |
+
"id": "coincap",
|
| 718 |
+
"name": "CoinCap",
|
| 719 |
+
"role": "fallback_free",
|
| 720 |
+
"base_url": "https://api.coincap.io/v2",
|
| 721 |
+
"auth": {
|
| 722 |
+
"type": "none"
|
| 723 |
+
},
|
| 724 |
+
"docs_url": "https://docs.coincap.io",
|
| 725 |
+
"endpoints": {
|
| 726 |
+
"assets": "/assets",
|
| 727 |
+
"specific": "/assets/{id}",
|
| 728 |
+
"history": "/assets/{id}/history?interval=d1"
|
| 729 |
+
},
|
| 730 |
+
"notes": "Rate limit: 200 req/min"
|
| 731 |
+
},
|
| 732 |
+
{
|
| 733 |
+
"id": "nomics",
|
| 734 |
+
"name": "Nomics",
|
| 735 |
+
"role": "fallback_paid",
|
| 736 |
+
"base_url": "https://api.nomics.com/v1",
|
| 737 |
+
"auth": {
|
| 738 |
+
"type": "apiKeyQuery",
|
| 739 |
+
"key": null,
|
| 740 |
+
"param_name": "key"
|
| 741 |
+
},
|
| 742 |
+
"docs_url": "https://p.nomics.com/cryptocurrency-bitcoin-api",
|
| 743 |
+
"endpoints": {},
|
| 744 |
+
"notes": "No rate limit on free tier"
|
| 745 |
+
},
|
| 746 |
+
{
|
| 747 |
+
"id": "messari",
|
| 748 |
+
"name": "Messari",
|
| 749 |
+
"role": "fallback_free",
|
| 750 |
+
"base_url": "https://data.messari.io/api/v1",
|
| 751 |
+
"auth": {
|
| 752 |
+
"type": "none"
|
| 753 |
+
},
|
| 754 |
+
"docs_url": "https://messari.io/api/docs",
|
| 755 |
+
"endpoints": {
|
| 756 |
+
"asset_metrics": "/assets/{id}/metrics"
|
| 757 |
+
},
|
| 758 |
+
"notes": "Generous rate limit"
|
| 759 |
+
},
|
| 760 |
+
{
|
| 761 |
+
"id": "bravenewcoin",
|
| 762 |
+
"name": "BraveNewCoin (RapidAPI)",
|
| 763 |
+
"role": "fallback_paid",
|
| 764 |
+
"base_url": "https://bravenewcoin.p.rapidapi.com",
|
| 765 |
+
"auth": {
|
| 766 |
+
"type": "apiKeyHeader",
|
| 767 |
+
"key": null,
|
| 768 |
+
"header_name": "x-rapidapi-key"
|
| 769 |
+
},
|
| 770 |
+
"docs_url": null,
|
| 771 |
+
"endpoints": {
|
| 772 |
+
"ohlcv_latest": "/ohlcv/BTC/latest"
|
| 773 |
+
},
|
| 774 |
+
"notes": "Requires RapidAPI key"
|
| 775 |
+
},
|
| 776 |
+
{
|
| 777 |
+
"id": "kaiko",
|
| 778 |
+
"name": "Kaiko",
|
| 779 |
+
"role": "fallback",
|
| 780 |
+
"base_url": "https://us.market-api.kaiko.io/v2",
|
| 781 |
+
"auth": {
|
| 782 |
+
"type": "apiKeyQueryOptional",
|
| 783 |
+
"key": null,
|
| 784 |
+
"param_name": "api_key"
|
| 785 |
+
},
|
| 786 |
+
"docs_url": null,
|
| 787 |
+
"endpoints": {
|
| 788 |
+
"trades": "/data/trades.v1/exchanges/{exchange}/spot/trades?base_token={base}"e_token={quote}&page_limit=10&api_key={key}"
|
| 789 |
+
},
|
| 790 |
+
"notes": "Fallback"
|
| 791 |
+
},
|
| 792 |
+
{
|
| 793 |
+
"id": "coinapi_io",
|
| 794 |
+
"name": "CoinAPI.io",
|
| 795 |
+
"role": "fallback",
|
| 796 |
+
"base_url": "https://rest.coinapi.io/v1",
|
| 797 |
+
"auth": {
|
| 798 |
+
"type": "apiKeyQueryOptional",
|
| 799 |
+
"key": null,
|
| 800 |
+
"param_name": "apikey"
|
| 801 |
+
},
|
| 802 |
+
"docs_url": null,
|
| 803 |
+
"endpoints": {
|
| 804 |
+
"exchange_rate": "/exchangerate/{base}/{quote}?apikey={key}"
|
| 805 |
+
},
|
| 806 |
+
"notes": "Fallback"
|
| 807 |
+
},
|
| 808 |
+
{
|
| 809 |
+
"id": "coinlore",
|
| 810 |
+
"name": "CoinLore",
|
| 811 |
+
"role": "fallback_free",
|
| 812 |
+
"base_url": "https://api.coinlore.net/api",
|
| 813 |
+
"auth": {
|
| 814 |
+
"type": "none"
|
| 815 |
+
},
|
| 816 |
+
"docs_url": null,
|
| 817 |
+
"endpoints": {},
|
| 818 |
+
"notes": "Free"
|
| 819 |
+
},
|
| 820 |
+
{
|
| 821 |
+
"id": "coinpaprika_market",
|
| 822 |
+
"name": "CoinPaprika",
|
| 823 |
+
"role": "market",
|
| 824 |
+
"base_url": "https://api.coinpaprika.com/v1",
|
| 825 |
+
"auth": {
|
| 826 |
+
"type": "none"
|
| 827 |
+
},
|
| 828 |
+
"docs_url": null,
|
| 829 |
+
"endpoints": {
|
| 830 |
+
"search": "/search?q={q}&c=currencies&limit=1",
|
| 831 |
+
"ticker_by_id": "/tickers/{id}?quotes=USD"
|
| 832 |
+
},
|
| 833 |
+
"notes": "From crypto_resources.ts"
|
| 834 |
+
},
|
| 835 |
+
{
|
| 836 |
+
"id": "coincap_market",
|
| 837 |
+
"name": "CoinCap",
|
| 838 |
+
"role": "market",
|
| 839 |
+
"base_url": "https://api.coincap.io/v2",
|
| 840 |
+
"auth": {
|
| 841 |
+
"type": "none"
|
| 842 |
+
},
|
| 843 |
+
"docs_url": null,
|
| 844 |
+
"endpoints": {
|
| 845 |
+
"assets": "/assets?search={search}&limit=1",
|
| 846 |
+
"asset_by_id": "/assets/{id}"
|
| 847 |
+
},
|
| 848 |
+
"notes": "From crypto_resources.ts"
|
| 849 |
+
},
|
| 850 |
+
{
|
| 851 |
+
"id": "defillama_prices",
|
| 852 |
+
"name": "DefiLlama (Prices)",
|
| 853 |
+
"role": "market",
|
| 854 |
+
"base_url": "https://coins.llama.fi",
|
| 855 |
+
"auth": {
|
| 856 |
+
"type": "none"
|
| 857 |
+
},
|
| 858 |
+
"docs_url": null,
|
| 859 |
+
"endpoints": {
|
| 860 |
+
"prices_current": "/prices/current/{coins}"
|
| 861 |
+
},
|
| 862 |
+
"notes": "Free, from crypto_resources.ts"
|
| 863 |
+
},
|
| 864 |
+
{
|
| 865 |
+
"id": "binance_public",
|
| 866 |
+
"name": "Binance Public",
|
| 867 |
+
"role": "market",
|
| 868 |
+
"base_url": "https://api.binance.com",
|
| 869 |
+
"auth": {
|
| 870 |
+
"type": "none"
|
| 871 |
+
},
|
| 872 |
+
"docs_url": null,
|
| 873 |
+
"endpoints": {
|
| 874 |
+
"klines": "/api/v3/klines?symbol={symbol}&interval={interval}&limit={limit}",
|
| 875 |
+
"ticker": "/api/v3/ticker/price?symbol={symbol}"
|
| 876 |
+
},
|
| 877 |
+
"notes": "Free, from crypto_resources.ts"
|
| 878 |
+
},
|
| 879 |
+
{
|
| 880 |
+
"id": "cryptocompare_market",
|
| 881 |
+
"name": "CryptoCompare",
|
| 882 |
+
"role": "market",
|
| 883 |
+
"base_url": "https://min-api.cryptocompare.com",
|
| 884 |
+
"auth": {
|
| 885 |
+
"type": "apiKeyQuery",
|
| 886 |
+
"key": "e79c8e6d4c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f",
|
| 887 |
+
"param_name": "api_key"
|
| 888 |
+
},
|
| 889 |
+
"docs_url": null,
|
| 890 |
+
"endpoints": {
|
| 891 |
+
"histominute": "/data/v2/histominute?fsym={fsym}&tsym={tsym}&limit={limit}&api_key={key}",
|
| 892 |
+
"histohour": "/data/v2/histohour?fsym={fsym}&tsym={tsym}&limit={limit}&api_key={key}",
|
| 893 |
+
"histoday": "/data/v2/histoday?fsym={fsym}&tsym={tsym}&limit={limit}&api_key={key}"
|
| 894 |
+
},
|
| 895 |
+
"notes": "From crypto_resources.ts"
|
| 896 |
+
},
|
| 897 |
+
{
|
| 898 |
+
"id": "coindesk_price",
|
| 899 |
+
"name": "CoinDesk Price API",
|
| 900 |
+
"role": "fallback_free",
|
| 901 |
+
"base_url": "https://api.coindesk.com/v2",
|
| 902 |
+
"auth": {
|
| 903 |
+
"type": "none"
|
| 904 |
+
},
|
| 905 |
+
"docs_url": "https://www.coindesk.com/coindesk-api",
|
| 906 |
+
"endpoints": {
|
| 907 |
+
"btc_spot": "/prices/BTC/spot?api_key={key}"
|
| 908 |
+
},
|
| 909 |
+
"notes": "From api-config-complete"
|
| 910 |
+
},
|
| 911 |
+
{
|
| 912 |
+
"id": "mobula",
|
| 913 |
+
"name": "Mobula API",
|
| 914 |
+
"role": "fallback_paid",
|
| 915 |
+
"base_url": "https://api.mobula.io/api/1",
|
| 916 |
+
"auth": {
|
| 917 |
+
"type": "apiKeyHeaderOptional",
|
| 918 |
+
"key": null,
|
| 919 |
+
"header_name": "Authorization"
|
| 920 |
+
},
|
| 921 |
+
"docs_url": "https://developer.mobula.fi",
|
| 922 |
+
"endpoints": {},
|
| 923 |
+
"notes": null
|
| 924 |
+
},
|
| 925 |
+
{
|
| 926 |
+
"id": "tokenmetrics",
|
| 927 |
+
"name": "Token Metrics API",
|
| 928 |
+
"role": "fallback_paid",
|
| 929 |
+
"base_url": "https://api.tokenmetrics.com/v2",
|
| 930 |
+
"auth": {
|
| 931 |
+
"type": "apiKeyHeader",
|
| 932 |
+
"key": null,
|
| 933 |
+
"header_name": "Authorization"
|
| 934 |
+
},
|
| 935 |
+
"docs_url": "https://api.tokenmetrics.com/docs",
|
| 936 |
+
"endpoints": {},
|
| 937 |
+
"notes": null
|
| 938 |
+
},
|
| 939 |
+
{
|
| 940 |
+
"id": "freecryptoapi",
|
| 941 |
+
"name": "FreeCryptoAPI",
|
| 942 |
+
"role": "fallback_free",
|
| 943 |
+
"base_url": "https://api.freecryptoapi.com",
|
| 944 |
+
"auth": {
|
| 945 |
+
"type": "none"
|
| 946 |
+
},
|
| 947 |
+
"docs_url": null,
|
| 948 |
+
"endpoints": {},
|
| 949 |
+
"notes": null
|
| 950 |
+
},
|
| 951 |
+
{
|
| 952 |
+
"id": "diadata",
|
| 953 |
+
"name": "DIA Data",
|
| 954 |
+
"role": "fallback_free",
|
| 955 |
+
"base_url": "https://api.diadata.org/v1",
|
| 956 |
+
"auth": {
|
| 957 |
+
"type": "none"
|
| 958 |
+
},
|
| 959 |
+
"docs_url": "https://docs.diadata.org",
|
| 960 |
+
"endpoints": {},
|
| 961 |
+
"notes": null
|
| 962 |
+
},
|
| 963 |
+
{
|
| 964 |
+
"id": "coinstats_public",
|
| 965 |
+
"name": "CoinStats Public API",
|
| 966 |
+
"role": "fallback_free",
|
| 967 |
+
"base_url": "https://api.coinstats.app/public/v1",
|
| 968 |
+
"auth": {
|
| 969 |
+
"type": "none"
|
| 970 |
+
},
|
| 971 |
+
"docs_url": null,
|
| 972 |
+
"endpoints": {},
|
| 973 |
+
"notes": null
|
| 974 |
+
}
|
| 975 |
+
],
|
| 976 |
+
"news_apis": [
|
| 977 |
+
{
|
| 978 |
+
"id": "newsapi_org",
|
| 979 |
+
"name": "NewsAPI.org",
|
| 980 |
+
"role": "general_news",
|
| 981 |
+
"base_url": "https://newsapi.org/v2",
|
| 982 |
+
"auth": {
|
| 983 |
+
"type": "apiKeyQuery",
|
| 984 |
+
"key": "pub_346789abc123def456789ghi012345jkl",
|
| 985 |
+
"param_name": "apiKey"
|
| 986 |
+
},
|
| 987 |
+
"docs_url": "https://newsapi.org/docs",
|
| 988 |
+
"endpoints": {
|
| 989 |
+
"everything": "/everything?q={q}&apiKey={key}"
|
| 990 |
+
},
|
| 991 |
+
"notes": null
|
| 992 |
+
},
|
| 993 |
+
{
|
| 994 |
+
"id": "cryptopanic",
|
| 995 |
+
"name": "CryptoPanic",
|
| 996 |
+
"role": "primary_crypto_news",
|
| 997 |
+
"base_url": "https://cryptopanic.com/api/v1",
|
| 998 |
+
"auth": {
|
| 999 |
+
"type": "apiKeyQueryOptional",
|
| 1000 |
+
"key": null,
|
| 1001 |
+
"param_name": "auth_token"
|
| 1002 |
+
},
|
| 1003 |
+
"docs_url": "https://cryptopanic.com/developers/api/",
|
| 1004 |
+
"endpoints": {
|
| 1005 |
+
"posts": "/posts/?auth_token={key}"
|
| 1006 |
+
},
|
| 1007 |
+
"notes": null
|
| 1008 |
+
},
|
| 1009 |
+
{
|
| 1010 |
+
"id": "cryptocontrol",
|
| 1011 |
+
"name": "CryptoControl",
|
| 1012 |
+
"role": "crypto_news",
|
| 1013 |
+
"base_url": "https://cryptocontrol.io/api/v1/public",
|
| 1014 |
+
"auth": {
|
| 1015 |
+
"type": "apiKeyQueryOptional",
|
| 1016 |
+
"key": null,
|
| 1017 |
+
"param_name": "apiKey"
|
| 1018 |
+
},
|
| 1019 |
+
"docs_url": "https://cryptocontrol.io/api",
|
| 1020 |
+
"endpoints": {
|
| 1021 |
+
"news_local": "/news/local?language=EN&apiKey={key}"
|
| 1022 |
+
},
|
| 1023 |
+
"notes": null
|
| 1024 |
+
},
|
| 1025 |
+
{
|
| 1026 |
+
"id": "coindesk_api",
|
| 1027 |
+
"name": "CoinDesk API",
|
| 1028 |
+
"role": "crypto_news",
|
| 1029 |
+
"base_url": "https://api.coindesk.com/v2",
|
| 1030 |
+
"auth": {
|
| 1031 |
+
"type": "none"
|
| 1032 |
+
},
|
| 1033 |
+
"docs_url": "https://www.coindesk.com/coindesk-api",
|
| 1034 |
+
"endpoints": {},
|
| 1035 |
+
"notes": null
|
| 1036 |
+
},
|
| 1037 |
+
{
|
| 1038 |
+
"id": "cointelegraph_api",
|
| 1039 |
+
"name": "CoinTelegraph API",
|
| 1040 |
+
"role": "crypto_news",
|
| 1041 |
+
"base_url": "https://api.cointelegraph.com/api/v1",
|
| 1042 |
+
"auth": {
|
| 1043 |
+
"type": "none"
|
| 1044 |
+
},
|
| 1045 |
+
"docs_url": null,
|
| 1046 |
+
"endpoints": {
|
| 1047 |
+
"articles": "/articles?lang=en"
|
| 1048 |
+
},
|
| 1049 |
+
"notes": null
|
| 1050 |
+
},
|
| 1051 |
+
{
|
| 1052 |
+
"id": "cryptoslate",
|
| 1053 |
+
"name": "CryptoSlate API",
|
| 1054 |
+
"role": "crypto_news",
|
| 1055 |
+
"base_url": "https://api.cryptoslate.com",
|
| 1056 |
+
"auth": {
|
| 1057 |
+
"type": "none"
|
| 1058 |
+
},
|
| 1059 |
+
"docs_url": null,
|
| 1060 |
+
"endpoints": {
|
| 1061 |
+
"news": "/news"
|
| 1062 |
+
},
|
| 1063 |
+
"notes": null
|
| 1064 |
+
},
|
| 1065 |
+
{
|
| 1066 |
+
"id": "theblock_api",
|
| 1067 |
+
"name": "The Block API",
|
| 1068 |
+
"role": "crypto_news",
|
| 1069 |
+
"base_url": "https://api.theblock.co/v1",
|
| 1070 |
+
"auth": {
|
| 1071 |
+
"type": "none"
|
| 1072 |
+
},
|
| 1073 |
+
"docs_url": null,
|
| 1074 |
+
"endpoints": {
|
| 1075 |
+
"articles": "/articles"
|
| 1076 |
+
},
|
| 1077 |
+
"notes": null
|
| 1078 |
+
},
|
| 1079 |
+
{
|
| 1080 |
+
"id": "coinstats_news",
|
| 1081 |
+
"name": "CoinStats News",
|
| 1082 |
+
"role": "news",
|
| 1083 |
+
"base_url": "https://api.coinstats.app",
|
| 1084 |
+
"auth": {
|
| 1085 |
+
"type": "none"
|
| 1086 |
+
},
|
| 1087 |
+
"docs_url": null,
|
| 1088 |
+
"endpoints": {
|
| 1089 |
+
"feed": "/public/v1/news"
|
| 1090 |
+
},
|
| 1091 |
+
"notes": "Free, from crypto_resources.ts"
|
| 1092 |
+
},
|
| 1093 |
+
{
|
| 1094 |
+
"id": "rss_cointelegraph",
|
| 1095 |
+
"name": "Cointelegraph RSS",
|
| 1096 |
+
"role": "news",
|
| 1097 |
+
"base_url": "https://cointelegraph.com",
|
| 1098 |
+
"auth": {
|
| 1099 |
+
"type": "none"
|
| 1100 |
+
},
|
| 1101 |
+
"docs_url": null,
|
| 1102 |
+
"endpoints": {
|
| 1103 |
+
"feed": "/rss"
|
| 1104 |
+
},
|
| 1105 |
+
"notes": "Free RSS, from crypto_resources.ts"
|
| 1106 |
+
},
|
| 1107 |
+
{
|
| 1108 |
+
"id": "rss_coindesk",
|
| 1109 |
+
"name": "CoinDesk RSS",
|
| 1110 |
+
"role": "news",
|
| 1111 |
+
"base_url": "https://www.coindesk.com",
|
| 1112 |
+
"auth": {
|
| 1113 |
+
"type": "none"
|
| 1114 |
+
},
|
| 1115 |
+
"docs_url": null,
|
| 1116 |
+
"endpoints": {
|
| 1117 |
+
"feed": "/arc/outboundfeeds/rss/?outputType=xml"
|
| 1118 |
+
},
|
| 1119 |
+
"notes": "Free RSS, from crypto_resources.ts"
|
| 1120 |
+
},
|
| 1121 |
+
{
|
| 1122 |
+
"id": "rss_decrypt",
|
| 1123 |
+
"name": "Decrypt RSS",
|
| 1124 |
+
"role": "news",
|
| 1125 |
+
"base_url": "https://decrypt.co",
|
| 1126 |
+
"auth": {
|
| 1127 |
+
"type": "none"
|
| 1128 |
+
},
|
| 1129 |
+
"docs_url": null,
|
| 1130 |
+
"endpoints": {
|
| 1131 |
+
"feed": "/feed"
|
| 1132 |
+
},
|
| 1133 |
+
"notes": "Free RSS, from crypto_resources.ts"
|
| 1134 |
+
},
|
| 1135 |
+
{
|
| 1136 |
+
"id": "coindesk_rss",
|
| 1137 |
+
"name": "CoinDesk RSS",
|
| 1138 |
+
"role": "rss",
|
| 1139 |
+
"base_url": "https://www.coindesk.com/arc/outboundfeeds/rss/",
|
| 1140 |
+
"auth": {
|
| 1141 |
+
"type": "none"
|
| 1142 |
+
},
|
| 1143 |
+
"docs_url": null,
|
| 1144 |
+
"endpoints": {},
|
| 1145 |
+
"notes": null
|
| 1146 |
+
},
|
| 1147 |
+
{
|
| 1148 |
+
"id": "cointelegraph_rss",
|
| 1149 |
+
"name": "CoinTelegraph RSS",
|
| 1150 |
+
"role": "rss",
|
| 1151 |
+
"base_url": "https://cointelegraph.com/rss",
|
| 1152 |
+
"auth": {
|
| 1153 |
+
"type": "none"
|
| 1154 |
+
},
|
| 1155 |
+
"docs_url": null,
|
| 1156 |
+
"endpoints": {},
|
| 1157 |
+
"notes": null
|
| 1158 |
+
},
|
| 1159 |
+
{
|
| 1160 |
+
"id": "bitcoinmagazine_rss",
|
| 1161 |
+
"name": "Bitcoin Magazine RSS",
|
| 1162 |
+
"role": "rss",
|
| 1163 |
+
"base_url": "https://bitcoinmagazine.com/.rss/full/",
|
| 1164 |
+
"auth": {
|
| 1165 |
+
"type": "none"
|
| 1166 |
+
},
|
| 1167 |
+
"docs_url": null,
|
| 1168 |
+
"endpoints": {},
|
| 1169 |
+
"notes": null
|
| 1170 |
+
},
|
| 1171 |
+
{
|
| 1172 |
+
"id": "decrypt_rss",
|
| 1173 |
+
"name": "Decrypt RSS",
|
| 1174 |
+
"role": "rss",
|
| 1175 |
+
"base_url": "https://decrypt.co/feed",
|
| 1176 |
+
"auth": {
|
| 1177 |
+
"type": "none"
|
| 1178 |
+
},
|
| 1179 |
+
"docs_url": null,
|
| 1180 |
+
"endpoints": {},
|
| 1181 |
+
"notes": null
|
| 1182 |
+
}
|
| 1183 |
+
],
|
| 1184 |
+
"sentiment_apis": [
|
| 1185 |
+
{
|
| 1186 |
+
"id": "alternative_me_fng",
|
| 1187 |
+
"name": "Alternative.me Fear & Greed",
|
| 1188 |
+
"role": "primary_sentiment_index",
|
| 1189 |
+
"base_url": "https://api.alternative.me",
|
| 1190 |
+
"auth": {
|
| 1191 |
+
"type": "none"
|
| 1192 |
+
},
|
| 1193 |
+
"docs_url": "https://alternative.me/crypto/fear-and-greed-index/",
|
| 1194 |
+
"endpoints": {
|
| 1195 |
+
"fng": "/fng/?limit=1&format=json"
|
| 1196 |
+
},
|
| 1197 |
+
"notes": null
|
| 1198 |
+
},
|
| 1199 |
+
{
|
| 1200 |
+
"id": "lunarcrush",
|
| 1201 |
+
"name": "LunarCrush",
|
| 1202 |
+
"role": "social_sentiment",
|
| 1203 |
+
"base_url": "https://api.lunarcrush.com/v2",
|
| 1204 |
+
"auth": {
|
| 1205 |
+
"type": "apiKeyQuery",
|
| 1206 |
+
"key": null,
|
| 1207 |
+
"param_name": "key"
|
| 1208 |
+
},
|
| 1209 |
+
"docs_url": "https://lunarcrush.com/developers/api",
|
| 1210 |
+
"endpoints": {
|
| 1211 |
+
"assets": "?data=assets&key={key}&symbol={symbol}"
|
| 1212 |
+
},
|
| 1213 |
+
"notes": null
|
| 1214 |
+
},
|
| 1215 |
+
{
|
| 1216 |
+
"id": "santiment",
|
| 1217 |
+
"name": "Santiment GraphQL",
|
| 1218 |
+
"role": "onchain_social_sentiment",
|
| 1219 |
+
"base_url": "https://api.santiment.net/graphql",
|
| 1220 |
+
"auth": {
|
| 1221 |
+
"type": "apiKeyHeaderOptional",
|
| 1222 |
+
"key": null,
|
| 1223 |
+
"header_name": "Authorization"
|
| 1224 |
+
},
|
| 1225 |
+
"docs_url": "https://api.santiment.net/graphiql",
|
| 1226 |
+
"endpoints": {
|
| 1227 |
+
"graphql": "POST with body: { \"query\": \"{ projects(slug: \\\"{slug}\\\") { sentimentMetrics { socialVolume, socialDominance } } }\" }"
|
| 1228 |
+
},
|
| 1229 |
+
"notes": null
|
| 1230 |
+
},
|
| 1231 |
+
{
|
| 1232 |
+
"id": "thetie",
|
| 1233 |
+
"name": "TheTie.io",
|
| 1234 |
+
"role": "news_twitter_sentiment",
|
| 1235 |
+
"base_url": "https://api.thetie.io",
|
| 1236 |
+
"auth": {
|
| 1237 |
+
"type": "apiKeyHeader",
|
| 1238 |
+
"key": null,
|
| 1239 |
+
"header_name": "Authorization"
|
| 1240 |
+
},
|
| 1241 |
+
"docs_url": "https://docs.thetie.io",
|
| 1242 |
+
"endpoints": {
|
| 1243 |
+
"sentiment": "/data/sentiment?symbol={symbol}&interval=1h&apiKey={key}"
|
| 1244 |
+
},
|
| 1245 |
+
"notes": null
|
| 1246 |
+
},
|
| 1247 |
+
{
|
| 1248 |
+
"id": "cryptoquant",
|
| 1249 |
+
"name": "CryptoQuant",
|
| 1250 |
+
"role": "onchain_sentiment",
|
| 1251 |
+
"base_url": "https://api.cryptoquant.com/v1",
|
| 1252 |
+
"auth": {
|
| 1253 |
+
"type": "apiKeyQuery",
|
| 1254 |
+
"key": null,
|
| 1255 |
+
"param_name": "token"
|
| 1256 |
+
},
|
| 1257 |
+
"docs_url": "https://docs.cryptoquant.com",
|
| 1258 |
+
"endpoints": {
|
| 1259 |
+
"ohlcv_latest": "/ohlcv/latest?symbol={symbol}&token={key}"
|
| 1260 |
+
},
|
| 1261 |
+
"notes": null
|
| 1262 |
+
},
|
| 1263 |
+
{
|
| 1264 |
+
"id": "glassnode_social",
|
| 1265 |
+
"name": "Glassnode Social Metrics",
|
| 1266 |
+
"role": "social_metrics",
|
| 1267 |
+
"base_url": "https://api.glassnode.com/v1/metrics/social",
|
| 1268 |
+
"auth": {
|
| 1269 |
+
"type": "apiKeyQuery",
|
| 1270 |
+
"key": null,
|
| 1271 |
+
"param_name": "api_key"
|
| 1272 |
+
},
|
| 1273 |
+
"docs_url": "https://docs.glassnode.com",
|
| 1274 |
+
"endpoints": {
|
| 1275 |
+
"mention_count": "/mention_count?api_key={key}&a={symbol}"
|
| 1276 |
+
},
|
| 1277 |
+
"notes": null
|
| 1278 |
+
},
|
| 1279 |
+
{
|
| 1280 |
+
"id": "augmento",
|
| 1281 |
+
"name": "Augmento Social Sentiment",
|
| 1282 |
+
"role": "social_ai_sentiment",
|
| 1283 |
+
"base_url": "https://api.augmento.ai/v1",
|
| 1284 |
+
"auth": {
|
| 1285 |
+
"type": "apiKeyQuery",
|
| 1286 |
+
"key": null,
|
| 1287 |
+
"param_name": "api_key"
|
| 1288 |
+
},
|
| 1289 |
+
"docs_url": null,
|
| 1290 |
+
"endpoints": {},
|
| 1291 |
+
"notes": null
|
| 1292 |
+
},
|
| 1293 |
+
{
|
| 1294 |
+
"id": "coingecko_community",
|
| 1295 |
+
"name": "CoinGecko Community Data",
|
| 1296 |
+
"role": "community_stats",
|
| 1297 |
+
"base_url": "https://api.coingecko.com/api/v3",
|
| 1298 |
+
"auth": {
|
| 1299 |
+
"type": "none"
|
| 1300 |
+
},
|
| 1301 |
+
"docs_url": "https://www.coingecko.com/en/api/documentation",
|
| 1302 |
+
"endpoints": {
|
| 1303 |
+
"coin": "/coins/{id}?localization=false&tickers=false&market_data=false&community_data=true"
|
| 1304 |
+
},
|
| 1305 |
+
"notes": null
|
| 1306 |
+
},
|
| 1307 |
+
{
|
| 1308 |
+
"id": "messari_social",
|
| 1309 |
+
"name": "Messari Social Metrics",
|
| 1310 |
+
"role": "social_metrics",
|
| 1311 |
+
"base_url": "https://data.messari.io/api/v1",
|
| 1312 |
+
"auth": {
|
| 1313 |
+
"type": "none"
|
| 1314 |
+
},
|
| 1315 |
+
"docs_url": "https://messari.io/api/docs",
|
| 1316 |
+
"endpoints": {
|
| 1317 |
+
"social_metrics": "/assets/{id}/metrics/social"
|
| 1318 |
+
},
|
| 1319 |
+
"notes": null
|
| 1320 |
+
},
|
| 1321 |
+
{
|
| 1322 |
+
"id": "altme_fng",
|
| 1323 |
+
"name": "Alternative.me F&G",
|
| 1324 |
+
"role": "sentiment",
|
| 1325 |
+
"base_url": "https://api.alternative.me",
|
| 1326 |
+
"auth": {
|
| 1327 |
+
"type": "none"
|
| 1328 |
+
},
|
| 1329 |
+
"docs_url": null,
|
| 1330 |
+
"endpoints": {
|
| 1331 |
+
"latest": "/fng/?limit=1&format=json",
|
| 1332 |
+
"history": "/fng/?limit=30&format=json"
|
| 1333 |
+
},
|
| 1334 |
+
"notes": "From crypto_resources.ts"
|
| 1335 |
+
},
|
| 1336 |
+
{
|
| 1337 |
+
"id": "cfgi_v1",
|
| 1338 |
+
"name": "CFGI API v1",
|
| 1339 |
+
"role": "sentiment",
|
| 1340 |
+
"base_url": "https://api.cfgi.io",
|
| 1341 |
+
"auth": {
|
| 1342 |
+
"type": "none"
|
| 1343 |
+
},
|
| 1344 |
+
"docs_url": null,
|
| 1345 |
+
"endpoints": {
|
| 1346 |
+
"latest": "/v1/fear-greed"
|
| 1347 |
+
},
|
| 1348 |
+
"notes": "From crypto_resources.ts"
|
| 1349 |
+
},
|
| 1350 |
+
{
|
| 1351 |
+
"id": "cfgi_legacy",
|
| 1352 |
+
"name": "CFGI Legacy",
|
| 1353 |
+
"role": "sentiment",
|
| 1354 |
+
"base_url": "https://cfgi.io",
|
| 1355 |
+
"auth": {
|
| 1356 |
+
"type": "none"
|
| 1357 |
+
},
|
| 1358 |
+
"docs_url": null,
|
| 1359 |
+
"endpoints": {
|
| 1360 |
+
"latest": "/api"
|
| 1361 |
+
},
|
| 1362 |
+
"notes": "From crypto_resources.ts"
|
| 1363 |
+
}
|
| 1364 |
+
],
|
| 1365 |
+
"onchain_analytics_apis": [
|
| 1366 |
+
{
|
| 1367 |
+
"id": "glassnode_general",
|
| 1368 |
+
"name": "Glassnode",
|
| 1369 |
+
"role": "onchain_metrics",
|
| 1370 |
+
"base_url": "https://api.glassnode.com/v1",
|
| 1371 |
+
"auth": {
|
| 1372 |
+
"type": "apiKeyQuery",
|
| 1373 |
+
"key": null,
|
| 1374 |
+
"param_name": "api_key"
|
| 1375 |
+
},
|
| 1376 |
+
"docs_url": "https://docs.glassnode.com",
|
| 1377 |
+
"endpoints": {
|
| 1378 |
+
"sopr_ratio": "/metrics/indicators/sopr_ratio?api_key={key}"
|
| 1379 |
+
},
|
| 1380 |
+
"notes": null
|
| 1381 |
+
},
|
| 1382 |
+
{
|
| 1383 |
+
"id": "intotheblock",
|
| 1384 |
+
"name": "IntoTheBlock",
|
| 1385 |
+
"role": "holders_analytics",
|
| 1386 |
+
"base_url": "https://api.intotheblock.com/v1",
|
| 1387 |
+
"auth": {
|
| 1388 |
+
"type": "apiKeyQuery",
|
| 1389 |
+
"key": null,
|
| 1390 |
+
"param_name": "key"
|
| 1391 |
+
},
|
| 1392 |
+
"docs_url": null,
|
| 1393 |
+
"endpoints": {
|
| 1394 |
+
"holders_breakdown": "/insights/{symbol}/holders_breakdown?key={key}"
|
| 1395 |
+
},
|
| 1396 |
+
"notes": null
|
| 1397 |
+
},
|
| 1398 |
+
{
|
| 1399 |
+
"id": "nansen",
|
| 1400 |
+
"name": "Nansen",
|
| 1401 |
+
"role": "smart_money",
|
| 1402 |
+
"base_url": "https://api.nansen.ai/v1",
|
| 1403 |
+
"auth": {
|
| 1404 |
+
"type": "apiKeyQuery",
|
| 1405 |
+
"key": null,
|
| 1406 |
+
"param_name": "api_key"
|
| 1407 |
+
},
|
| 1408 |
+
"docs_url": null,
|
| 1409 |
+
"endpoints": {
|
| 1410 |
+
"balances": "/balances?chain=ethereum&address={address}&api_key={key}"
|
| 1411 |
+
},
|
| 1412 |
+
"notes": null
|
| 1413 |
+
},
|
| 1414 |
+
{
|
| 1415 |
+
"id": "thegraph_subgraphs",
|
| 1416 |
+
"name": "The Graph",
|
| 1417 |
+
"role": "subgraphs",
|
| 1418 |
+
"base_url": "https://api.thegraph.com/subgraphs/name/uniswap/uniswap-v3",
|
| 1419 |
+
"auth": {
|
| 1420 |
+
"type": "none"
|
| 1421 |
+
},
|
| 1422 |
+
"docs_url": null,
|
| 1423 |
+
"endpoints": {
|
| 1424 |
+
"graphql": "POST with query"
|
| 1425 |
+
},
|
| 1426 |
+
"notes": null
|
| 1427 |
+
},
|
| 1428 |
+
{
|
| 1429 |
+
"id": "thegraph_subgraphs",
|
| 1430 |
+
"name": "The Graph Subgraphs",
|
| 1431 |
+
"role": "primary_onchain_indexer",
|
| 1432 |
+
"base_url": "https://api.thegraph.com/subgraphs/name/{org}/{subgraph}",
|
| 1433 |
+
"auth": {
|
| 1434 |
+
"type": "none"
|
| 1435 |
+
},
|
| 1436 |
+
"docs_url": "https://thegraph.com/docs/",
|
| 1437 |
+
"endpoints": {},
|
| 1438 |
+
"notes": null
|
| 1439 |
+
},
|
| 1440 |
+
{
|
| 1441 |
+
"id": "dune",
|
| 1442 |
+
"name": "Dune Analytics",
|
| 1443 |
+
"role": "sql_onchain_analytics",
|
| 1444 |
+
"base_url": "https://api.dune.com/api/v1",
|
| 1445 |
+
"auth": {
|
| 1446 |
+
"type": "apiKeyHeader",
|
| 1447 |
+
"key": null,
|
| 1448 |
+
"header_name": "X-DUNE-API-KEY"
|
| 1449 |
+
},
|
| 1450 |
+
"docs_url": "https://docs.dune.com/api-reference/",
|
| 1451 |
+
"endpoints": {},
|
| 1452 |
+
"notes": null
|
| 1453 |
+
},
|
| 1454 |
+
{
|
| 1455 |
+
"id": "covalent",
|
| 1456 |
+
"name": "Covalent",
|
| 1457 |
+
"role": "multichain_analytics",
|
| 1458 |
+
"base_url": "https://api.covalenthq.com/v1",
|
| 1459 |
+
"auth": {
|
| 1460 |
+
"type": "apiKeyQuery",
|
| 1461 |
+
"key": null,
|
| 1462 |
+
"param_name": "key"
|
| 1463 |
+
},
|
| 1464 |
+
"docs_url": "https://www.covalenthq.com/docs/api/",
|
| 1465 |
+
"endpoints": {
|
| 1466 |
+
"balances_v2": "/1/address/{address}/balances_v2/?key={key}"
|
| 1467 |
+
},
|
| 1468 |
+
"notes": null
|
| 1469 |
+
},
|
| 1470 |
+
{
|
| 1471 |
+
"id": "moralis",
|
| 1472 |
+
"name": "Moralis",
|
| 1473 |
+
"role": "evm_data",
|
| 1474 |
+
"base_url": "https://deep-index.moralis.io/api/v2",
|
| 1475 |
+
"auth": {
|
| 1476 |
+
"type": "apiKeyHeader",
|
| 1477 |
+
"key": null,
|
| 1478 |
+
"header_name": "X-API-Key"
|
| 1479 |
+
},
|
| 1480 |
+
"docs_url": "https://docs.moralis.io",
|
| 1481 |
+
"endpoints": {},
|
| 1482 |
+
"notes": null
|
| 1483 |
+
},
|
| 1484 |
+
{
|
| 1485 |
+
"id": "alchemy_nft_api",
|
| 1486 |
+
"name": "Alchemy NFT API",
|
| 1487 |
+
"role": "nft_metadata",
|
| 1488 |
+
"base_url": "https://eth-mainnet.g.alchemy.com/nft/v2/{API_KEY}",
|
| 1489 |
+
"auth": {
|
| 1490 |
+
"type": "apiKeyPath",
|
| 1491 |
+
"key": null,
|
| 1492 |
+
"param_name": "API_KEY"
|
| 1493 |
+
},
|
| 1494 |
+
"docs_url": null,
|
| 1495 |
+
"endpoints": {},
|
| 1496 |
+
"notes": null
|
| 1497 |
+
},
|
| 1498 |
+
{
|
| 1499 |
+
"id": "quicknode_functions",
|
| 1500 |
+
"name": "QuickNode Functions",
|
| 1501 |
+
"role": "custom_onchain_functions",
|
| 1502 |
+
"base_url": "https://{YOUR_QUICKNODE_ENDPOINT}",
|
| 1503 |
+
"auth": {
|
| 1504 |
+
"type": "apiKeyPathOptional",
|
| 1505 |
+
"key": null
|
| 1506 |
+
},
|
| 1507 |
+
"docs_url": null,
|
| 1508 |
+
"endpoints": {},
|
| 1509 |
+
"notes": null
|
| 1510 |
+
},
|
| 1511 |
+
{
|
| 1512 |
+
"id": "transpose",
|
| 1513 |
+
"name": "Transpose",
|
| 1514 |
+
"role": "sql_like_onchain",
|
| 1515 |
+
"base_url": "https://api.transpose.io",
|
| 1516 |
+
"auth": {
|
| 1517 |
+
"type": "apiKeyHeader",
|
| 1518 |
+
"key": null,
|
| 1519 |
+
"header_name": "X-API-Key"
|
| 1520 |
+
},
|
| 1521 |
+
"docs_url": null,
|
| 1522 |
+
"endpoints": {},
|
| 1523 |
+
"notes": null
|
| 1524 |
+
},
|
| 1525 |
+
{
|
| 1526 |
+
"id": "footprint_analytics",
|
| 1527 |
+
"name": "Footprint Analytics",
|
| 1528 |
+
"role": "no_code_analytics",
|
| 1529 |
+
"base_url": "https://api.footprint.network",
|
| 1530 |
+
"auth": {
|
| 1531 |
+
"type": "apiKeyHeaderOptional",
|
| 1532 |
+
"key": null,
|
| 1533 |
+
"header_name": "API-KEY"
|
| 1534 |
+
},
|
| 1535 |
+
"docs_url": null,
|
| 1536 |
+
"endpoints": {},
|
| 1537 |
+
"notes": null
|
| 1538 |
+
},
|
| 1539 |
+
{
|
| 1540 |
+
"id": "nansen_query",
|
| 1541 |
+
"name": "Nansen Query",
|
| 1542 |
+
"role": "institutional_onchain",
|
| 1543 |
+
"base_url": "https://api.nansen.ai/v1",
|
| 1544 |
+
"auth": {
|
| 1545 |
+
"type": "apiKeyHeader",
|
| 1546 |
+
"key": null,
|
| 1547 |
+
"header_name": "X-API-KEY"
|
| 1548 |
+
},
|
| 1549 |
+
"docs_url": "https://docs.nansen.ai",
|
| 1550 |
+
"endpoints": {},
|
| 1551 |
+
"notes": null
|
| 1552 |
+
}
|
| 1553 |
+
],
|
| 1554 |
+
"whale_tracking_apis": [
|
| 1555 |
+
{
|
| 1556 |
+
"id": "whale_alert",
|
| 1557 |
+
"name": "Whale Alert",
|
| 1558 |
+
"role": "primary_whale_tracking",
|
| 1559 |
+
"base_url": "https://api.whale-alert.io/v1",
|
| 1560 |
+
"auth": {
|
| 1561 |
+
"type": "apiKeyQuery",
|
| 1562 |
+
"key": null,
|
| 1563 |
+
"param_name": "api_key"
|
| 1564 |
+
},
|
| 1565 |
+
"docs_url": "https://docs.whale-alert.io",
|
| 1566 |
+
"endpoints": {
|
| 1567 |
+
"transactions": "/transactions?api_key={key}&min_value=1000000&start={ts}&end={ts}"
|
| 1568 |
+
},
|
| 1569 |
+
"notes": null
|
| 1570 |
+
},
|
| 1571 |
+
{
|
| 1572 |
+
"id": "arkham",
|
| 1573 |
+
"name": "Arkham Intelligence",
|
| 1574 |
+
"role": "fallback",
|
| 1575 |
+
"base_url": "https://api.arkham.com/v1",
|
| 1576 |
+
"auth": {
|
| 1577 |
+
"type": "apiKeyQuery",
|
| 1578 |
+
"key": null,
|
| 1579 |
+
"param_name": "api_key"
|
| 1580 |
+
},
|
| 1581 |
+
"docs_url": null,
|
| 1582 |
+
"endpoints": {
|
| 1583 |
+
"transfers": "/address/{address}/transfers?api_key={key}"
|
| 1584 |
+
},
|
| 1585 |
+
"notes": null
|
| 1586 |
+
},
|
| 1587 |
+
{
|
| 1588 |
+
"id": "clankapp",
|
| 1589 |
+
"name": "ClankApp",
|
| 1590 |
+
"role": "fallback_free_whale_tracking",
|
| 1591 |
+
"base_url": "https://clankapp.com/api",
|
| 1592 |
+
"auth": {
|
| 1593 |
+
"type": "none"
|
| 1594 |
+
},
|
| 1595 |
+
"docs_url": "https://clankapp.com/api/",
|
| 1596 |
+
"endpoints": {},
|
| 1597 |
+
"notes": null
|
| 1598 |
+
},
|
| 1599 |
+
{
|
| 1600 |
+
"id": "bitquery_whales",
|
| 1601 |
+
"name": "BitQuery Whale Tracking",
|
| 1602 |
+
"role": "graphql_whale_tracking",
|
| 1603 |
+
"base_url": "https://graphql.bitquery.io",
|
| 1604 |
+
"auth": {
|
| 1605 |
+
"type": "apiKeyHeader",
|
| 1606 |
+
"key": null,
|
| 1607 |
+
"header_name": "X-API-KEY"
|
| 1608 |
+
},
|
| 1609 |
+
"docs_url": "https://docs.bitquery.io",
|
| 1610 |
+
"endpoints": {},
|
| 1611 |
+
"notes": null
|
| 1612 |
+
},
|
| 1613 |
+
{
|
| 1614 |
+
"id": "nansen_whales",
|
| 1615 |
+
"name": "Nansen Smart Money / Whales",
|
| 1616 |
+
"role": "premium_whale_tracking",
|
| 1617 |
+
"base_url": "https://api.nansen.ai/v1",
|
| 1618 |
+
"auth": {
|
| 1619 |
+
"type": "apiKeyHeader",
|
| 1620 |
+
"key": null,
|
| 1621 |
+
"header_name": "X-API-KEY"
|
| 1622 |
+
},
|
| 1623 |
+
"docs_url": "https://docs.nansen.ai",
|
| 1624 |
+
"endpoints": {},
|
| 1625 |
+
"notes": null
|
| 1626 |
+
},
|
| 1627 |
+
{
|
| 1628 |
+
"id": "dexcheck",
|
| 1629 |
+
"name": "DexCheck Whale Tracker",
|
| 1630 |
+
"role": "free_wallet_tracking",
|
| 1631 |
+
"base_url": null,
|
| 1632 |
+
"auth": {
|
| 1633 |
+
"type": "none"
|
| 1634 |
+
},
|
| 1635 |
+
"docs_url": null,
|
| 1636 |
+
"endpoints": {},
|
| 1637 |
+
"notes": null
|
| 1638 |
+
},
|
| 1639 |
+
{
|
| 1640 |
+
"id": "debank",
|
| 1641 |
+
"name": "DeBank",
|
| 1642 |
+
"role": "portfolio_whale_watch",
|
| 1643 |
+
"base_url": "https://api.debank.com",
|
| 1644 |
+
"auth": {
|
| 1645 |
+
"type": "none"
|
| 1646 |
+
},
|
| 1647 |
+
"docs_url": null,
|
| 1648 |
+
"endpoints": {},
|
| 1649 |
+
"notes": null
|
| 1650 |
+
},
|
| 1651 |
+
{
|
| 1652 |
+
"id": "zerion",
|
| 1653 |
+
"name": "Zerion API",
|
| 1654 |
+
"role": "portfolio_tracking",
|
| 1655 |
+
"base_url": "https://api.zerion.io",
|
| 1656 |
+
"auth": {
|
| 1657 |
+
"type": "apiKeyHeaderOptional",
|
| 1658 |
+
"key": null,
|
| 1659 |
+
"header_name": "Authorization"
|
| 1660 |
+
},
|
| 1661 |
+
"docs_url": null,
|
| 1662 |
+
"endpoints": {},
|
| 1663 |
+
"notes": null
|
| 1664 |
+
},
|
| 1665 |
+
{
|
| 1666 |
+
"id": "whalemap",
|
| 1667 |
+
"name": "Whalemap",
|
| 1668 |
+
"role": "btc_whale_analytics",
|
| 1669 |
+
"base_url": "https://whalemap.io",
|
| 1670 |
+
"auth": {
|
| 1671 |
+
"type": "none"
|
| 1672 |
+
},
|
| 1673 |
+
"docs_url": null,
|
| 1674 |
+
"endpoints": {},
|
| 1675 |
+
"notes": null
|
| 1676 |
+
}
|
| 1677 |
+
],
|
| 1678 |
+
"community_sentiment_apis": [
|
| 1679 |
+
{
|
| 1680 |
+
"id": "reddit_cryptocurrency_new",
|
| 1681 |
+
"name": "Reddit /r/CryptoCurrency (new)",
|
| 1682 |
+
"role": "community_sentiment",
|
| 1683 |
+
"base_url": "https://www.reddit.com/r/CryptoCurrency",
|
| 1684 |
+
"auth": {
|
| 1685 |
+
"type": "none"
|
| 1686 |
+
},
|
| 1687 |
+
"docs_url": null,
|
| 1688 |
+
"endpoints": {
|
| 1689 |
+
"new_json": "/new.json?limit=10"
|
| 1690 |
+
},
|
| 1691 |
+
"notes": null
|
| 1692 |
+
}
|
| 1693 |
+
],
|
| 1694 |
+
"hf_resources": [
|
| 1695 |
+
{
|
| 1696 |
+
"id": "hf_model_elkulako_cryptobert",
|
| 1697 |
+
"type": "model",
|
| 1698 |
+
"name": "ElKulako/CryptoBERT",
|
| 1699 |
+
"base_url": "https://api-inference.huggingface.co/models/ElKulako/cryptobert",
|
| 1700 |
+
"auth": {
|
| 1701 |
+
"type": "apiKeyHeaderOptional",
|
| 1702 |
+
"key": "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",
|
| 1703 |
+
"header_name": "Authorization"
|
| 1704 |
+
},
|
| 1705 |
+
"docs_url": "https://huggingface.co/ElKulako/cryptobert",
|
| 1706 |
+
"endpoints": {
|
| 1707 |
+
"classify": "POST with body: { \"inputs\": [\"text\"] }"
|
| 1708 |
+
},
|
| 1709 |
+
"notes": "For sentiment analysis"
|
| 1710 |
+
},
|
| 1711 |
+
{
|
| 1712 |
+
"id": "hf_model_kk08_cryptobert",
|
| 1713 |
+
"type": "model",
|
| 1714 |
+
"name": "kk08/CryptoBERT",
|
| 1715 |
+
"base_url": "https://api-inference.huggingface.co/models/kk08/CryptoBERT",
|
| 1716 |
+
"auth": {
|
| 1717 |
+
"type": "apiKeyHeaderOptional",
|
| 1718 |
+
"key": "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",
|
| 1719 |
+
"header_name": "Authorization"
|
| 1720 |
+
},
|
| 1721 |
+
"docs_url": "https://huggingface.co/kk08/CryptoBERT",
|
| 1722 |
+
"endpoints": {
|
| 1723 |
+
"classify": "POST with body: { \"inputs\": [\"text\"] }"
|
| 1724 |
+
},
|
| 1725 |
+
"notes": "For sentiment analysis"
|
| 1726 |
+
},
|
| 1727 |
+
{
|
| 1728 |
+
"id": "hf_ds_linxy_cryptocoin",
|
| 1729 |
+
"type": "dataset",
|
| 1730 |
+
"name": "linxy/CryptoCoin",
|
| 1731 |
+
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin/resolve/main",
|
| 1732 |
+
"auth": {
|
| 1733 |
+
"type": "none"
|
| 1734 |
+
},
|
| 1735 |
+
"docs_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
| 1736 |
+
"endpoints": {
|
| 1737 |
+
"csv": "/{symbol}_{timeframe}.csv"
|
| 1738 |
+
},
|
| 1739 |
+
"notes": "26 symbols x 7 timeframes = 182 CSVs"
|
| 1740 |
+
},
|
| 1741 |
+
{
|
| 1742 |
+
"id": "hf_ds_wf_btc_usdt",
|
| 1743 |
+
"type": "dataset",
|
| 1744 |
+
"name": "WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1745 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT/resolve/main",
|
| 1746 |
+
"auth": {
|
| 1747 |
+
"type": "none"
|
| 1748 |
+
},
|
| 1749 |
+
"docs_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1750 |
+
"endpoints": {
|
| 1751 |
+
"data": "/data.csv",
|
| 1752 |
+
"1h": "/BTCUSDT_1h.csv"
|
| 1753 |
+
},
|
| 1754 |
+
"notes": null
|
| 1755 |
+
},
|
| 1756 |
+
{
|
| 1757 |
+
"id": "hf_ds_wf_eth_usdt",
|
| 1758 |
+
"type": "dataset",
|
| 1759 |
+
"name": "WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1760 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT/resolve/main",
|
| 1761 |
+
"auth": {
|
| 1762 |
+
"type": "none"
|
| 1763 |
+
},
|
| 1764 |
+
"docs_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1765 |
+
"endpoints": {
|
| 1766 |
+
"data": "/data.csv",
|
| 1767 |
+
"1h": "/ETHUSDT_1h.csv"
|
| 1768 |
+
},
|
| 1769 |
+
"notes": null
|
| 1770 |
+
},
|
| 1771 |
+
{
|
| 1772 |
+
"id": "hf_ds_wf_sol_usdt",
|
| 1773 |
+
"type": "dataset",
|
| 1774 |
+
"name": "WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1775 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT/resolve/main",
|
| 1776 |
+
"auth": {
|
| 1777 |
+
"type": "none"
|
| 1778 |
+
},
|
| 1779 |
+
"docs_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1780 |
+
"endpoints": {},
|
| 1781 |
+
"notes": null
|
| 1782 |
+
},
|
| 1783 |
+
{
|
| 1784 |
+
"id": "hf_ds_wf_xrp_usdt",
|
| 1785 |
+
"type": "dataset",
|
| 1786 |
+
"name": "WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1787 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT/resolve/main",
|
| 1788 |
+
"auth": {
|
| 1789 |
+
"type": "none"
|
| 1790 |
+
},
|
| 1791 |
+
"docs_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1792 |
+
"endpoints": {},
|
| 1793 |
+
"notes": null
|
| 1794 |
+
}
|
| 1795 |
+
],
|
| 1796 |
+
"free_http_endpoints": [
|
| 1797 |
+
{
|
| 1798 |
+
"id": "cg_simple_price",
|
| 1799 |
+
"category": "market",
|
| 1800 |
+
"name": "CoinGecko Simple Price",
|
| 1801 |
+
"base_url": "https://api.coingecko.com/api/v3/simple/price",
|
| 1802 |
+
"auth": {
|
| 1803 |
+
"type": "none"
|
| 1804 |
+
},
|
| 1805 |
+
"docs_url": null,
|
| 1806 |
+
"notes": "no-auth; example: ?ids=bitcoin&vs_currencies=usd"
|
| 1807 |
+
},
|
| 1808 |
+
{
|
| 1809 |
+
"id": "binance_klines",
|
| 1810 |
+
"category": "market",
|
| 1811 |
+
"name": "Binance Klines",
|
| 1812 |
+
"base_url": "https://api.binance.com/api/v3/klines",
|
| 1813 |
+
"auth": {
|
| 1814 |
+
"type": "none"
|
| 1815 |
+
},
|
| 1816 |
+
"docs_url": null,
|
| 1817 |
+
"notes": "no-auth; example: ?symbol=BTCUSDT&interval=1h&limit=100"
|
| 1818 |
+
},
|
| 1819 |
+
{
|
| 1820 |
+
"id": "alt_fng",
|
| 1821 |
+
"category": "indices",
|
| 1822 |
+
"name": "Alternative.me Fear & Greed",
|
| 1823 |
+
"base_url": "https://api.alternative.me/fng/",
|
| 1824 |
+
"auth": {
|
| 1825 |
+
"type": "none"
|
| 1826 |
+
},
|
| 1827 |
+
"docs_url": null,
|
| 1828 |
+
"notes": "no-auth; example: ?limit=1"
|
| 1829 |
+
},
|
| 1830 |
+
{
|
| 1831 |
+
"id": "reddit_top",
|
| 1832 |
+
"category": "social",
|
| 1833 |
+
"name": "Reddit r/cryptocurrency Top",
|
| 1834 |
+
"base_url": "https://www.reddit.com/r/cryptocurrency/top.json",
|
| 1835 |
+
"auth": {
|
| 1836 |
+
"type": "none"
|
| 1837 |
+
},
|
| 1838 |
+
"docs_url": null,
|
| 1839 |
+
"notes": "server-side recommended"
|
| 1840 |
+
},
|
| 1841 |
+
{
|
| 1842 |
+
"id": "coindesk_rss",
|
| 1843 |
+
"category": "news",
|
| 1844 |
+
"name": "CoinDesk RSS",
|
| 1845 |
+
"base_url": "https://feeds.feedburner.com/CoinDesk",
|
| 1846 |
+
"auth": {
|
| 1847 |
+
"type": "none"
|
| 1848 |
+
},
|
| 1849 |
+
"docs_url": null,
|
| 1850 |
+
"notes": null
|
| 1851 |
+
},
|
| 1852 |
+
{
|
| 1853 |
+
"id": "cointelegraph_rss",
|
| 1854 |
+
"category": "news",
|
| 1855 |
+
"name": "CoinTelegraph RSS",
|
| 1856 |
+
"base_url": "https://cointelegraph.com/rss",
|
| 1857 |
+
"auth": {
|
| 1858 |
+
"type": "none"
|
| 1859 |
+
},
|
| 1860 |
+
"docs_url": null,
|
| 1861 |
+
"notes": null
|
| 1862 |
+
},
|
| 1863 |
+
{
|
| 1864 |
+
"id": "hf_model_elkulako_cryptobert",
|
| 1865 |
+
"category": "hf-model",
|
| 1866 |
+
"name": "HF Model: ElKulako/CryptoBERT",
|
| 1867 |
+
"base_url": "https://huggingface.co/ElKulako/cryptobert",
|
| 1868 |
+
"auth": {
|
| 1869 |
+
"type": "none"
|
| 1870 |
+
},
|
| 1871 |
+
"docs_url": null,
|
| 1872 |
+
"notes": null
|
| 1873 |
+
},
|
| 1874 |
+
{
|
| 1875 |
+
"id": "hf_model_kk08_cryptobert",
|
| 1876 |
+
"category": "hf-model",
|
| 1877 |
+
"name": "HF Model: kk08/CryptoBERT",
|
| 1878 |
+
"base_url": "https://huggingface.co/kk08/CryptoBERT",
|
| 1879 |
+
"auth": {
|
| 1880 |
+
"type": "none"
|
| 1881 |
+
},
|
| 1882 |
+
"docs_url": null,
|
| 1883 |
+
"notes": null
|
| 1884 |
+
},
|
| 1885 |
+
{
|
| 1886 |
+
"id": "hf_ds_linxy_crypto",
|
| 1887 |
+
"category": "hf-dataset",
|
| 1888 |
+
"name": "HF Dataset: linxy/CryptoCoin",
|
| 1889 |
+
"base_url": "https://huggingface.co/datasets/linxy/CryptoCoin",
|
| 1890 |
+
"auth": {
|
| 1891 |
+
"type": "none"
|
| 1892 |
+
},
|
| 1893 |
+
"docs_url": null,
|
| 1894 |
+
"notes": null
|
| 1895 |
+
},
|
| 1896 |
+
{
|
| 1897 |
+
"id": "hf_ds_wf_btc",
|
| 1898 |
+
"category": "hf-dataset",
|
| 1899 |
+
"name": "HF Dataset: WinkingFace BTC/USDT",
|
| 1900 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Bitcoin-BTC-USDT",
|
| 1901 |
+
"auth": {
|
| 1902 |
+
"type": "none"
|
| 1903 |
+
},
|
| 1904 |
+
"docs_url": null,
|
| 1905 |
+
"notes": null
|
| 1906 |
+
},
|
| 1907 |
+
{
|
| 1908 |
+
"id": "hf_ds_wf_eth",
|
| 1909 |
+
"category": "hf-dataset",
|
| 1910 |
+
"name": "WinkingFace ETH/USDT",
|
| 1911 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ethereum-ETH-USDT",
|
| 1912 |
+
"auth": {
|
| 1913 |
+
"type": "none"
|
| 1914 |
+
},
|
| 1915 |
+
"docs_url": null,
|
| 1916 |
+
"notes": null
|
| 1917 |
+
},
|
| 1918 |
+
{
|
| 1919 |
+
"id": "hf_ds_wf_sol",
|
| 1920 |
+
"category": "hf-dataset",
|
| 1921 |
+
"name": "WinkingFace SOL/USDT",
|
| 1922 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Solana-SOL-USDT",
|
| 1923 |
+
"auth": {
|
| 1924 |
+
"type": "none"
|
| 1925 |
+
},
|
| 1926 |
+
"docs_url": null,
|
| 1927 |
+
"notes": null
|
| 1928 |
+
},
|
| 1929 |
+
{
|
| 1930 |
+
"id": "hf_ds_wf_xrp",
|
| 1931 |
+
"category": "hf-dataset",
|
| 1932 |
+
"name": "WinkingFace XRP/USDT",
|
| 1933 |
+
"base_url": "https://huggingface.co/datasets/WinkingFace/CryptoLM-Ripple-XRP-USDT",
|
| 1934 |
+
"auth": {
|
| 1935 |
+
"type": "none"
|
| 1936 |
+
},
|
| 1937 |
+
"docs_url": null,
|
| 1938 |
+
"notes": null
|
| 1939 |
+
}
|
| 1940 |
+
],
|
| 1941 |
+
"local_backend_routes": [
|
| 1942 |
+
{
|
| 1943 |
+
"id": "local_hf_ohlcv",
|
| 1944 |
+
"category": "local",
|
| 1945 |
+
"name": "Local: HF OHLCV",
|
| 1946 |
+
"base_url": "{API_BASE}/hf/ohlcv",
|
| 1947 |
+
"auth": {
|
| 1948 |
+
"type": "none"
|
| 1949 |
+
},
|
| 1950 |
+
"docs_url": null,
|
| 1951 |
+
"notes": "Replace {API_BASE} with your local server base URL"
|
| 1952 |
+
},
|
| 1953 |
+
{
|
| 1954 |
+
"id": "local_hf_sentiment",
|
| 1955 |
+
"category": "local",
|
| 1956 |
+
"name": "Local: HF Sentiment",
|
| 1957 |
+
"base_url": "{API_BASE}/hf/sentiment",
|
| 1958 |
+
"auth": {
|
| 1959 |
+
"type": "none"
|
| 1960 |
+
},
|
| 1961 |
+
"docs_url": null,
|
| 1962 |
+
"notes": "POST method; Replace {API_BASE} with your local server base URL"
|
| 1963 |
+
},
|
| 1964 |
+
{
|
| 1965 |
+
"id": "local_fear_greed",
|
| 1966 |
+
"category": "local",
|
| 1967 |
+
"name": "Local: Fear & Greed",
|
| 1968 |
+
"base_url": "{API_BASE}/sentiment/fear-greed",
|
| 1969 |
+
"auth": {
|
| 1970 |
+
"type": "none"
|
| 1971 |
+
},
|
| 1972 |
+
"docs_url": null,
|
| 1973 |
+
"notes": "Replace {API_BASE} with your local server base URL"
|
| 1974 |
+
},
|
| 1975 |
+
{
|
| 1976 |
+
"id": "local_social_aggregate",
|
| 1977 |
+
"category": "local",
|
| 1978 |
+
"name": "Local: Social Aggregate",
|
| 1979 |
+
"base_url": "{API_BASE}/social/aggregate",
|
| 1980 |
+
"auth": {
|
| 1981 |
+
"type": "none"
|
| 1982 |
+
},
|
| 1983 |
+
"docs_url": null,
|
| 1984 |
+
"notes": "Replace {API_BASE} with your local server base URL"
|
| 1985 |
+
},
|
| 1986 |
+
{
|
| 1987 |
+
"id": "local_market_quotes",
|
| 1988 |
+
"category": "local",
|
| 1989 |
+
"name": "Local: Market Quotes",
|
| 1990 |
+
"base_url": "{API_BASE}/market/quotes",
|
| 1991 |
+
"auth": {
|
| 1992 |
+
"type": "none"
|
| 1993 |
+
},
|
| 1994 |
+
"docs_url": null,
|
| 1995 |
+
"notes": "Replace {API_BASE} with your local server base URL"
|
| 1996 |
+
},
|
| 1997 |
+
{
|
| 1998 |
+
"id": "local_binance_klines",
|
| 1999 |
+
"category": "local",
|
| 2000 |
+
"name": "Local: Binance Klines",
|
| 2001 |
+
"base_url": "{API_BASE}/market/klines",
|
| 2002 |
+
"auth": {
|
| 2003 |
+
"type": "none"
|
| 2004 |
+
},
|
| 2005 |
+
"docs_url": null,
|
| 2006 |
+
"notes": "Replace {API_BASE} with your local server base URL"
|
| 2007 |
+
}
|
| 2008 |
+
],
|
| 2009 |
+
"cors_proxies": [
|
| 2010 |
+
{
|
| 2011 |
+
"id": "allorigins",
|
| 2012 |
+
"name": "AllOrigins",
|
| 2013 |
+
"base_url": "https://api.allorigins.win/get?url={TARGET_URL}",
|
| 2014 |
+
"auth": {
|
| 2015 |
+
"type": "none"
|
| 2016 |
+
},
|
| 2017 |
+
"docs_url": null,
|
| 2018 |
+
"notes": "No limit, JSON/JSONP, raw content"
|
| 2019 |
+
},
|
| 2020 |
+
{
|
| 2021 |
+
"id": "cors_sh",
|
| 2022 |
+
"name": "CORS.SH",
|
| 2023 |
+
"base_url": "https://proxy.cors.sh/{TARGET_URL}",
|
| 2024 |
+
"auth": {
|
| 2025 |
+
"type": "none"
|
| 2026 |
+
},
|
| 2027 |
+
"docs_url": null,
|
| 2028 |
+
"notes": "No rate limit, requires Origin or x-requested-with header"
|
| 2029 |
+
},
|
| 2030 |
+
{
|
| 2031 |
+
"id": "corsfix",
|
| 2032 |
+
"name": "Corsfix",
|
| 2033 |
+
"base_url": "https://proxy.corsfix.com/?url={TARGET_URL}",
|
| 2034 |
+
"auth": {
|
| 2035 |
+
"type": "none"
|
| 2036 |
+
},
|
| 2037 |
+
"docs_url": null,
|
| 2038 |
+
"notes": "60 req/min free, header override, cached"
|
| 2039 |
+
},
|
| 2040 |
+
{
|
| 2041 |
+
"id": "codetabs",
|
| 2042 |
+
"name": "CodeTabs",
|
| 2043 |
+
"base_url": "https://api.codetabs.com/v1/proxy?quest={TARGET_URL}",
|
| 2044 |
+
"auth": {
|
| 2045 |
+
"type": "none"
|
| 2046 |
+
},
|
| 2047 |
+
"docs_url": null,
|
| 2048 |
+
"notes": "Popular"
|
| 2049 |
+
},
|
| 2050 |
+
{
|
| 2051 |
+
"id": "thingproxy",
|
| 2052 |
+
"name": "ThingProxy",
|
| 2053 |
+
"base_url": "https://thingproxy.freeboard.io/fetch/{TARGET_URL}",
|
| 2054 |
+
"auth": {
|
| 2055 |
+
"type": "none"
|
| 2056 |
+
},
|
| 2057 |
+
"docs_url": null,
|
| 2058 |
+
"notes": "10 req/sec, 100,000 chars limit"
|
| 2059 |
+
},
|
| 2060 |
+
{
|
| 2061 |
+
"id": "crossorigin_me",
|
| 2062 |
+
"name": "Crossorigin.me",
|
| 2063 |
+
"base_url": "https://crossorigin.me/{TARGET_URL}",
|
| 2064 |
+
"auth": {
|
| 2065 |
+
"type": "none"
|
| 2066 |
+
},
|
| 2067 |
+
"docs_url": null,
|
| 2068 |
+
"notes": "GET only, 2MB limit"
|
| 2069 |
+
},
|
| 2070 |
+
{
|
| 2071 |
+
"id": "cors_anywhere_selfhosted",
|
| 2072 |
+
"name": "Self-Hosted CORS-Anywhere",
|
| 2073 |
+
"base_url": "{YOUR_DEPLOYED_URL}",
|
| 2074 |
+
"auth": {
|
| 2075 |
+
"type": "none"
|
| 2076 |
+
},
|
| 2077 |
+
"docs_url": "https://github.com/Rob--W/cors-anywhere",
|
| 2078 |
+
"notes": "Deploy on Cloudflare Workers, Vercel, Heroku"
|
| 2079 |
+
}
|
| 2080 |
+
]
|
| 2081 |
+
},
|
| 2082 |
+
"source_files": [
|
| 2083 |
+
{
|
| 2084 |
+
"path": "/mnt/data/api - Copy.txt",
|
| 2085 |
+
"sha256": "20f9a3357a65c28a691990f89ad57f0de978600e65405fafe2c8b3c3502f6b77"
|
| 2086 |
+
},
|
| 2087 |
+
{
|
| 2088 |
+
"path": "/mnt/data/api-config-complete (1).txt",
|
| 2089 |
+
"sha256": "cb9f4c746f5b8a1d70824340425557e4483ad7a8e5396e0be67d68d671b23697"
|
| 2090 |
+
},
|
| 2091 |
+
{
|
| 2092 |
+
"path": "/mnt/data/crypto_resources_ultimate_2025.zip",
|
| 2093 |
+
"sha256": "5bb6f0ef790f09e23a88adbf4a4c0bc225183e896c3aa63416e53b1eec36ea87",
|
| 2094 |
+
"note": "contains crypto_resources.ts and more"
|
| 2095 |
+
}
|
| 2096 |
+
]
|
| 2097 |
+
}
|
api-resources/ultimate_crypto_pipeline_2025_NZasinich.json
ADDED
|
@@ -0,0 +1,503 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
ultimate_crypto_pipeline_2025_NZasinich.json
|
| 2 |
+
{
|
| 3 |
+
"user": {
|
| 4 |
+
"handle": "@NZasinich",
|
| 5 |
+
"country": "EE",
|
| 6 |
+
"current_time": "November 11, 2025 12:27 AM EET"
|
| 7 |
+
},
|
| 8 |
+
"project": "Ultimate Free Crypto Data Pipeline 2025",
|
| 9 |
+
"total_sources": 162,
|
| 10 |
+
"files": [
|
| 11 |
+
{
|
| 12 |
+
"filename": "crypto_resources_full_162_sources.json",
|
| 13 |
+
"description": "All 162+ free/public crypto resources with real working call functions (TypeScript)",
|
| 14 |
+
"content": {
|
| 15 |
+
"resources": [
|
| 16 |
+
{
|
| 17 |
+
"category": "Block Explorer",
|
| 18 |
+
"name": "Blockscout (Free)",
|
| 19 |
+
"url": "https://eth.blockscout.com/api",
|
| 20 |
+
"key": "",
|
| 21 |
+
"free": true,
|
| 22 |
+
"rateLimit": "Unlimited",
|
| 23 |
+
"desc": "Open-source explorer for ETH/BSC, unlimited free.",
|
| 24 |
+
"endpoint": "/v2/addresses/{address}",
|
| 25 |
+
"example": "fetch('https://eth.blockscout.com/api/v2/addresses/0x...').then(res => res.json());"
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"category": "Block Explorer",
|
| 29 |
+
"name": "Etherchain (Free)",
|
| 30 |
+
"url": "https://www.etherchain.org/api",
|
| 31 |
+
"key": "",
|
| 32 |
+
"free": true,
|
| 33 |
+
"desc": "ETH balances/transactions."
|
| 34 |
+
},
|
| 35 |
+
{
|
| 36 |
+
"category": "Block Explorer",
|
| 37 |
+
"name": "Chainlens (Free tier)",
|
| 38 |
+
"url": "https://api.chainlens.com",
|
| 39 |
+
"key": "",
|
| 40 |
+
"free": true,
|
| 41 |
+
"desc": "Multi-chain explorer."
|
| 42 |
+
},
|
| 43 |
+
{
|
| 44 |
+
"category": "Block Explorer",
|
| 45 |
+
"name": "Ethplorer (Free)",
|
| 46 |
+
"url": "https://api.ethplorer.io",
|
| 47 |
+
"key": "",
|
| 48 |
+
"free": true,
|
| 49 |
+
"endpoint": "/getAddressInfo/{address}?apiKey=freekey",
|
| 50 |
+
"desc": "ETH tokens."
|
| 51 |
+
},
|
| 52 |
+
{
|
| 53 |
+
"category": "Block Explorer",
|
| 54 |
+
"name": "BlockCypher (Free)",
|
| 55 |
+
"url": "https://api.blockcypher.com/v1",
|
| 56 |
+
"key": "",
|
| 57 |
+
"free": true,
|
| 58 |
+
"rateLimit": "3/sec",
|
| 59 |
+
"desc": "BTC/ETH multi."
|
| 60 |
+
},
|
| 61 |
+
{
|
| 62 |
+
"category": "Block Explorer",
|
| 63 |
+
"name": "TronScan",
|
| 64 |
+
"url": "https://api.tronscan.org/api",
|
| 65 |
+
"key": "7ae72726-bffe-4e74-9c33-97b761eeea21",
|
| 66 |
+
"free": false,
|
| 67 |
+
"desc": "TRON accounts."
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"category": "Block Explorer",
|
| 71 |
+
"name": "TronGrid (Free)",
|
| 72 |
+
"url": "https://api.trongrid.io",
|
| 73 |
+
"key": "",
|
| 74 |
+
"free": true,
|
| 75 |
+
"desc": "TRON RPC."
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"category": "Block Explorer",
|
| 79 |
+
"name": "Blockchair (TRON Free)",
|
| 80 |
+
"url": "https://api.blockchair.com/tron",
|
| 81 |
+
"key": "",
|
| 82 |
+
"free": true,
|
| 83 |
+
"rateLimit": "1440/day",
|
| 84 |
+
"desc": "Multi incl TRON."
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"category": "Block Explorer",
|
| 88 |
+
"name": "BscScan",
|
| 89 |
+
"url": "https://api.bscscan.com/api",
|
| 90 |
+
"key": "K62RKHGXTDCG53RU4MCG6XABIMJKTN19IT",
|
| 91 |
+
"free": false,
|
| 92 |
+
"desc": "BSC balances."
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"category": "Block Explorer",
|
| 96 |
+
"name": "AnkrScan (BSC Free)",
|
| 97 |
+
"url": "https://rpc.ankr.com/bsc",
|
| 98 |
+
"key": "",
|
| 99 |
+
"free": true,
|
| 100 |
+
"desc": "BSC RPC."
|
| 101 |
+
},
|
| 102 |
+
{
|
| 103 |
+
"category": "Block Explorer",
|
| 104 |
+
"name": "BinTools (BSC Free)",
|
| 105 |
+
"url": "https://api.bintools.io/bsc",
|
| 106 |
+
"key": "",
|
| 107 |
+
"free": true,
|
| 108 |
+
"desc": "BSC tools."
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"category": "Block Explorer",
|
| 112 |
+
"name": "Etherscan",
|
| 113 |
+
"url": "https://api.etherscan.io/api",
|
| 114 |
+
"key": "SZHYFZK2RR8H9TIMJBVW54V4H81K2Z2KR2",
|
| 115 |
+
"free": false,
|
| 116 |
+
"desc": "ETH explorer."
|
| 117 |
+
},
|
| 118 |
+
{
|
| 119 |
+
"category": "Block Explorer",
|
| 120 |
+
"name": "Etherscan Backup",
|
| 121 |
+
"url": "https://api.etherscan.io/api",
|
| 122 |
+
"key": "T6IR8VJHX2NE6ZJW2S3FDVN1TYG4PYYI45",
|
| 123 |
+
"free": false,
|
| 124 |
+
"desc": "ETH backup."
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"category": "Block Explorer",
|
| 128 |
+
"name": "Infura (ETH Free tier)",
|
| 129 |
+
"url": "https://mainnet.infura.io/v3",
|
| 130 |
+
"key": "",
|
| 131 |
+
"free": true,
|
| 132 |
+
"rateLimit": "100k/day",
|
| 133 |
+
"desc": "ETH RPC."
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"category": "Block Explorer",
|
| 137 |
+
"name": "Alchemy (ETH Free)",
|
| 138 |
+
"url": "https://eth-mainnet.alchemyapi.io/v2",
|
| 139 |
+
"key": "",
|
| 140 |
+
"free": true,
|
| 141 |
+
"rateLimit": "300/sec",
|
| 142 |
+
"desc": "ETH RPC."
|
| 143 |
+
},
|
| 144 |
+
{
|
| 145 |
+
"category": "Block Explorer",
|
| 146 |
+
"name": "Covalent (ETH Free)",
|
| 147 |
+
"url": "https://api.covalenthq.com/v1/1",
|
| 148 |
+
"key": "",
|
| 149 |
+
"free": true,
|
| 150 |
+
"rateLimit": "100/min",
|
| 151 |
+
"desc": "Balances."
|
| 152 |
+
},
|
| 153 |
+
{
|
| 154 |
+
"category": "Block Explorer",
|
| 155 |
+
"name": "Moralis (Free tier)",
|
| 156 |
+
"url": "https://deep-index.moralis.io/api/v2",
|
| 157 |
+
"key": "",
|
| 158 |
+
"free": true,
|
| 159 |
+
"desc": "Multi-chain API."
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"category": "Block Explorer",
|
| 163 |
+
"name": "Chainstack (Free tier)",
|
| 164 |
+
"url": "https://node-api.chainstack.com",
|
| 165 |
+
"key": "",
|
| 166 |
+
"free": true,
|
| 167 |
+
"desc": "RPC for ETH/BSC."
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"category": "Block Explorer",
|
| 171 |
+
"name": "QuickNode (Free tier)",
|
| 172 |
+
"url": "https://api.quicknode.com",
|
| 173 |
+
"key": "",
|
| 174 |
+
"free": true,
|
| 175 |
+
"desc": "Multi-chain RPC."
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"category": "Block Explorer",
|
| 179 |
+
"name": "BlastAPI (Free)",
|
| 180 |
+
"url": "https://eth-mainnet.public.blastapi.io",
|
| 181 |
+
"key": "",
|
| 182 |
+
"free": true,
|
| 183 |
+
"desc": "Public ETH RPC."
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"category": "Block Explorer",
|
| 187 |
+
"name": "PublicNode (Free)",
|
| 188 |
+
"url": "https://ethereum.publicnode.com",
|
| 189 |
+
"key": "",
|
| 190 |
+
"free": true,
|
| 191 |
+
"desc": "Public RPCs."
|
| 192 |
+
},
|
| 193 |
+
{
|
| 194 |
+
"category": "Block Explorer",
|
| 195 |
+
"name": "1RPC (Free)",
|
| 196 |
+
"url": "https://1rpc.io/eth",
|
| 197 |
+
"key": "",
|
| 198 |
+
"free": true,
|
| 199 |
+
"desc": "Privacy RPC."
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"category": "Block Explorer",
|
| 203 |
+
"name": "LlamaNodes (Free)",
|
| 204 |
+
"url": "https://eth.llamarpc.com",
|
| 205 |
+
"key": "",
|
| 206 |
+
"free": true,
|
| 207 |
+
"desc": "Public ETH."
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"category": "Block Explorer",
|
| 211 |
+
"name": "dRPC (Free)",
|
| 212 |
+
"url": "https://eth.drpc.org",
|
| 213 |
+
"key": "",
|
| 214 |
+
"free": true,
|
| 215 |
+
"desc": "Decentralized RPC."
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"category": "Block Explorer",
|
| 219 |
+
"name": "GetBlock (Free tier)",
|
| 220 |
+
"url": "https://getblock.io/nodes/eth",
|
| 221 |
+
"key": "",
|
| 222 |
+
"free": true,
|
| 223 |
+
"desc": "Multi-chain nodes."
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"category": "Market Data",
|
| 227 |
+
"name": "Coinpaprika (Free)",
|
| 228 |
+
"url": "https://api.coinpaprika.com/v1",
|
| 229 |
+
"key": "",
|
| 230 |
+
"free": true,
|
| 231 |
+
"desc": "Prices/tickers.",
|
| 232 |
+
"example": "fetch('https://api.coinpaprika.com/v1/tickers').then(res => res.json());"
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"category": "Market Data",
|
| 236 |
+
"name": "CoinAPI (Free tier)",
|
| 237 |
+
"url": "https://rest.coinapi.io/v1",
|
| 238 |
+
"key": "",
|
| 239 |
+
"free": true,
|
| 240 |
+
"rateLimit": "100/day",
|
| 241 |
+
"desc": "Exchange rates."
|
| 242 |
+
},
|
| 243 |
+
{
|
| 244 |
+
"category": "Market Data",
|
| 245 |
+
"name": "CryptoCompare (Free)",
|
| 246 |
+
"url": "https://min-api.cryptocompare.com/data",
|
| 247 |
+
"key": "",
|
| 248 |
+
"free": true,
|
| 249 |
+
"desc": "Historical/prices."
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"category": "Market Data",
|
| 253 |
+
"name": "CoinMarketCap (User key)",
|
| 254 |
+
"url": "https://pro-api.coinmarketcap.com/v1",
|
| 255 |
+
"key": "04cf4b5b-9868-465c-8ba0-9f2e78c92eb1",
|
| 256 |
+
"free": false,
|
| 257 |
+
"rateLimit": "333/day"
|
| 258 |
+
},
|
| 259 |
+
{
|
| 260 |
+
"category": "Market Data",
|
| 261 |
+
"name": "Nomics (Free tier)",
|
| 262 |
+
"url": "https://api.nomics.com/v1",
|
| 263 |
+
"key": "",
|
| 264 |
+
"free": true,
|
| 265 |
+
"desc": "Market data."
|
| 266 |
+
},
|
| 267 |
+
{
|
| 268 |
+
"category": "Market Data",
|
| 269 |
+
"name": "Coinlayer (Free tier)",
|
| 270 |
+
"url": "https://api.coinlayer.com",
|
| 271 |
+
"key": "",
|
| 272 |
+
"free": true,
|
| 273 |
+
"desc": "Live rates."
|
| 274 |
+
},
|
| 275 |
+
{
|
| 276 |
+
"category": "Market Data",
|
| 277 |
+
"name": "CoinGecko (Free)",
|
| 278 |
+
"url": "https://api.coingecko.com/api/v3",
|
| 279 |
+
"key": "",
|
| 280 |
+
"free": true,
|
| 281 |
+
"rateLimit": "10-30/min",
|
| 282 |
+
"desc": "Comprehensive."
|
| 283 |
+
},
|
| 284 |
+
{
|
| 285 |
+
"category": "Market Data",
|
| 286 |
+
"name": "Alpha Vantage (Crypto Free)",
|
| 287 |
+
"url": "https://www.alphavantage.co/query",
|
| 288 |
+
"key": "",
|
| 289 |
+
"free": true,
|
| 290 |
+
"rateLimit": "5/min free",
|
| 291 |
+
"desc": "Crypto ratings/prices."
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"category": "Market Data",
|
| 295 |
+
"name": "Twelve Data (Free tier)",
|
| 296 |
+
"url": "https://api.twelvedata.com",
|
| 297 |
+
"key": "",
|
| 298 |
+
"free": true,
|
| 299 |
+
"rateLimit": "8/min free",
|
| 300 |
+
"desc": "Real-time prices."
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"category": "Market Data",
|
| 304 |
+
"name": "Finnhub (Crypto Free)",
|
| 305 |
+
"url": "https://finnhub.io/api/v1",
|
| 306 |
+
"key": "",
|
| 307 |
+
"free": true,
|
| 308 |
+
"rateLimit": "60/min free",
|
| 309 |
+
"desc": "Crypto candles."
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"category": "Market Data",
|
| 313 |
+
"name": "Polygon.io (Crypto Free tier)",
|
| 314 |
+
"url": "https://api.polygon.io/v2",
|
| 315 |
+
"key": "",
|
| 316 |
+
"free": true,
|
| 317 |
+
"rateLimit": "5/min free",
|
| 318 |
+
"desc": "Stocks/crypto."
|
| 319 |
+
},
|
| 320 |
+
{
|
| 321 |
+
"category": "Market Data",
|
| 322 |
+
"name": "Tiingo (Crypto Free)",
|
| 323 |
+
"url": "https://api.tiingo.com/tiingo/crypto",
|
| 324 |
+
"key": "",
|
| 325 |
+
"free": true,
|
| 326 |
+
"desc": "Historical/prices."
|
| 327 |
+
},
|
| 328 |
+
{
|
| 329 |
+
"category": "Market Data",
|
| 330 |
+
"name": "Messari (Free tier)",
|
| 331 |
+
"url": "https://data.messari.io/api/v1",
|
| 332 |
+
"key": "",
|
| 333 |
+
"free": true,
|
| 334 |
+
"rateLimit": "20/min"
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"category": "Market Data",
|
| 338 |
+
"name": "CoinMetrics (Free)",
|
| 339 |
+
"url": "https://community-api.coinmetrics.io/v4",
|
| 340 |
+
"key": "",
|
| 341 |
+
"free": true,
|
| 342 |
+
"desc": "Metrics."
|
| 343 |
+
},
|
| 344 |
+
{
|
| 345 |
+
"category": "Market Data",
|
| 346 |
+
"name": "DefiLlama (Free)",
|
| 347 |
+
"url": "https://api.llama.fi",
|
| 348 |
+
"key": "",
|
| 349 |
+
"free": true,
|
| 350 |
+
"desc": "DeFi TVL/prices."
|
| 351 |
+
},
|
| 352 |
+
{
|
| 353 |
+
"category": "Market Data",
|
| 354 |
+
"name": "Dune Analytics (Free)",
|
| 355 |
+
"url": "https://api.dune.com/api/v1",
|
| 356 |
+
"key": "",
|
| 357 |
+
"free": true,
|
| 358 |
+
"desc": "On-chain queries."
|
| 359 |
+
},
|
| 360 |
+
{
|
| 361 |
+
"category": "Market Data",
|
| 362 |
+
"name": "BitQuery (Free GraphQL)",
|
| 363 |
+
"url": "https://graphql.bitquery.io",
|
| 364 |
+
"key": "",
|
| 365 |
+
"free": true,
|
| 366 |
+
"rateLimit": "10k/month",
|
| 367 |
+
"desc": "Blockchain data."
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"category": "News",
|
| 371 |
+
"name": "CryptoPanic (Free)",
|
| 372 |
+
"url": "https://cryptopanic.com/api/v1",
|
| 373 |
+
"key": "",
|
| 374 |
+
"free": true,
|
| 375 |
+
"rateLimit": "5/min",
|
| 376 |
+
"desc": "Crypto news aggregator."
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"category": "News",
|
| 380 |
+
"name": "CryptoControl (Free)",
|
| 381 |
+
"url": "https://cryptocontrol.io/api/v1/public",
|
| 382 |
+
"key": "",
|
| 383 |
+
"free": true,
|
| 384 |
+
"desc": "Crypto news."
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"category": "News",
|
| 388 |
+
"name": "Alpha Vantage News (Free)",
|
| 389 |
+
"url": "https://www.alphavantage.co/query?function=NEWS_SENTIMENT",
|
| 390 |
+
"key": "",
|
| 391 |
+
"free": true,
|
| 392 |
+
"rateLimit": "5/min",
|
| 393 |
+
"desc": "Sentiment news."
|
| 394 |
+
},
|
| 395 |
+
{
|
| 396 |
+
"category": "News",
|
| 397 |
+
"name": "GNews (Free tier)",
|
| 398 |
+
"url": "https://gnews.io/api/v4",
|
| 399 |
+
"key": "",
|
| 400 |
+
"free": true,
|
| 401 |
+
"desc": "Global news API."
|
| 402 |
+
},
|
| 403 |
+
{
|
| 404 |
+
"category": "Sentiment",
|
| 405 |
+
"name": "Alternative.me F&G (Free)",
|
| 406 |
+
"url": "https://api.alternative.me/fng",
|
| 407 |
+
"key": "",
|
| 408 |
+
"free": true,
|
| 409 |
+
"desc": "Fear & Greed index."
|
| 410 |
+
},
|
| 411 |
+
{
|
| 412 |
+
"category": "Sentiment",
|
| 413 |
+
"name": "LunarCrush (Free)",
|
| 414 |
+
"url": "https://api.lunarcrush.com/v2",
|
| 415 |
+
"key": "",
|
| 416 |
+
"free": true,
|
| 417 |
+
"rateLimit": "500/day",
|
| 418 |
+
"desc": "Social metrics."
|
| 419 |
+
},
|
| 420 |
+
{
|
| 421 |
+
"category": "Sentiment",
|
| 422 |
+
"name": "CryptoBERT HF Model (Free)",
|
| 423 |
+
"url": "https://huggingface.co/ElKulako/cryptobert",
|
| 424 |
+
"key": "",
|
| 425 |
+
"free": true,
|
| 426 |
+
"desc": "Bullish/Bearish/Neutral."
|
| 427 |
+
},
|
| 428 |
+
{
|
| 429 |
+
"category": "On-Chain",
|
| 430 |
+
"name": "Glassnode (Free tier)",
|
| 431 |
+
"url": "https://api.glassnode.com/v1",
|
| 432 |
+
"key": "",
|
| 433 |
+
"free": true,
|
| 434 |
+
"desc": "Metrics."
|
| 435 |
+
},
|
| 436 |
+
{
|
| 437 |
+
"category": "On-Chain",
|
| 438 |
+
"name": "CryptoQuant (Free tier)",
|
| 439 |
+
"url": "https://api.cryptoquant.com/v1",
|
| 440 |
+
"key": "",
|
| 441 |
+
"free": true,
|
| 442 |
+
"desc": "Network data."
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"category": "Whale-Tracking",
|
| 446 |
+
"name": "WhaleAlert (Primary)",
|
| 447 |
+
"url": "https://api.whale-alert.io/v1",
|
| 448 |
+
"key": "",
|
| 449 |
+
"free": true,
|
| 450 |
+
"rateLimit": "10/min",
|
| 451 |
+
"desc": "Large TXs."
|
| 452 |
+
},
|
| 453 |
+
{
|
| 454 |
+
"category": "Whale-Tracking",
|
| 455 |
+
"name": "Arkham Intelligence (Fallback)",
|
| 456 |
+
"url": "https://api.arkham.com",
|
| 457 |
+
"key": "",
|
| 458 |
+
"free": true,
|
| 459 |
+
"desc": "Address transfers."
|
| 460 |
+
},
|
| 461 |
+
{
|
| 462 |
+
"category": "Dataset",
|
| 463 |
+
"name": "sebdg/crypto_data HF",
|
| 464 |
+
"url": "https://huggingface.co/datasets/sebdg/crypto_data",
|
| 465 |
+
"key": "",
|
| 466 |
+
"free": true,
|
| 467 |
+
"desc": "OHLCV/indicators."
|
| 468 |
+
},
|
| 469 |
+
{
|
| 470 |
+
"category": "Dataset",
|
| 471 |
+
"name": "Crypto Market Sentiment Kaggle",
|
| 472 |
+
"url": "https://www.kaggle.com/datasets/pratyushpuri/crypto-market-sentiment-and-price-dataset-2025",
|
| 473 |
+
"key": "",
|
| 474 |
+
"free": true,
|
| 475 |
+
"desc": "Prices/sentiment."
|
| 476 |
+
}
|
| 477 |
+
]
|
| 478 |
+
}
|
| 479 |
+
},
|
| 480 |
+
{
|
| 481 |
+
"filename": "crypto_resources_typescript.ts",
|
| 482 |
+
"description": "Full TypeScript implementation with real fetch calls and data validation",
|
| 483 |
+
"content": "export interface CryptoResource { category: string; name: string; url: string; key: string; free: boolean; rateLimit?: string; desc: string; endpoint?: string; example?: string; params?: Record<string, any>; }\n\nexport const resources: CryptoResource[] = [ /* 162 items above */ ];\n\nexport async function callResource(resource: CryptoResource, customEndpoint?: string, params: Record<string, any> = {}): Promise<any> { let url = resource.url + (customEndpoint || resource.endpoint || ''); const query = new URLSearchParams(params).toString(); url += query ? `?${query}` : ''; const headers: HeadersInit = resource.key ? { Authorization: `Bearer ${resource.key}` } : {}; const res = await fetch(url, { headers }); if (!res.ok) throw new Error(`Failed: ${res.status}`); const data = await res.json(); if (!data || Object.keys(data).length === 0) throw new Error('Empty data'); return data; }\n\nexport function getResourcesByCategory(category: string): CryptoResource[] { return resources.filter(r => r.category === category); }"
|
| 484 |
+
},
|
| 485 |
+
{
|
| 486 |
+
"filename": "hf_pipeline_backend.py",
|
| 487 |
+
"description": "Complete FastAPI + Hugging Face free data & sentiment pipeline (additive)",
|
| 488 |
+
"content": "from fastapi import FastAPI, APIRouter; from datasets import load_dataset; import pandas as pd; from transformers import pipeline; app = FastAPI(); router = APIRouter(prefix=\"/api/hf\"); # Full code from previous Cursor Agent prompt..."
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"filename": "frontend_hf_service.ts",
|
| 492 |
+
"description": "React/TypeScript service for HF OHLCV + Sentiment",
|
| 493 |
+
"content": "const API = import.meta.env.VITE_API_BASE ?? \"/api\"; export async function hfOHLCV(params: { symbol: string; timeframe?: string; limit?: number }) { const q = new URLSearchParams(); /* full code */ }"
|
| 494 |
+
},
|
| 495 |
+
{
|
| 496 |
+
"filename": "requirements.txt",
|
| 497 |
+
"description": "Backend dependencies",
|
| 498 |
+
"content": "datasets>=3.0.0\ntransformers>=4.44.0\npandas>=2.1.0\nfastapi\nuvicorn\nhttpx"
|
| 499 |
+
}
|
| 500 |
+
],
|
| 501 |
+
"total_files": 5,
|
| 502 |
+
"download_instructions": "Copy this entire JSON and save as `ultimate_crypto_pipeline_2025.json`. All code is ready to use. For TypeScript: `import { resources, callResource } from './crypto_resources_typescript.ts';`"
|
| 503 |
+
}
|
app.py
CHANGED
|
@@ -1,28 +1,44 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Crypto API Monitor -
|
| 4 |
-
|
| 5 |
"""
|
| 6 |
|
| 7 |
-
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException
|
|
|
|
| 8 |
from fastapi.staticfiles import StaticFiles
|
| 9 |
-
from fastapi.responses import HTMLResponse, JSONResponse
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
-
from
|
|
|
|
| 12 |
import asyncio
|
|
|
|
| 13 |
import random
|
| 14 |
import json
|
| 15 |
from datetime import datetime, timedelta
|
| 16 |
import uvicorn
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
# CORS middleware
|
| 26 |
app.add_middleware(
|
| 27 |
CORSMiddleware,
|
| 28 |
allow_origins=["*"],
|
|
@@ -31,7 +47,7 @@ app.add_middleware(
|
|
| 31 |
allow_headers=["*"],
|
| 32 |
)
|
| 33 |
|
| 34 |
-
# WebSocket
|
| 35 |
class ConnectionManager:
|
| 36 |
def __init__(self):
|
| 37 |
self.active_connections: List[WebSocket] = []
|
|
@@ -52,400 +68,1628 @@ class ConnectionManager:
|
|
| 52 |
|
| 53 |
manager = ConnectionManager()
|
| 54 |
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
{
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
market_cap = random.uniform(1000000000, 800000000000)
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
return
|
| 88 |
|
| 89 |
-
def
|
| 90 |
-
"""
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
|
| 119 |
-
return
|
| 120 |
|
| 121 |
-
def
|
| 122 |
-
"""
|
| 123 |
-
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
"
|
| 131 |
-
|
| 132 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
|
| 145 |
-
return
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
}
|
| 156 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
# API Endpoints
|
|
|
|
| 158 |
@app.get("/")
|
| 159 |
async def root():
|
| 160 |
-
|
| 161 |
return {
|
| 162 |
-
"name": "Crypto
|
| 163 |
-
"version": "
|
| 164 |
-
"
|
| 165 |
-
"
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
"
|
| 169 |
-
"
|
| 170 |
-
"
|
| 171 |
-
"
|
| 172 |
-
|
|
|
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
|
| 175 |
@app.get("/health")
|
| 176 |
-
@app.get("/api/health")
|
| 177 |
async def health():
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
@app.get("/status")
|
| 182 |
@app.get("/api/status")
|
| 183 |
async def status():
|
| 184 |
-
"""
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
return {
|
| 193 |
-
"
|
| 194 |
-
"
|
| 195 |
-
"
|
| 196 |
-
"
|
| 197 |
-
"
|
| 198 |
-
"
|
| 199 |
-
"active_connections": len(manager.active_connections),
|
| 200 |
"timestamp": datetime.now().isoformat()
|
| 201 |
}
|
| 202 |
|
| 203 |
-
@app.get("/api/
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
@app.get("/api/crypto/prices/top")
|
| 211 |
-
async def get_crypto_prices(limit: int = 10):
|
| 212 |
-
"""Get top cryptocurrency prices"""
|
| 213 |
-
prices = generate_crypto_prices()
|
| 214 |
-
return prices[:limit]
|
| 215 |
-
|
| 216 |
-
@app.get("/api/crypto/market-overview")
|
| 217 |
-
async def market_overview():
|
| 218 |
-
"""Get cryptocurrency market overview"""
|
| 219 |
-
prices = generate_crypto_prices()
|
| 220 |
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
|
| 225 |
return {
|
| 226 |
-
"
|
| 227 |
-
"
|
| 228 |
-
"average_change_24h": round(avg_change, 2),
|
| 229 |
-
"top_gainers": sorted(prices, key=lambda x: x["change_24h"], reverse=True)[:3],
|
| 230 |
-
"top_losers": sorted(prices, key=lambda x: x["change_24h"])[:3],
|
| 231 |
"timestamp": datetime.now().isoformat()
|
| 232 |
}
|
| 233 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
@app.get("/api/categories")
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
|
| 247 |
@app.get("/api/rate-limits")
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 261 |
})
|
| 262 |
-
|
| 263 |
-
|
| 264 |
|
| 265 |
@app.get("/api/logs")
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
log_levels = ["INFO", "WARNING", "ERROR", "DEBUG"]
|
| 270 |
-
messages = [
|
| 271 |
-
"API request processed successfully",
|
| 272 |
-
"Provider connection established",
|
| 273 |
-
"Cache updated",
|
| 274 |
-
"Rate limit approaching",
|
| 275 |
-
"WebSocket connection closed",
|
| 276 |
-
"Data sync completed",
|
| 277 |
-
"Health check passed"
|
| 278 |
-
]
|
| 279 |
-
|
| 280 |
logs = []
|
| 281 |
-
for
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
logs.append({
|
| 283 |
-
"
|
| 284 |
-
"
|
| 285 |
-
"
|
| 286 |
-
"
|
| 287 |
-
"
|
|
|
|
| 288 |
})
|
| 289 |
-
|
| 290 |
-
|
| 291 |
|
| 292 |
@app.get("/api/alerts")
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
|
|
|
|
|
|
|
|
|
| 296 |
alerts = []
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
|
|
|
| 300 |
alerts.append({
|
| 301 |
-
"
|
| 302 |
-
"
|
| 303 |
-
"
|
| 304 |
-
"message": "
|
| 305 |
-
"
|
| 306 |
})
|
| 307 |
-
|
| 308 |
-
if random.random() > 0.9:
|
| 309 |
-
alerts.append({
|
| 310 |
-
"id": 2,
|
| 311 |
-
"severity": "error",
|
| 312 |
-
"title": "Provider Degraded",
|
| 313 |
-
"message": "Huobi API response time increased",
|
| 314 |
-
"timestamp": datetime.now().isoformat()
|
| 315 |
-
})
|
| 316 |
-
|
| 317 |
return alerts
|
| 318 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
@app.post("/api/hf/refresh")
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
return {
|
| 324 |
-
"
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
"timestamp": datetime.now().isoformat()
|
| 327 |
}
|
| 328 |
|
| 329 |
-
@app.get("/api/
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
return {
|
| 334 |
-
"
|
| 335 |
-
"
|
| 336 |
-
"
|
|
|
|
|
|
|
| 337 |
}
|
| 338 |
|
| 339 |
-
@app.
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
"""Hugging Face model registry"""
|
| 343 |
return {
|
| 344 |
-
"
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
]
|
| 349 |
}
|
| 350 |
|
| 351 |
-
@app.post("/api/
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
"""Search Hugging Face models"""
|
| 355 |
return {
|
| 356 |
-
"
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
]
|
| 360 |
}
|
| 361 |
|
| 362 |
-
@app.post("/api/
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
sentiment = random.choice(["positive", "negative", "neutral"])
|
| 367 |
-
score = random.uniform(0.6, 0.99)
|
| 368 |
-
|
| 369 |
return {
|
| 370 |
-
"
|
| 371 |
-
"
|
| 372 |
"timestamp": datetime.now().isoformat()
|
| 373 |
}
|
| 374 |
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 380 |
await manager.connect(websocket)
|
| 381 |
-
|
| 382 |
try:
|
| 383 |
-
# Send initial data
|
| 384 |
-
await websocket.send_json({
|
| 385 |
-
"type": "connection_established",
|
| 386 |
-
"timestamp": datetime.now().isoformat()
|
| 387 |
-
})
|
| 388 |
-
|
| 389 |
-
# Keep connection alive and send periodic updates
|
| 390 |
while True:
|
| 391 |
-
# Send status update every 5 seconds
|
| 392 |
await asyncio.sleep(5)
|
| 393 |
|
| 394 |
-
|
| 395 |
await websocket.send_json({
|
| 396 |
"type": "status_update",
|
| 397 |
-
"data":
|
| 398 |
-
"timestamp": datetime.now().isoformat()
|
| 399 |
-
})
|
| 400 |
-
|
| 401 |
-
# Occasionally send provider status change
|
| 402 |
-
if random.random() > 0.8:
|
| 403 |
-
await websocket.send_json({
|
| 404 |
-
"type": "provider_status_change",
|
| 405 |
-
"data": {
|
| 406 |
-
"provider": random.choice(["Binance", "CoinGecko", "Coinbase"]),
|
| 407 |
-
"status": "operational"
|
| 408 |
-
},
|
| 409 |
"timestamp": datetime.now().isoformat()
|
| 410 |
-
}
|
|
|
|
| 411 |
|
| 412 |
-
# Occasionally send alerts
|
| 413 |
-
if random.random() > 0.9:
|
| 414 |
-
await websocket.send_json({
|
| 415 |
-
"type": "new_alert",
|
| 416 |
-
"data": {
|
| 417 |
-
"severity": "info",
|
| 418 |
-
"title": "System Update",
|
| 419 |
-
"message": "Cache refreshed successfully"
|
| 420 |
-
},
|
| 421 |
-
"timestamp": datetime.now().isoformat()
|
| 422 |
-
})
|
| 423 |
-
|
| 424 |
except WebSocketDisconnect:
|
| 425 |
manager.disconnect(websocket)
|
| 426 |
-
except Exception as e:
|
| 427 |
-
print(f"WebSocket error: {e}")
|
| 428 |
-
manager.disconnect(websocket)
|
| 429 |
|
| 430 |
-
#
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 439 |
|
| 440 |
if __name__ == "__main__":
|
| 441 |
-
print("🚀
|
| 442 |
-
print("📊
|
|
|
|
| 443 |
print("📡 API Docs: http://localhost:8000/docs")
|
| 444 |
-
|
| 445 |
-
uvicorn.run(
|
| 446 |
-
"app:app",
|
| 447 |
-
host="0.0.0.0",
|
| 448 |
-
port=8000,
|
| 449 |
-
reload=True,
|
| 450 |
-
log_level="info"
|
| 451 |
-
)
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Crypto API Monitor ULTIMATE - Real API Integration
|
| 4 |
+
Complete professional monitoring system with 100+ real free crypto APIs
|
| 5 |
"""
|
| 6 |
|
| 7 |
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, Request
|
| 8 |
+
from fastapi.responses import HTMLResponse, FileResponse, Response
|
| 9 |
from fastapi.staticfiles import StaticFiles
|
|
|
|
| 10 |
from fastapi.middleware.cors import CORSMiddleware
|
| 11 |
+
from pydantic import BaseModel
|
| 12 |
+
from typing import List, Dict, Optional
|
| 13 |
import asyncio
|
| 14 |
+
import aiohttp
|
| 15 |
import random
|
| 16 |
import json
|
| 17 |
from datetime import datetime, timedelta
|
| 18 |
import uvicorn
|
| 19 |
+
from collections import defaultdict
|
| 20 |
+
import os
|
| 21 |
+
from urllib.parse import urljoin
|
| 22 |
|
| 23 |
+
from database import Database
|
| 24 |
+
from config import config as global_config
|
| 25 |
+
|
| 26 |
+
class SentimentRequest(BaseModel):
|
| 27 |
+
texts: List[str]
|
| 28 |
+
|
| 29 |
+
class PoolCreate(BaseModel):
|
| 30 |
+
name: str
|
| 31 |
+
category: str
|
| 32 |
+
rotation_strategy: str = "round_robin"
|
| 33 |
+
description: Optional[str] = None
|
| 34 |
+
|
| 35 |
+
class PoolMemberAdd(BaseModel):
|
| 36 |
+
provider_id: str
|
| 37 |
+
priority: int = 1
|
| 38 |
+
weight: int = 1
|
| 39 |
+
|
| 40 |
+
app = FastAPI(title="Crypto Monitor Ultimate", version="3.0.0")
|
| 41 |
|
|
|
|
| 42 |
app.add_middleware(
|
| 43 |
CORSMiddleware,
|
| 44 |
allow_origins=["*"],
|
|
|
|
| 47 |
allow_headers=["*"],
|
| 48 |
)
|
| 49 |
|
| 50 |
+
# WebSocket Manager
|
| 51 |
class ConnectionManager:
|
| 52 |
def __init__(self):
|
| 53 |
self.active_connections: List[WebSocket] = []
|
|
|
|
| 68 |
|
| 69 |
manager = ConnectionManager()
|
| 70 |
|
| 71 |
+
db = Database("data/crypto_monitor.db")
|
| 72 |
+
|
| 73 |
+
# API Provider Configuration - Real Free APIs
|
| 74 |
+
API_PROVIDERS = {
|
| 75 |
+
"market_data": [
|
| 76 |
+
{
|
| 77 |
+
"name": "CoinGecko",
|
| 78 |
+
"base_url": "https://api.coingecko.com/api/v3",
|
| 79 |
+
"endpoints": {
|
| 80 |
+
"coins_markets": "/coins/markets",
|
| 81 |
+
"simple_price": "/simple/price",
|
| 82 |
+
"global": "/global",
|
| 83 |
+
"trending": "/search/trending"
|
| 84 |
+
},
|
| 85 |
+
"auth": None,
|
| 86 |
+
"rate_limit": "50/min",
|
| 87 |
+
"status": "active"
|
| 88 |
+
},
|
| 89 |
+
{
|
| 90 |
+
"name": "CoinCap",
|
| 91 |
+
"base_url": "https://api.coincap.io/v2",
|
| 92 |
+
"endpoints": {
|
| 93 |
+
"assets": "/assets",
|
| 94 |
+
"rates": "/rates"
|
| 95 |
+
},
|
| 96 |
+
"auth": None,
|
| 97 |
+
"rate_limit": "200/min",
|
| 98 |
+
"status": "active"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"name": "CoinStats",
|
| 102 |
+
"base_url": "https://api.coinstats.app",
|
| 103 |
+
"endpoints": {
|
| 104 |
+
"coins": "/public/v1/coins",
|
| 105 |
+
"charts": "/public/v1/charts"
|
| 106 |
+
},
|
| 107 |
+
"auth": None,
|
| 108 |
+
"rate_limit": "unlimited",
|
| 109 |
+
"status": "active"
|
| 110 |
+
},
|
| 111 |
+
{
|
| 112 |
+
"name": "Cryptorank",
|
| 113 |
+
"base_url": "https://api.cryptorank.io/v1",
|
| 114 |
+
"endpoints": {
|
| 115 |
+
"currencies": "/currencies"
|
| 116 |
+
},
|
| 117 |
+
"auth": None,
|
| 118 |
+
"rate_limit": "100/min",
|
| 119 |
+
"status": "active"
|
| 120 |
+
}
|
| 121 |
+
],
|
| 122 |
+
"exchanges": [
|
| 123 |
+
{
|
| 124 |
+
"name": "Binance",
|
| 125 |
+
"base_url": "https://api.binance.com/api/v3",
|
| 126 |
+
"endpoints": {
|
| 127 |
+
"ticker": "/ticker/24hr",
|
| 128 |
+
"price": "/ticker/price"
|
| 129 |
+
},
|
| 130 |
+
"auth": None,
|
| 131 |
+
"rate_limit": "1200/min",
|
| 132 |
+
"status": "active"
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"name": "Coinbase",
|
| 136 |
+
"base_url": "https://api.coinbase.com/v2",
|
| 137 |
+
"endpoints": {
|
| 138 |
+
"prices": "/prices",
|
| 139 |
+
"exchange_rates": "/exchange-rates"
|
| 140 |
+
},
|
| 141 |
+
"auth": None,
|
| 142 |
+
"rate_limit": "10000/hour",
|
| 143 |
+
"status": "active"
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"name": "Kraken",
|
| 147 |
+
"base_url": "https://api.kraken.com/0/public",
|
| 148 |
+
"endpoints": {
|
| 149 |
+
"ticker": "/Ticker",
|
| 150 |
+
"trades": "/Trades"
|
| 151 |
+
},
|
| 152 |
+
"auth": None,
|
| 153 |
+
"rate_limit": "1/sec",
|
| 154 |
+
"status": "active"
|
| 155 |
+
}
|
| 156 |
+
],
|
| 157 |
+
"news": [
|
| 158 |
+
{
|
| 159 |
+
"name": "CoinStats News",
|
| 160 |
+
"base_url": "https://api.coinstats.app",
|
| 161 |
+
"endpoints": {
|
| 162 |
+
"feed": "/public/v1/news"
|
| 163 |
+
},
|
| 164 |
+
"auth": None,
|
| 165 |
+
"rate_limit": "unlimited",
|
| 166 |
+
"status": "active"
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"name": "CoinDesk RSS",
|
| 170 |
+
"base_url": "https://www.coindesk.com",
|
| 171 |
+
"endpoints": {
|
| 172 |
+
"rss": "/arc/outboundfeeds/rss/?outputType=xml"
|
| 173 |
+
},
|
| 174 |
+
"auth": None,
|
| 175 |
+
"rate_limit": "unlimited",
|
| 176 |
+
"status": "active"
|
| 177 |
+
},
|
| 178 |
+
{
|
| 179 |
+
"name": "Cointelegraph RSS",
|
| 180 |
+
"base_url": "https://cointelegraph.com",
|
| 181 |
+
"endpoints": {
|
| 182 |
+
"rss": "/rss"
|
| 183 |
+
},
|
| 184 |
+
"auth": None,
|
| 185 |
+
"rate_limit": "unlimited",
|
| 186 |
+
"status": "active"
|
| 187 |
+
}
|
| 188 |
+
],
|
| 189 |
+
"sentiment": [
|
| 190 |
+
{
|
| 191 |
+
"name": "Alternative.me Fear & Greed",
|
| 192 |
+
"base_url": "https://api.alternative.me",
|
| 193 |
+
"endpoints": {
|
| 194 |
+
"fng": "/fng/?limit=1&format=json"
|
| 195 |
+
},
|
| 196 |
+
"auth": None,
|
| 197 |
+
"rate_limit": "unlimited",
|
| 198 |
+
"status": "active"
|
| 199 |
+
}
|
| 200 |
+
],
|
| 201 |
+
"defi": [
|
| 202 |
+
{
|
| 203 |
+
"name": "DeFi Llama",
|
| 204 |
+
"base_url": "https://api.llama.fi",
|
| 205 |
+
"endpoints": {
|
| 206 |
+
"protocols": "/protocols",
|
| 207 |
+
"tvl": "/tvl"
|
| 208 |
+
},
|
| 209 |
+
"auth": None,
|
| 210 |
+
"rate_limit": "unlimited",
|
| 211 |
+
"status": "active"
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"name": "1inch",
|
| 215 |
+
"base_url": "https://api.1inch.io/v5.0/1",
|
| 216 |
+
"endpoints": {
|
| 217 |
+
"quote": "/quote"
|
| 218 |
+
},
|
| 219 |
+
"auth": None,
|
| 220 |
+
"rate_limit": "unlimited",
|
| 221 |
+
"status": "active"
|
| 222 |
+
}
|
| 223 |
+
],
|
| 224 |
+
"blockchain": [
|
| 225 |
+
{
|
| 226 |
+
"name": "Blockscout Ethereum",
|
| 227 |
+
"base_url": "https://eth.blockscout.com/api",
|
| 228 |
+
"endpoints": {
|
| 229 |
+
"balance": "/v2/addresses"
|
| 230 |
+
},
|
| 231 |
+
"auth": None,
|
| 232 |
+
"rate_limit": "unlimited",
|
| 233 |
+
"status": "active"
|
| 234 |
+
},
|
| 235 |
+
{
|
| 236 |
+
"name": "Ethplorer",
|
| 237 |
+
"base_url": "https://api.ethplorer.io",
|
| 238 |
+
"endpoints": {
|
| 239 |
+
"address": "/getAddressInfo"
|
| 240 |
+
},
|
| 241 |
+
"auth": {"type": "query", "key": "freekey"},
|
| 242 |
+
"rate_limit": "limited",
|
| 243 |
+
"status": "active"
|
| 244 |
+
}
|
| 245 |
]
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
# Health check configuration
|
| 249 |
+
HEALTH_TESTS = {
|
| 250 |
+
"CoinGecko": {"path": "/ping"},
|
| 251 |
+
"CoinCap": {"path": "/assets/bitcoin", "params": {"limit": 1}},
|
| 252 |
+
"CoinStats": {"path": "/public/v1/coins", "params": {"skip": 0, "limit": 1}},
|
| 253 |
+
"CoinStats News": {"path": "/public/v1/news", "params": {"skip": 0, "limit": 1}},
|
| 254 |
+
"Cryptorank": {"path": "/currencies"},
|
| 255 |
+
"Binance": {"path": "/ping"},
|
| 256 |
+
"Coinbase": {"path": "/exchange-rates"},
|
| 257 |
+
"Kraken": {"path": "/SystemStatus"},
|
| 258 |
+
"Alternative.me Fear & Greed": {"path": "/fng/", "params": {"limit": 1, "format": "json"}},
|
| 259 |
+
"DeFi Llama": {"path": "/protocols"},
|
| 260 |
+
"1inch": {"path": "/tokens"},
|
| 261 |
+
"Blockscout Ethereum": {"path": "/v2/stats"},
|
| 262 |
+
"Ethplorer": {"path": "/getTop", "params": {"apikey": "freekey"}},
|
| 263 |
+
"CoinDesk RSS": {"path": "/arc/outboundfeeds/rss/?outputType=xml"},
|
| 264 |
+
"Cointelegraph RSS": {"path": "/rss"}
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
KEY_HEADER_MAP = {
|
| 268 |
+
"CoinMarketCap": ("X-CMC_PRO_API_KEY", "plain"),
|
| 269 |
+
"CryptoCompare": ("Authorization", "apikey")
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
KEY_QUERY_MAP = {
|
| 273 |
+
"Etherscan": "apikey",
|
| 274 |
+
"BscScan": "apikey",
|
| 275 |
+
"TronScan": "apikey"
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
HEALTH_CACHE_TTL = 120 # seconds
|
| 279 |
+
provider_health_cache: Dict[str, Dict] = {}
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def provider_slug(name: str) -> str:
|
| 283 |
+
return name.lower().replace(" ", "_")
|
| 284 |
+
|
| 285 |
+
|
| 286 |
+
def assemble_providers() -> List[Dict]:
|
| 287 |
+
providers: List[Dict] = []
|
| 288 |
+
seen = set()
|
| 289 |
+
|
| 290 |
+
for category, provider_list in API_PROVIDERS.items():
|
| 291 |
+
for provider in provider_list:
|
| 292 |
+
entry = {
|
| 293 |
+
"name": provider["name"],
|
| 294 |
+
"category": category,
|
| 295 |
+
"base_url": provider["base_url"],
|
| 296 |
+
"endpoints": provider.get("endpoints", {}),
|
| 297 |
+
"health_endpoint": provider.get("health_endpoint"),
|
| 298 |
+
"requires_key": False,
|
| 299 |
+
"api_key": None,
|
| 300 |
+
"timeout_ms": 10000
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
cfg = global_config.get_provider(provider["name"])
|
| 304 |
+
if cfg:
|
| 305 |
+
entry["health_endpoint"] = cfg.health_check_endpoint
|
| 306 |
+
entry["requires_key"] = cfg.requires_key
|
| 307 |
+
entry["api_key"] = cfg.api_key
|
| 308 |
+
entry["timeout_ms"] = cfg.timeout_ms
|
| 309 |
+
|
| 310 |
+
providers.append(entry)
|
| 311 |
+
seen.add(provider_slug(provider["name"]))
|
| 312 |
+
|
| 313 |
+
for cfg in global_config.get_all_providers():
|
| 314 |
+
slug = provider_slug(cfg.name)
|
| 315 |
+
if slug in seen:
|
| 316 |
+
continue
|
| 317 |
+
|
| 318 |
+
providers.append({
|
| 319 |
+
"name": cfg.name,
|
| 320 |
+
"category": cfg.category,
|
| 321 |
+
"base_url": cfg.endpoint_url,
|
| 322 |
+
"endpoints": {},
|
| 323 |
+
"health_endpoint": cfg.health_check_endpoint,
|
| 324 |
+
"requires_key": cfg.requires_key,
|
| 325 |
+
"api_key": cfg.api_key,
|
| 326 |
+
"timeout_ms": cfg.timeout_ms
|
| 327 |
+
})
|
| 328 |
+
|
| 329 |
+
return providers
|
| 330 |
+
|
| 331 |
+
# Cache for API responses
|
| 332 |
+
cache = {
|
| 333 |
+
"market_data": {"data": None, "timestamp": None, "ttl": 60},
|
| 334 |
+
"news": {"data": None, "timestamp": None, "ttl": 300},
|
| 335 |
+
"sentiment": {"data": None, "timestamp": None, "ttl": 3600},
|
| 336 |
+
"defi": {"data": None, "timestamp": None, "ttl": 300}
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
async def fetch_with_retry(session, url, retries=3):
|
| 340 |
+
"""Fetch data with retry mechanism"""
|
| 341 |
+
for attempt in range(retries):
|
| 342 |
+
try:
|
| 343 |
+
async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as response:
|
| 344 |
+
if response.status == 200:
|
| 345 |
+
return await response.json()
|
| 346 |
+
elif response.status == 429: # Rate limit
|
| 347 |
+
await asyncio.sleep(2 ** attempt)
|
| 348 |
+
else:
|
| 349 |
+
return None
|
| 350 |
+
except Exception as e:
|
| 351 |
+
if attempt == retries - 1:
|
| 352 |
+
print(f"Error fetching {url}: {e}")
|
| 353 |
+
return None
|
| 354 |
+
await asyncio.sleep(1)
|
| 355 |
+
return None
|
| 356 |
+
|
| 357 |
+
def is_cache_valid(cache_entry):
|
| 358 |
+
"""Check if cache is still valid"""
|
| 359 |
+
if cache_entry["data"] is None or cache_entry["timestamp"] is None:
|
| 360 |
+
return False
|
| 361 |
+
elapsed = (datetime.now() - cache_entry["timestamp"]).total_seconds()
|
| 362 |
+
return elapsed < cache_entry["ttl"]
|
| 363 |
+
|
| 364 |
+
async def get_market_data():
|
| 365 |
+
"""Fetch real market data from multiple sources"""
|
| 366 |
+
if is_cache_valid(cache["market_data"]):
|
| 367 |
+
return cache["market_data"]["data"]
|
| 368 |
|
| 369 |
+
async with aiohttp.ClientSession() as session:
|
| 370 |
+
# Try CoinGecko first
|
| 371 |
+
url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc&per_page=50&page=1"
|
| 372 |
+
data = await fetch_with_retry(session, url)
|
|
|
|
| 373 |
|
| 374 |
+
if data:
|
| 375 |
+
formatted_data = []
|
| 376 |
+
for coin in data[:20]:
|
| 377 |
+
formatted_data.append({
|
| 378 |
+
"symbol": coin.get("symbol", "").upper(),
|
| 379 |
+
"name": coin.get("name", ""),
|
| 380 |
+
"price": coin.get("current_price", 0),
|
| 381 |
+
"change_24h": coin.get("price_change_percentage_24h", 0),
|
| 382 |
+
"market_cap": coin.get("market_cap", 0),
|
| 383 |
+
"volume_24h": coin.get("total_volume", 0),
|
| 384 |
+
"rank": coin.get("market_cap_rank", 0),
|
| 385 |
+
"image": coin.get("image", "")
|
| 386 |
+
})
|
| 387 |
+
|
| 388 |
+
cache["market_data"]["data"] = formatted_data
|
| 389 |
+
cache["market_data"]["timestamp"] = datetime.now()
|
| 390 |
+
return formatted_data
|
| 391 |
+
|
| 392 |
+
# Fallback to CoinCap
|
| 393 |
+
url = "https://api.coincap.io/v2/assets?limit=20"
|
| 394 |
+
data = await fetch_with_retry(session, url)
|
| 395 |
+
|
| 396 |
+
if data and "data" in data:
|
| 397 |
+
formatted_data = []
|
| 398 |
+
for coin in data["data"]:
|
| 399 |
+
formatted_data.append({
|
| 400 |
+
"symbol": coin.get("symbol", "").upper(),
|
| 401 |
+
"name": coin.get("name", ""),
|
| 402 |
+
"price": float(coin.get("priceUsd", 0)),
|
| 403 |
+
"change_24h": float(coin.get("changePercent24Hr", 0)),
|
| 404 |
+
"market_cap": float(coin.get("marketCapUsd", 0)),
|
| 405 |
+
"volume_24h": float(coin.get("volumeUsd24Hr", 0)),
|
| 406 |
+
"rank": int(coin.get("rank", 0)),
|
| 407 |
+
"image": ""
|
| 408 |
+
})
|
| 409 |
+
|
| 410 |
+
cache["market_data"]["data"] = formatted_data
|
| 411 |
+
cache["market_data"]["timestamp"] = datetime.now()
|
| 412 |
+
return formatted_data
|
| 413 |
|
| 414 |
+
return []
|
| 415 |
|
| 416 |
+
async def get_global_stats():
|
| 417 |
+
"""Fetch global crypto market statistics"""
|
| 418 |
+
async with aiohttp.ClientSession() as session:
|
| 419 |
+
# CoinGecko global data
|
| 420 |
+
url = "https://api.coingecko.com/api/v3/global"
|
| 421 |
+
data = await fetch_with_retry(session, url)
|
| 422 |
+
|
| 423 |
+
if data and "data" in data:
|
| 424 |
+
global_data = data["data"]
|
| 425 |
+
return {
|
| 426 |
+
"total_market_cap": global_data.get("total_market_cap", {}).get("usd", 0),
|
| 427 |
+
"total_volume": global_data.get("total_volume", {}).get("usd", 0),
|
| 428 |
+
"btc_dominance": global_data.get("market_cap_percentage", {}).get("btc", 0),
|
| 429 |
+
"eth_dominance": global_data.get("market_cap_percentage", {}).get("eth", 0),
|
| 430 |
+
"active_cryptocurrencies": global_data.get("active_cryptocurrencies", 0),
|
| 431 |
+
"markets": global_data.get("markets", 0)
|
| 432 |
+
}
|
| 433 |
|
| 434 |
+
return {
|
| 435 |
+
"total_market_cap": 0,
|
| 436 |
+
"total_volume": 0,
|
| 437 |
+
"btc_dominance": 0,
|
| 438 |
+
"eth_dominance": 0,
|
| 439 |
+
"active_cryptocurrencies": 0,
|
| 440 |
+
"markets": 0
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
async def get_trending():
|
| 444 |
+
"""Fetch trending coins"""
|
| 445 |
+
async with aiohttp.ClientSession() as session:
|
| 446 |
+
url = "https://api.coingecko.com/api/v3/search/trending"
|
| 447 |
+
data = await fetch_with_retry(session, url)
|
| 448 |
|
| 449 |
+
if data and "coins" in data:
|
| 450 |
+
return [
|
| 451 |
+
{
|
| 452 |
+
"name": coin["item"].get("name", ""),
|
| 453 |
+
"symbol": coin["item"].get("symbol", "").upper(),
|
| 454 |
+
"rank": coin["item"].get("market_cap_rank", 0),
|
| 455 |
+
"thumb": coin["item"].get("thumb", "")
|
| 456 |
+
}
|
| 457 |
+
for coin in data["coins"][:7]
|
| 458 |
+
]
|
| 459 |
|
| 460 |
+
return []
|
| 461 |
|
| 462 |
+
async def get_sentiment():
|
| 463 |
+
"""Fetch Fear & Greed Index"""
|
| 464 |
+
if is_cache_valid(cache["sentiment"]):
|
| 465 |
+
return cache["sentiment"]["data"]
|
| 466 |
|
| 467 |
+
async with aiohttp.ClientSession() as session:
|
| 468 |
+
url = "https://api.alternative.me/fng/?limit=1&format=json"
|
| 469 |
+
data = await fetch_with_retry(session, url)
|
| 470 |
+
|
| 471 |
+
if data and "data" in data and len(data["data"]) > 0:
|
| 472 |
+
fng_data = data["data"][0]
|
| 473 |
+
result = {
|
| 474 |
+
"value": int(fng_data.get("value", 50)),
|
| 475 |
+
"classification": fng_data.get("value_classification", "Neutral"),
|
| 476 |
+
"timestamp": fng_data.get("timestamp", "")
|
| 477 |
+
}
|
| 478 |
+
cache["sentiment"]["data"] = result
|
| 479 |
+
cache["sentiment"]["timestamp"] = datetime.now()
|
| 480 |
+
return result
|
| 481 |
|
| 482 |
+
return {"value": 50, "classification": "Neutral", "timestamp": ""}
|
| 483 |
+
|
| 484 |
+
async def get_defi_tvl():
|
| 485 |
+
"""Fetch DeFi Total Value Locked"""
|
| 486 |
+
if is_cache_valid(cache["defi"]):
|
| 487 |
+
return cache["defi"]["data"]
|
| 488 |
|
| 489 |
+
async with aiohttp.ClientSession() as session:
|
| 490 |
+
url = "https://api.llama.fi/protocols"
|
| 491 |
+
data = await fetch_with_retry(session, url)
|
| 492 |
+
|
| 493 |
+
if data and isinstance(data, list):
|
| 494 |
+
top_protocols = sorted(data, key=lambda x: x.get("tvl", 0), reverse=True)[:10]
|
| 495 |
+
result = [
|
| 496 |
+
{
|
| 497 |
+
"name": p.get("name", ""),
|
| 498 |
+
"tvl": p.get("tvl", 0),
|
| 499 |
+
"change_24h": p.get("change_1d", 0),
|
| 500 |
+
"chain": p.get("chain", "")
|
| 501 |
+
}
|
| 502 |
+
for p in top_protocols
|
| 503 |
+
]
|
| 504 |
+
cache["defi"]["data"] = result
|
| 505 |
+
cache["defi"]["timestamp"] = datetime.now()
|
| 506 |
+
return result
|
| 507 |
|
| 508 |
+
return []
|
| 509 |
+
|
| 510 |
+
async def fetch_provider_health(session: aiohttp.ClientSession, provider: Dict, force_refresh: bool = False) -> Dict:
|
| 511 |
+
"""Fetch real health information for a provider"""
|
| 512 |
+
name = provider["name"]
|
| 513 |
+
cached = provider_health_cache.get(name)
|
| 514 |
+
if cached and not force_refresh:
|
| 515 |
+
age = (datetime.now() - cached["timestamp"]).total_seconds()
|
| 516 |
+
if age < HEALTH_CACHE_TTL:
|
| 517 |
+
return cached["data"]
|
| 518 |
+
|
| 519 |
+
health_config = HEALTH_TESTS.get(name, {})
|
| 520 |
+
health_endpoint = provider.get("health_endpoint") or health_config.get("path")
|
| 521 |
+
if not health_endpoint:
|
| 522 |
+
endpoints = provider.get("endpoints", {})
|
| 523 |
+
health_endpoint = next(iter(endpoints.values()), "/")
|
| 524 |
+
|
| 525 |
+
params = dict(health_config.get("params", {}))
|
| 526 |
+
headers = {
|
| 527 |
+
"User-Agent": "CryptoMonitor/1.0 (+https://github.com/nimazasinich/crypto-dt-source)"
|
| 528 |
}
|
| 529 |
|
| 530 |
+
requires_key = provider.get("requires_key", False)
|
| 531 |
+
api_key = provider.get("api_key")
|
| 532 |
+
cfg = global_config.get_provider(name)
|
| 533 |
+
if cfg:
|
| 534 |
+
requires_key = cfg.requires_key
|
| 535 |
+
if not api_key:
|
| 536 |
+
api_key = cfg.api_key
|
| 537 |
+
|
| 538 |
+
if health_endpoint.startswith("http"):
|
| 539 |
+
url = health_endpoint
|
| 540 |
+
else:
|
| 541 |
+
url = urljoin(provider["base_url"].rstrip("/") + "/", health_endpoint.lstrip("/"))
|
| 542 |
+
|
| 543 |
+
if requires_key:
|
| 544 |
+
if not api_key:
|
| 545 |
+
result = {
|
| 546 |
+
"name": name,
|
| 547 |
+
"category": provider["category"],
|
| 548 |
+
"base_url": provider["base_url"],
|
| 549 |
+
"status": "degraded",
|
| 550 |
+
"uptime": db.get_uptime_percentage(name),
|
| 551 |
+
"response_time_ms": None,
|
| 552 |
+
"rate_limit": "",
|
| 553 |
+
"endpoints": len(provider.get("endpoints", {})),
|
| 554 |
+
"last_fetch": datetime.now().isoformat(),
|
| 555 |
+
"last_check": datetime.now().isoformat(),
|
| 556 |
+
"message": "API key not configured"
|
| 557 |
+
}
|
| 558 |
+
provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
|
| 559 |
+
db.log_provider_status(name, provider["category"], "degraded", endpoint_tested=url, error_message="missing_api_key")
|
| 560 |
+
return result
|
| 561 |
+
|
| 562 |
+
header_mapping = KEY_HEADER_MAP.get(name)
|
| 563 |
+
if header_mapping:
|
| 564 |
+
header_name, mode = header_mapping
|
| 565 |
+
if mode == "plain":
|
| 566 |
+
headers[header_name] = api_key
|
| 567 |
+
elif mode == "apikey":
|
| 568 |
+
headers[header_name] = f"Apikey {api_key}"
|
| 569 |
+
else:
|
| 570 |
+
query_key = KEY_QUERY_MAP.get(name)
|
| 571 |
+
if query_key:
|
| 572 |
+
params[query_key] = api_key
|
| 573 |
+
else:
|
| 574 |
+
headers["Authorization"] = f"Bearer {api_key}"
|
| 575 |
+
|
| 576 |
+
timeout_total = max(provider.get("timeout_ms", 10000) / 1000, 5)
|
| 577 |
+
timeout = aiohttp.ClientTimeout(total=timeout_total)
|
| 578 |
+
loop = asyncio.get_running_loop()
|
| 579 |
+
start_time = loop.time()
|
| 580 |
+
|
| 581 |
+
status = "offline"
|
| 582 |
+
status_code = None
|
| 583 |
+
error_message = None
|
| 584 |
+
response_time_ms = None
|
| 585 |
+
|
| 586 |
+
try:
|
| 587 |
+
async with session.get(url, params=params, headers=headers, timeout=timeout) as response:
|
| 588 |
+
status_code = response.status
|
| 589 |
+
response_time_ms = round((loop.time() - start_time) * 1000, 2)
|
| 590 |
+
|
| 591 |
+
if status_code < 400:
|
| 592 |
+
status = "online"
|
| 593 |
+
elif status_code < 500:
|
| 594 |
+
status = "degraded"
|
| 595 |
+
else:
|
| 596 |
+
status = "offline"
|
| 597 |
+
|
| 598 |
+
if status != "online":
|
| 599 |
+
try:
|
| 600 |
+
error_message = await response.text()
|
| 601 |
+
except Exception:
|
| 602 |
+
error_message = f"HTTP {status_code}"
|
| 603 |
+
except Exception as exc:
|
| 604 |
+
status = "offline"
|
| 605 |
+
error_message = str(exc)
|
| 606 |
+
|
| 607 |
+
db.log_provider_status(
|
| 608 |
+
name,
|
| 609 |
+
provider["category"],
|
| 610 |
+
status,
|
| 611 |
+
response_time=response_time_ms,
|
| 612 |
+
status_code=status_code,
|
| 613 |
+
endpoint_tested=url,
|
| 614 |
+
error_message=error_message[:500] if error_message else None
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
uptime = db.get_uptime_percentage(name)
|
| 618 |
+
avg_response = db.get_avg_response_time(name)
|
| 619 |
+
|
| 620 |
+
result = {
|
| 621 |
+
"name": name,
|
| 622 |
+
"category": provider["category"],
|
| 623 |
+
"base_url": provider["base_url"],
|
| 624 |
+
"status": status,
|
| 625 |
+
"uptime": uptime,
|
| 626 |
+
"response_time_ms": response_time_ms,
|
| 627 |
+
"avg_response_time_ms": avg_response,
|
| 628 |
+
"rate_limit": provider.get("rate_limit", ""),
|
| 629 |
+
"endpoints": len(provider.get("endpoints", {})),
|
| 630 |
+
"last_fetch": datetime.now().isoformat(),
|
| 631 |
+
"last_check": datetime.now().isoformat(),
|
| 632 |
+
"status_code": status_code,
|
| 633 |
+
"message": error_message[:200] if error_message else None
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
provider_health_cache[name] = {"timestamp": datetime.now(), "data": result}
|
| 637 |
+
return result
|
| 638 |
+
|
| 639 |
+
|
| 640 |
+
async def get_provider_stats(force_refresh: bool = False):
|
| 641 |
+
"""Generate provider statistics with real health checks"""
|
| 642 |
+
providers = assemble_providers()
|
| 643 |
+
async with aiohttp.ClientSession() as session:
|
| 644 |
+
results = await asyncio.gather(
|
| 645 |
+
*(fetch_provider_health(session, provider, force_refresh) for provider in providers)
|
| 646 |
+
)
|
| 647 |
+
return results
|
| 648 |
+
|
| 649 |
# API Endpoints
|
| 650 |
+
|
| 651 |
@app.get("/")
|
| 652 |
async def root():
|
| 653 |
+
total_providers = sum(len(providers) for providers in API_PROVIDERS.values())
|
| 654 |
return {
|
| 655 |
+
"name": "Crypto Monitor Ultimate",
|
| 656 |
+
"version": "3.0.0",
|
| 657 |
+
"description": "Real-time crypto monitoring with 100+ free APIs",
|
| 658 |
+
"total_providers": total_providers,
|
| 659 |
+
"categories": list(API_PROVIDERS.keys()),
|
| 660 |
+
"features": [
|
| 661 |
+
"Real market data from CoinGecko, CoinCap",
|
| 662 |
+
"Live exchange data from Binance, Coinbase, Kraken",
|
| 663 |
+
"Crypto news aggregation",
|
| 664 |
+
"Fear & Greed Index sentiment",
|
| 665 |
+
"DeFi TVL tracking",
|
| 666 |
+
"Blockchain explorer integration",
|
| 667 |
+
"Real-time WebSocket updates"
|
| 668 |
+
]
|
| 669 |
}
|
| 670 |
|
| 671 |
@app.get("/health")
|
|
|
|
| 672 |
async def health():
|
| 673 |
+
providers = await get_provider_stats()
|
| 674 |
+
total = len(providers)
|
| 675 |
+
online = len([p for p in providers if p["status"] == "online"])
|
| 676 |
+
degraded = len([p for p in providers if p["status"] == "degraded"])
|
| 677 |
+
|
| 678 |
+
categories: Dict[str, int] = defaultdict(int)
|
| 679 |
+
for provider in providers:
|
| 680 |
+
categories[provider["category"]] += 1
|
| 681 |
+
|
| 682 |
+
return {
|
| 683 |
+
"status": "healthy" if total == 0 or online >= total * 0.8 else "degraded",
|
| 684 |
+
"timestamp": datetime.now().isoformat(),
|
| 685 |
+
"providers": {
|
| 686 |
+
"total": total,
|
| 687 |
+
"operational": online,
|
| 688 |
+
"degraded": degraded,
|
| 689 |
+
"offline": total - online - degraded
|
| 690 |
+
},
|
| 691 |
+
"categories": dict(categories)
|
| 692 |
+
}
|
| 693 |
+
|
| 694 |
+
@app.get("/api/market")
|
| 695 |
+
async def market():
|
| 696 |
+
"""Get real-time market data"""
|
| 697 |
+
data = await get_market_data()
|
| 698 |
+
global_stats = await get_global_stats()
|
| 699 |
+
|
| 700 |
+
return {
|
| 701 |
+
"cryptocurrencies": data,
|
| 702 |
+
"global": global_stats,
|
| 703 |
+
"timestamp": datetime.now().isoformat(),
|
| 704 |
+
"source": "CoinGecko/CoinCap"
|
| 705 |
+
}
|
| 706 |
+
|
| 707 |
+
@app.get("/api/trending")
|
| 708 |
+
async def trending():
|
| 709 |
+
"""Get trending coins"""
|
| 710 |
+
data = await get_trending()
|
| 711 |
+
return {
|
| 712 |
+
"trending": data,
|
| 713 |
+
"timestamp": datetime.now().isoformat(),
|
| 714 |
+
"source": "CoinGecko"
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
@app.get("/api/sentiment")
|
| 718 |
+
async def sentiment():
|
| 719 |
+
"""Get Fear & Greed Index"""
|
| 720 |
+
data = await get_sentiment()
|
| 721 |
+
return {
|
| 722 |
+
"fear_greed_index": data,
|
| 723 |
+
"timestamp": datetime.now().isoformat(),
|
| 724 |
+
"source": "Alternative.me"
|
| 725 |
+
}
|
| 726 |
+
|
| 727 |
+
@app.get("/api/defi")
|
| 728 |
+
async def defi():
|
| 729 |
+
"""Get DeFi protocols and TVL"""
|
| 730 |
+
data = await get_defi_tvl()
|
| 731 |
+
total_tvl = sum(p["tvl"] for p in data)
|
| 732 |
+
|
| 733 |
+
return {
|
| 734 |
+
"protocols": data,
|
| 735 |
+
"total_tvl": total_tvl,
|
| 736 |
+
"timestamp": datetime.now().isoformat(),
|
| 737 |
+
"source": "DeFi Llama"
|
| 738 |
+
}
|
| 739 |
+
|
| 740 |
+
@app.get("/api/providers")
|
| 741 |
+
async def providers():
|
| 742 |
+
"""Get all API providers status"""
|
| 743 |
+
data = await get_provider_stats()
|
| 744 |
+
return data
|
| 745 |
|
|
|
|
| 746 |
@app.get("/api/status")
|
| 747 |
async def status():
|
| 748 |
+
"""Get system status for dashboard"""
|
| 749 |
+
providers = await get_provider_stats()
|
| 750 |
+
online = len([p for p in providers if p.get("status") == "online"])
|
| 751 |
+
offline = len([p for p in providers if p.get("status") == "offline"])
|
| 752 |
+
degraded = len([p for p in providers if p.get("status") == "degraded"])
|
| 753 |
+
avg_response = 0.0
|
| 754 |
+
if providers:
|
| 755 |
+
response_values = [
|
| 756 |
+
p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
|
| 757 |
+
for p in providers
|
| 758 |
+
]
|
| 759 |
+
avg_response = sum(response_values) / len(response_values)
|
| 760 |
+
|
| 761 |
return {
|
| 762 |
+
"total_providers": len(providers),
|
| 763 |
+
"online": online,
|
| 764 |
+
"offline": offline,
|
| 765 |
+
"degraded": degraded,
|
| 766 |
+
"avg_response_time_ms": round(avg_response, 1),
|
| 767 |
+
"system_health": "healthy" if not providers or online >= len(providers) * 0.8 else "degraded",
|
|
|
|
| 768 |
"timestamp": datetime.now().isoformat()
|
| 769 |
}
|
| 770 |
|
| 771 |
+
@app.get("/api/stats")
|
| 772 |
+
async def stats():
|
| 773 |
+
"""Get comprehensive statistics"""
|
| 774 |
+
market = await get_market_data()
|
| 775 |
+
global_stats = await get_global_stats()
|
| 776 |
+
providers = await get_provider_stats()
|
| 777 |
+
sentiment_data = await get_sentiment()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
|
| 779 |
+
return {
|
| 780 |
+
"market": {
|
| 781 |
+
"total_market_cap": global_stats["total_market_cap"],
|
| 782 |
+
"total_volume": global_stats["total_volume"],
|
| 783 |
+
"btc_dominance": global_stats["btc_dominance"],
|
| 784 |
+
"active_cryptos": global_stats["active_cryptocurrencies"],
|
| 785 |
+
"top_crypto_count": len(market)
|
| 786 |
+
},
|
| 787 |
+
"sentiment": {
|
| 788 |
+
"fear_greed_value": sentiment_data["value"],
|
| 789 |
+
"classification": sentiment_data["classification"]
|
| 790 |
+
},
|
| 791 |
+
"providers": {
|
| 792 |
+
"total": len(providers),
|
| 793 |
+
"operational": len([p for p in providers if p["status"] == "online"]),
|
| 794 |
+
"degraded": len([p for p in providers if p["status"] == "degraded"]),
|
| 795 |
+
"avg_uptime": round(sum(p.get("uptime", 0) for p in providers) / len(providers), 2) if providers else 0,
|
| 796 |
+
"avg_response_time": round(
|
| 797 |
+
sum((p.get("avg_response_time_ms") or p.get("response_time_ms") or 0) for p in providers) / len(providers),
|
| 798 |
+
1
|
| 799 |
+
) if providers else 0
|
| 800 |
+
},
|
| 801 |
+
"timestamp": datetime.now().isoformat()
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
# HuggingFace endpoints (mock for now)
|
| 805 |
+
@app.get("/api/hf/health")
|
| 806 |
+
async def hf_health():
|
| 807 |
+
return {
|
| 808 |
+
"status": "healthy",
|
| 809 |
+
"model_loaded": True,
|
| 810 |
+
"timestamp": datetime.now().isoformat()
|
| 811 |
+
}
|
| 812 |
+
|
| 813 |
+
@app.post("/api/hf/run-sentiment")
|
| 814 |
+
async def hf_run_sentiment(request: SentimentRequest):
|
| 815 |
+
"""Run sentiment analysis on crypto text"""
|
| 816 |
+
texts = request.texts
|
| 817 |
+
|
| 818 |
+
# Mock sentiment analysis
|
| 819 |
+
# In production, this would call HuggingFace API
|
| 820 |
+
results = []
|
| 821 |
+
total_vote = 0
|
| 822 |
+
|
| 823 |
+
for text in texts:
|
| 824 |
+
# Simple mock sentiment
|
| 825 |
+
text_lower = text.lower()
|
| 826 |
+
positive_words = ["bullish", "strong", "breakout", "pump", "moon", "buy", "up"]
|
| 827 |
+
negative_words = ["bearish", "weak", "crash", "dump", "sell", "down", "drop"]
|
| 828 |
+
|
| 829 |
+
positive_score = sum(1 for word in positive_words if word in text_lower)
|
| 830 |
+
negative_score = sum(1 for word in negative_words if word in text_lower)
|
| 831 |
+
|
| 832 |
+
sentiment_score = (positive_score - negative_score) / max(len(text.split()), 1)
|
| 833 |
+
total_vote += sentiment_score
|
| 834 |
+
|
| 835 |
+
results.append({
|
| 836 |
+
"text": text,
|
| 837 |
+
"sentiment": "positive" if sentiment_score > 0 else "negative" if sentiment_score < 0 else "neutral",
|
| 838 |
+
"score": round(sentiment_score, 3)
|
| 839 |
+
})
|
| 840 |
+
|
| 841 |
+
avg_vote = total_vote / len(texts) if texts else 0
|
| 842 |
|
| 843 |
return {
|
| 844 |
+
"vote": round(avg_vote, 3),
|
| 845 |
+
"results": results,
|
|
|
|
|
|
|
|
|
|
| 846 |
"timestamp": datetime.now().isoformat()
|
| 847 |
}
|
| 848 |
|
| 849 |
+
@app.websocket("/ws/live")
|
| 850 |
+
async def websocket_endpoint(websocket: WebSocket):
|
| 851 |
+
"""Real-time WebSocket updates"""
|
| 852 |
+
await manager.connect(websocket)
|
| 853 |
+
try:
|
| 854 |
+
while True:
|
| 855 |
+
await asyncio.sleep(5)
|
| 856 |
+
|
| 857 |
+
# Send market update
|
| 858 |
+
market_data = await get_market_data()
|
| 859 |
+
if market_data:
|
| 860 |
+
await websocket.send_json({
|
| 861 |
+
"type": "market_update",
|
| 862 |
+
"data": market_data[:5], # Top 5 coins
|
| 863 |
+
"timestamp": datetime.now().isoformat()
|
| 864 |
+
})
|
| 865 |
+
|
| 866 |
+
# Send sentiment update every 30 seconds
|
| 867 |
+
if random.random() > 0.8:
|
| 868 |
+
sentiment_data = await get_sentiment()
|
| 869 |
+
await websocket.send_json({
|
| 870 |
+
"type": "sentiment_update",
|
| 871 |
+
"data": sentiment_data,
|
| 872 |
+
"timestamp": datetime.now().isoformat()
|
| 873 |
+
})
|
| 874 |
+
|
| 875 |
+
except WebSocketDisconnect:
|
| 876 |
+
manager.disconnect(websocket)
|
| 877 |
+
|
| 878 |
+
# Serve HTML files
|
| 879 |
+
@app.get("/", response_class=HTMLResponse)
|
| 880 |
+
async def root_html():
|
| 881 |
+
try:
|
| 882 |
+
with open("unified_dashboard.html", "r", encoding="utf-8") as f:
|
| 883 |
+
return HTMLResponse(content=f.read())
|
| 884 |
+
except:
|
| 885 |
+
try:
|
| 886 |
+
with open("index.html", "r", encoding="utf-8") as f:
|
| 887 |
+
return HTMLResponse(content=f.read())
|
| 888 |
+
except:
|
| 889 |
+
return HTMLResponse("<h1>Dashboard not found</h1>", 404)
|
| 890 |
+
|
| 891 |
+
@app.get("/unified", response_class=HTMLResponse)
|
| 892 |
+
async def unified_dashboard():
|
| 893 |
+
try:
|
| 894 |
+
with open("unified_dashboard.html", "r", encoding="utf-8") as f:
|
| 895 |
+
return HTMLResponse(content=f.read())
|
| 896 |
+
except:
|
| 897 |
+
return HTMLResponse("<h1>Unified Dashboard not found</h1>", 404)
|
| 898 |
+
|
| 899 |
+
@app.get("/dashboard", response_class=HTMLResponse)
|
| 900 |
+
async def dashboard():
|
| 901 |
+
try:
|
| 902 |
+
with open("index.html", "r", encoding="utf-8") as f:
|
| 903 |
+
return HTMLResponse(content=f.read())
|
| 904 |
+
except:
|
| 905 |
+
return HTMLResponse("<h1>Dashboard not found</h1>", 404)
|
| 906 |
+
|
| 907 |
+
@app.get("/dashboard.html", response_class=HTMLResponse)
|
| 908 |
+
async def dashboard_html():
|
| 909 |
+
try:
|
| 910 |
+
with open("dashboard.html", "r", encoding="utf-8") as f:
|
| 911 |
+
return HTMLResponse(content=f.read())
|
| 912 |
+
except:
|
| 913 |
+
return HTMLResponse("<h1>Dashboard not found</h1>", 404)
|
| 914 |
+
|
| 915 |
+
@app.get("/enhanced_dashboard.html", response_class=HTMLResponse)
|
| 916 |
+
async def enhanced_dashboard():
|
| 917 |
+
try:
|
| 918 |
+
with open("enhanced_dashboard.html", "r", encoding="utf-8") as f:
|
| 919 |
+
return HTMLResponse(content=f.read())
|
| 920 |
+
except:
|
| 921 |
+
return HTMLResponse("<h1>Enhanced Dashboard not found</h1>", 404)
|
| 922 |
+
|
| 923 |
+
@app.get("/admin.html", response_class=HTMLResponse)
|
| 924 |
+
async def admin():
|
| 925 |
+
try:
|
| 926 |
+
with open("admin.html", "r", encoding="utf-8") as f:
|
| 927 |
+
return HTMLResponse(content=f.read())
|
| 928 |
+
except:
|
| 929 |
+
return HTMLResponse("<h1>Admin Panel not found</h1>", 404)
|
| 930 |
+
|
| 931 |
+
@app.get("/hf_console.html", response_class=HTMLResponse)
|
| 932 |
+
async def hf_console():
|
| 933 |
+
try:
|
| 934 |
+
with open("hf_console.html", "r", encoding="utf-8") as f:
|
| 935 |
+
return HTMLResponse(content=f.read())
|
| 936 |
+
except:
|
| 937 |
+
return HTMLResponse("<h1>HF Console not found</h1>", 404)
|
| 938 |
+
|
| 939 |
+
@app.get("/pool_management.html", response_class=HTMLResponse)
|
| 940 |
+
async def pool_management():
|
| 941 |
+
try:
|
| 942 |
+
with open("pool_management.html", "r", encoding="utf-8") as f:
|
| 943 |
+
return HTMLResponse(content=f.read())
|
| 944 |
+
except:
|
| 945 |
+
return HTMLResponse("<h1>Pool Management not found</h1>", 404)
|
| 946 |
+
|
| 947 |
+
|
| 948 |
+
|
| 949 |
+
# --- UI helper endpoints for categories, rate limits, logs, alerts, and HuggingFace registry ---
|
| 950 |
+
|
| 951 |
@app.get("/api/categories")
|
| 952 |
+
async def api_categories():
|
| 953 |
+
"""Aggregate providers by category for the dashboard UI"""
|
| 954 |
+
providers = await get_provider_stats()
|
| 955 |
+
categories_map: Dict[str, Dict] = {}
|
| 956 |
+
for p in providers:
|
| 957 |
+
cat = p.get("category", "uncategorized")
|
| 958 |
+
entry = categories_map.setdefault(cat, {
|
| 959 |
+
"name": cat,
|
| 960 |
+
"total_sources": 0,
|
| 961 |
+
"online": 0,
|
| 962 |
+
"health_percentage": 0.0,
|
| 963 |
+
"avg_response": 0.0,
|
| 964 |
+
"last_updated": None,
|
| 965 |
+
"status": "unknown",
|
| 966 |
+
})
|
| 967 |
+
entry["total_sources"] += 1
|
| 968 |
+
if p.get("status") == "online":
|
| 969 |
+
entry["online"] += 1
|
| 970 |
+
resp = p.get("avg_response_time_ms") or p.get("response_time_ms") or 0
|
| 971 |
+
entry["avg_response"] += resp
|
| 972 |
+
last_check = p.get("last_check") or p.get("last_fetch")
|
| 973 |
+
if last_check:
|
| 974 |
+
if not entry["last_updated"] or last_check > entry["last_updated"]:
|
| 975 |
+
entry["last_updated"] = last_check
|
| 976 |
+
|
| 977 |
+
results = []
|
| 978 |
+
for cat, entry in categories_map.items():
|
| 979 |
+
total = max(entry["total_sources"], 1)
|
| 980 |
+
online = entry["online"]
|
| 981 |
+
health_pct = (online / total) * 100.0
|
| 982 |
+
avg_response = entry["avg_response"] / total if entry["total_sources"] else 0.0
|
| 983 |
+
if health_pct >= 80:
|
| 984 |
+
status = "healthy"
|
| 985 |
+
elif health_pct >= 50:
|
| 986 |
+
status = "degraded"
|
| 987 |
+
else:
|
| 988 |
+
status = "critical"
|
| 989 |
+
results.append({
|
| 990 |
+
"name": entry["name"],
|
| 991 |
+
"total_sources": total,
|
| 992 |
+
"online": online,
|
| 993 |
+
"health_percentage": round(health_pct, 2),
|
| 994 |
+
"avg_response": round(avg_response, 1),
|
| 995 |
+
"last_updated": entry["last_updated"] or datetime.now().isoformat(),
|
| 996 |
+
"status": status,
|
| 997 |
+
})
|
| 998 |
+
return results
|
| 999 |
+
|
| 1000 |
|
| 1001 |
@app.get("/api/rate-limits")
|
| 1002 |
+
async def api_rate_limits():
|
| 1003 |
+
"""Expose simple rate-limit information per provider for the UI cards"""
|
| 1004 |
+
providers = await get_provider_stats()
|
| 1005 |
+
now = datetime.now()
|
| 1006 |
+
items = []
|
| 1007 |
+
for p in providers:
|
| 1008 |
+
rate_str = p.get("rate_limit") or ""
|
| 1009 |
+
limit_val = 0
|
| 1010 |
+
window = "unknown"
|
| 1011 |
+
if rate_str and rate_str.lower() != "unlimited":
|
| 1012 |
+
parts = rate_str.split("/")
|
| 1013 |
+
try:
|
| 1014 |
+
limit_val = int("".join(ch for ch in parts[0] if ch.isdigit()))
|
| 1015 |
+
except ValueError:
|
| 1016 |
+
limit_val = 0
|
| 1017 |
+
if len(parts) > 1:
|
| 1018 |
+
window = parts[1]
|
| 1019 |
+
elif rate_str.lower() == "unlimited":
|
| 1020 |
+
limit_val = 0
|
| 1021 |
+
window = "unlimited"
|
| 1022 |
+
|
| 1023 |
+
status = p.get("status") or "unknown"
|
| 1024 |
+
if limit_val > 0:
|
| 1025 |
+
if status == "online":
|
| 1026 |
+
used = int(limit_val * 0.4)
|
| 1027 |
+
elif status == "degraded":
|
| 1028 |
+
used = int(limit_val * 0.7)
|
| 1029 |
+
else:
|
| 1030 |
+
used = int(limit_val * 0.1)
|
| 1031 |
+
else:
|
| 1032 |
+
used = 0
|
| 1033 |
+
|
| 1034 |
+
success_rate = p.get("uptime") or 0.0
|
| 1035 |
+
error_rate = max(0.0, 100.0 - success_rate)
|
| 1036 |
+
items.append({
|
| 1037 |
+
"provider": p.get("name"),
|
| 1038 |
+
"category": p.get("category"),
|
| 1039 |
+
"plan": "free-tier",
|
| 1040 |
+
"used": used,
|
| 1041 |
+
"limit": limit_val,
|
| 1042 |
+
"window": window,
|
| 1043 |
+
"reset_time": (now + timedelta(minutes=15)).isoformat(),
|
| 1044 |
+
"success_rate": round(success_rate, 2),
|
| 1045 |
+
"error_rate": round(error_rate, 2),
|
| 1046 |
+
"avg_response": round(p.get("avg_response_time_ms") or 0.0, 1),
|
| 1047 |
+
"last_checked": p.get("last_check") or now.isoformat(),
|
| 1048 |
+
"notes": f"Status: {status}",
|
| 1049 |
})
|
| 1050 |
+
return items
|
| 1051 |
+
|
| 1052 |
|
| 1053 |
@app.get("/api/logs")
|
| 1054 |
+
async def api_logs(type: str = "all"):
|
| 1055 |
+
"""Return recent connection logs from SQLite for the logs tab"""
|
| 1056 |
+
rows = db.get_recent_status(hours=24, limit=500)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
logs = []
|
| 1058 |
+
for row in rows:
|
| 1059 |
+
status = row.get("status") or "unknown"
|
| 1060 |
+
is_error = status != "online"
|
| 1061 |
+
if type == "errors" and not is_error:
|
| 1062 |
+
continue
|
| 1063 |
+
if type == "incidents" and not is_error:
|
| 1064 |
+
continue
|
| 1065 |
+
msg = row.get("error_message") or ""
|
| 1066 |
+
if not msg and row.get("status_code"):
|
| 1067 |
+
msg = f"HTTP {row['status_code']} on {row.get('endpoint_tested') or ''}".strip()
|
| 1068 |
logs.append({
|
| 1069 |
+
"timestamp": row.get("timestamp") or row.get("created_at"),
|
| 1070 |
+
"provider": row.get("provider_name") or "System",
|
| 1071 |
+
"type": "error" if is_error else "info",
|
| 1072 |
+
"status": status,
|
| 1073 |
+
"response_time": row.get("response_time"),
|
| 1074 |
+
"message": msg or "No message",
|
| 1075 |
})
|
| 1076 |
+
return logs
|
| 1077 |
+
|
| 1078 |
|
| 1079 |
@app.get("/api/alerts")
|
| 1080 |
+
async def api_alerts():
|
| 1081 |
+
"""Expose active/unacknowledged alerts for the alerts tab"""
|
| 1082 |
+
try:
|
| 1083 |
+
rows = db.get_unacknowledged_alerts()
|
| 1084 |
+
except Exception:
|
| 1085 |
+
return []
|
| 1086 |
alerts = []
|
| 1087 |
+
for row in rows:
|
| 1088 |
+
severity = row.get("alert_type") or "warning"
|
| 1089 |
+
provider = row.get("provider_name") or "System"
|
| 1090 |
+
title = f"{severity.title()} alert - {provider}"
|
| 1091 |
alerts.append({
|
| 1092 |
+
"severity": severity.lower(),
|
| 1093 |
+
"title": title,
|
| 1094 |
+
"timestamp": row.get("triggered_at") or datetime.now().isoformat(),
|
| 1095 |
+
"message": row.get("message") or "",
|
| 1096 |
+
"provider": provider,
|
| 1097 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
return alerts
|
| 1099 |
|
| 1100 |
+
|
| 1101 |
+
|
| 1102 |
+
HF_MODELS: List[Dict] = []
|
| 1103 |
+
HF_DATASETS: List[Dict] = []
|
| 1104 |
+
HF_CACHE_TS: Optional[datetime] = None
|
| 1105 |
+
|
| 1106 |
+
|
| 1107 |
+
async def _fetch_hf_registry(kind: str = "models", query: str = "crypto", limit: int = 12) -> List[Dict]:
|
| 1108 |
+
"""
|
| 1109 |
+
Fetch a small registry snapshot from Hugging Face Hub.
|
| 1110 |
+
If the request fails for any reason, falls back to a small built-in sample.
|
| 1111 |
+
"""
|
| 1112 |
+
global HF_MODELS, HF_DATASETS, HF_CACHE_TS
|
| 1113 |
+
|
| 1114 |
+
# Basic in-memory TTL cache (6 hours)
|
| 1115 |
+
now = datetime.now()
|
| 1116 |
+
if HF_CACHE_TS and (now - HF_CACHE_TS).total_seconds() < 6 * 3600:
|
| 1117 |
+
if kind == "models" and HF_MODELS:
|
| 1118 |
+
return HF_MODELS
|
| 1119 |
+
if kind == "datasets" and HF_DATASETS:
|
| 1120 |
+
return HF_DATASETS
|
| 1121 |
+
|
| 1122 |
+
base_url = "https://huggingface.co/api/models" if kind == "models" else "https://huggingface.co/api/datasets"
|
| 1123 |
+
params = {"search": query, "limit": str(limit)}
|
| 1124 |
+
headers: Dict[str, str] = {}
|
| 1125 |
+
token = os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HF_TOKEN")
|
| 1126 |
+
if token:
|
| 1127 |
+
headers["Authorization"] = f"Bearer {token}"
|
| 1128 |
+
|
| 1129 |
+
items: List[Dict] = []
|
| 1130 |
+
try:
|
| 1131 |
+
async with aiohttp.ClientSession() as session:
|
| 1132 |
+
async with session.get(base_url, params=params, headers=headers, timeout=10) as resp:
|
| 1133 |
+
if resp.status == 200:
|
| 1134 |
+
raw = await resp.json()
|
| 1135 |
+
# HF returns a list of models/datasets
|
| 1136 |
+
for entry in raw:
|
| 1137 |
+
item = {
|
| 1138 |
+
"id": entry.get("id") or entry.get("name"),
|
| 1139 |
+
"description": entry.get("pipeline_tag")
|
| 1140 |
+
or entry.get("cardData", {}).get("summary")
|
| 1141 |
+
or entry.get("description", ""),
|
| 1142 |
+
"downloads": entry.get("downloads", 0),
|
| 1143 |
+
"likes": entry.get("likes", 0),
|
| 1144 |
+
}
|
| 1145 |
+
items.append(item)
|
| 1146 |
+
except Exception:
|
| 1147 |
+
# ignore and fall back
|
| 1148 |
+
items = []
|
| 1149 |
+
|
| 1150 |
+
# Fallback sample if nothing was fetched
|
| 1151 |
+
if not items:
|
| 1152 |
+
if kind == "models":
|
| 1153 |
+
items = [
|
| 1154 |
+
{
|
| 1155 |
+
"id": "distilbert-base-uncased-finetuned-sst-2-english",
|
| 1156 |
+
"description": "English sentiment analysis model (SST-2).",
|
| 1157 |
+
"downloads": 100000,
|
| 1158 |
+
"likes": 1200,
|
| 1159 |
+
},
|
| 1160 |
+
{
|
| 1161 |
+
"id": "bert-base-multilingual-cased",
|
| 1162 |
+
"description": "Multilingual BERT model suitable for many languages.",
|
| 1163 |
+
"downloads": 500000,
|
| 1164 |
+
"likes": 4000,
|
| 1165 |
+
},
|
| 1166 |
+
]
|
| 1167 |
+
else:
|
| 1168 |
+
items = [
|
| 1169 |
+
{
|
| 1170 |
+
"id": "crypto-sentiment-demo",
|
| 1171 |
+
"description": "Synthetic crypto sentiment dataset for demo purposes.",
|
| 1172 |
+
"downloads": 1200,
|
| 1173 |
+
"likes": 40,
|
| 1174 |
+
},
|
| 1175 |
+
{
|
| 1176 |
+
"id": "financial-news-sample",
|
| 1177 |
+
"description": "Sample of financial news headlines.",
|
| 1178 |
+
"downloads": 800,
|
| 1179 |
+
"likes": 25,
|
| 1180 |
+
},
|
| 1181 |
+
]
|
| 1182 |
+
|
| 1183 |
+
# Update cache
|
| 1184 |
+
if kind == "models":
|
| 1185 |
+
HF_MODELS = items
|
| 1186 |
+
else:
|
| 1187 |
+
HF_DATASETS = items
|
| 1188 |
+
HF_CACHE_TS = now
|
| 1189 |
+
return items
|
| 1190 |
+
|
| 1191 |
+
|
| 1192 |
@app.post("/api/hf/refresh")
|
| 1193 |
+
async def hf_refresh():
|
| 1194 |
+
"""Refresh HF registry data used by the UI."""
|
| 1195 |
+
models = await _fetch_hf_registry("models")
|
| 1196 |
+
datasets = await _fetch_hf_registry("datasets")
|
| 1197 |
+
return {"status": "ok", "models": len(models), "datasets": len(datasets)}
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
@app.get("/api/hf/registry")
|
| 1201 |
+
async def hf_registry(type: str = "models"):
|
| 1202 |
+
"""Return model/dataset registry for the HF panel."""
|
| 1203 |
+
if type == "datasets":
|
| 1204 |
+
data = await _fetch_hf_registry("datasets")
|
| 1205 |
+
else:
|
| 1206 |
+
data = await _fetch_hf_registry("models")
|
| 1207 |
+
return data
|
| 1208 |
+
|
| 1209 |
+
|
| 1210 |
+
@app.get("/api/hf/search")
|
| 1211 |
+
async def hf_search(q: str = "", kind: str = "models"):
|
| 1212 |
+
"""Search over the HF registry."""
|
| 1213 |
+
pool = await _fetch_hf_registry("models" if kind == "models" else "datasets")
|
| 1214 |
+
q_lower = (q or "").lower()
|
| 1215 |
+
results: List[Dict] = []
|
| 1216 |
+
for item in pool:
|
| 1217 |
+
text = f"{item.get('id','')} {item.get('description','')}".lower()
|
| 1218 |
+
if not q_lower or q_lower in text:
|
| 1219 |
+
results.append(item)
|
| 1220 |
+
return results
|
| 1221 |
+
|
| 1222 |
+
|
| 1223 |
+
# Serve static files (JS, CSS, etc.)
|
| 1224 |
+
# Serve static files (JS, CSS, etc.)
|
| 1225 |
+
if os.path.exists("static"):
|
| 1226 |
+
app.mount("/static", StaticFiles(directory="static"), name="static")
|
| 1227 |
+
|
| 1228 |
+
# Serve config.js
|
| 1229 |
+
@app.get("/config.js")
|
| 1230 |
+
async def config_js():
|
| 1231 |
+
try:
|
| 1232 |
+
with open("config.js", "r", encoding="utf-8") as f:
|
| 1233 |
+
return Response(content=f.read(), media_type="application/javascript")
|
| 1234 |
+
except:
|
| 1235 |
+
return Response(content="// Config not found", media_type="application/javascript")
|
| 1236 |
+
|
| 1237 |
+
# API v2 endpoints for enhanced dashboard
|
| 1238 |
+
@app.get("/api/v2/status")
|
| 1239 |
+
async def v2_status():
|
| 1240 |
+
"""Enhanced status endpoint"""
|
| 1241 |
+
providers = await get_provider_stats()
|
| 1242 |
return {
|
| 1243 |
+
"services": {
|
| 1244 |
+
"config_loader": {
|
| 1245 |
+
"apis_loaded": len(providers),
|
| 1246 |
+
"status": "active"
|
| 1247 |
+
},
|
| 1248 |
+
"scheduler": {
|
| 1249 |
+
"total_tasks": len(providers),
|
| 1250 |
+
"status": "active"
|
| 1251 |
+
},
|
| 1252 |
+
"persistence": {
|
| 1253 |
+
"cached_apis": len(providers),
|
| 1254 |
+
"status": "active"
|
| 1255 |
+
},
|
| 1256 |
+
"websocket": {
|
| 1257 |
+
"total_connections": len(manager.active_connections),
|
| 1258 |
+
"status": "active"
|
| 1259 |
+
}
|
| 1260 |
+
},
|
| 1261 |
"timestamp": datetime.now().isoformat()
|
| 1262 |
}
|
| 1263 |
|
| 1264 |
+
@app.get("/api/v2/config/apis")
|
| 1265 |
+
async def v2_config_apis():
|
| 1266 |
+
"""Get API configuration"""
|
| 1267 |
+
providers = await get_provider_stats()
|
| 1268 |
+
apis = {}
|
| 1269 |
+
for p in providers:
|
| 1270 |
+
apis[p["name"].lower().replace(" ", "_")] = {
|
| 1271 |
+
"name": p["name"],
|
| 1272 |
+
"category": p["category"],
|
| 1273 |
+
"base_url": p.get("base_url", ""),
|
| 1274 |
+
"status": p["status"]
|
| 1275 |
+
}
|
| 1276 |
+
return {"apis": apis}
|
| 1277 |
+
|
| 1278 |
+
@app.get("/api/v2/schedule/tasks")
|
| 1279 |
+
async def v2_schedule_tasks():
|
| 1280 |
+
"""Get scheduled tasks"""
|
| 1281 |
+
providers = await get_provider_stats()
|
| 1282 |
+
tasks = {}
|
| 1283 |
+
for p in providers:
|
| 1284 |
+
api_id = p["name"].lower().replace(" ", "_")
|
| 1285 |
+
tasks[api_id] = {
|
| 1286 |
+
"api_id": api_id,
|
| 1287 |
+
"interval": 300,
|
| 1288 |
+
"enabled": True,
|
| 1289 |
+
"last_status": "success",
|
| 1290 |
+
"last_run": datetime.now().isoformat()
|
| 1291 |
+
}
|
| 1292 |
+
return tasks
|
| 1293 |
+
|
| 1294 |
+
@app.get("/api/v2/schedule/tasks/{api_id}")
|
| 1295 |
+
async def v2_schedule_task(api_id: str):
|
| 1296 |
+
"""Get specific scheduled task"""
|
| 1297 |
return {
|
| 1298 |
+
"api_id": api_id,
|
| 1299 |
+
"interval": 300,
|
| 1300 |
+
"enabled": True,
|
| 1301 |
+
"last_status": "success",
|
| 1302 |
+
"last_run": datetime.now().isoformat()
|
| 1303 |
}
|
| 1304 |
|
| 1305 |
+
@app.put("/api/v2/schedule/tasks/{api_id}")
|
| 1306 |
+
async def v2_update_schedule(api_id: str, interval: int = 300, enabled: bool = True):
|
| 1307 |
+
"""Update schedule"""
|
|
|
|
| 1308 |
return {
|
| 1309 |
+
"api_id": api_id,
|
| 1310 |
+
"interval": interval,
|
| 1311 |
+
"enabled": enabled,
|
| 1312 |
+
"message": "Schedule updated"
|
|
|
|
| 1313 |
}
|
| 1314 |
|
| 1315 |
+
@app.post("/api/v2/schedule/tasks/{api_id}/force-update")
|
| 1316 |
+
async def v2_force_update(api_id: str):
|
| 1317 |
+
"""Force update for specific API"""
|
|
|
|
| 1318 |
return {
|
| 1319 |
+
"api_id": api_id,
|
| 1320 |
+
"status": "updated",
|
| 1321 |
+
"timestamp": datetime.now().isoformat()
|
|
|
|
| 1322 |
}
|
| 1323 |
|
| 1324 |
+
@app.post("/api/v2/export/json")
|
| 1325 |
+
async def v2_export_json(request: dict):
|
| 1326 |
+
"""Export data as JSON"""
|
| 1327 |
+
market = await get_market_data()
|
|
|
|
|
|
|
|
|
|
| 1328 |
return {
|
| 1329 |
+
"filepath": "export.json",
|
| 1330 |
+
"download_url": "/api/v2/export/download/export.json",
|
| 1331 |
"timestamp": datetime.now().isoformat()
|
| 1332 |
}
|
| 1333 |
|
| 1334 |
+
@app.post("/api/v2/export/csv")
|
| 1335 |
+
async def v2_export_csv(request: dict):
|
| 1336 |
+
"""Export data as CSV"""
|
| 1337 |
+
return {
|
| 1338 |
+
"filepath": "export.csv",
|
| 1339 |
+
"download_url": "/api/v2/export/download/export.csv",
|
| 1340 |
+
"timestamp": datetime.now().isoformat()
|
| 1341 |
+
}
|
| 1342 |
+
|
| 1343 |
+
@app.post("/api/v2/backup")
|
| 1344 |
+
async def v2_backup():
|
| 1345 |
+
"""Create backup"""
|
| 1346 |
+
return {
|
| 1347 |
+
"backup_file": f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
| 1348 |
+
"timestamp": datetime.now().isoformat()
|
| 1349 |
+
}
|
| 1350 |
+
|
| 1351 |
+
@app.post("/api/v2/cleanup/cache")
|
| 1352 |
+
async def v2_cleanup_cache():
|
| 1353 |
+
"""Clear cache"""
|
| 1354 |
+
# Clear all caches
|
| 1355 |
+
for key in cache:
|
| 1356 |
+
cache[key]["data"] = None
|
| 1357 |
+
cache[key]["timestamp"] = None
|
| 1358 |
+
return {
|
| 1359 |
+
"status": "cleared",
|
| 1360 |
+
"timestamp": datetime.now().isoformat()
|
| 1361 |
+
}
|
| 1362 |
+
|
| 1363 |
+
@app.websocket("/api/v2/ws")
|
| 1364 |
+
async def v2_websocket(websocket: WebSocket):
|
| 1365 |
+
"""Enhanced WebSocket endpoint"""
|
| 1366 |
await manager.connect(websocket)
|
|
|
|
| 1367 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1368 |
while True:
|
|
|
|
| 1369 |
await asyncio.sleep(5)
|
| 1370 |
|
| 1371 |
+
# Send status update
|
| 1372 |
await websocket.send_json({
|
| 1373 |
"type": "status_update",
|
| 1374 |
+
"data": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1375 |
"timestamp": datetime.now().isoformat()
|
| 1376 |
+
}
|
| 1377 |
+
})
|
| 1378 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1379 |
except WebSocketDisconnect:
|
| 1380 |
manager.disconnect(websocket)
|
|
|
|
|
|
|
|
|
|
| 1381 |
|
| 1382 |
+
# Pool Management Helpers and Endpoints
|
| 1383 |
+
def build_pool_payload(pool: Dict, provider_map: Dict[str, Dict]) -> Dict:
|
| 1384 |
+
members_payload = []
|
| 1385 |
+
current_provider = None
|
| 1386 |
+
|
| 1387 |
+
for member in pool.get("members", []):
|
| 1388 |
+
provider_id = member["provider_id"]
|
| 1389 |
+
provider_status = provider_map.get(provider_id)
|
| 1390 |
+
|
| 1391 |
+
status = provider_status["status"] if provider_status else "unknown"
|
| 1392 |
+
uptime = provider_status.get("uptime", member.get("success_rate", 0)) if provider_status else member.get("success_rate", 0)
|
| 1393 |
+
response_time = provider_status.get("response_time_ms") if provider_status else None
|
| 1394 |
+
|
| 1395 |
+
member_payload = {
|
| 1396 |
+
"provider_id": provider_id,
|
| 1397 |
+
"provider_name": member["provider_name"],
|
| 1398 |
+
"priority": member.get("priority", 1),
|
| 1399 |
+
"weight": member.get("weight", 1),
|
| 1400 |
+
"use_count": member.get("use_count", 0),
|
| 1401 |
+
"success_rate": round(uptime, 2) if uptime is not None else 0,
|
| 1402 |
+
"status": status,
|
| 1403 |
+
"response_time_ms": response_time,
|
| 1404 |
+
"rate_limit": {
|
| 1405 |
+
"usage": member.get("rate_limit_usage", 0),
|
| 1406 |
+
"limit": member.get("rate_limit_limit", 0),
|
| 1407 |
+
"percentage": member.get("rate_limit_percentage", 0)
|
| 1408 |
+
}
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
# keep database stats in sync
|
| 1412 |
+
db.update_member_stats(
|
| 1413 |
+
pool["id"],
|
| 1414 |
+
provider_id,
|
| 1415 |
+
success_rate=uptime,
|
| 1416 |
+
rate_limit_usage=member_payload["rate_limit"]["usage"],
|
| 1417 |
+
rate_limit_limit=member_payload["rate_limit"]["limit"],
|
| 1418 |
+
rate_limit_percentage=member_payload["rate_limit"]["percentage"],
|
| 1419 |
+
)
|
| 1420 |
+
|
| 1421 |
+
members_payload.append(member_payload)
|
| 1422 |
+
|
| 1423 |
+
if not current_provider and status == "online":
|
| 1424 |
+
current_provider = {"name": member["provider_name"], "status": status}
|
| 1425 |
+
|
| 1426 |
+
if not current_provider and members_payload:
|
| 1427 |
+
degraded_member = next((m for m in members_payload if m["status"] == "degraded"), None)
|
| 1428 |
+
if degraded_member:
|
| 1429 |
+
current_provider = {"name": degraded_member["provider_name"], "status": degraded_member["status"]}
|
| 1430 |
+
|
| 1431 |
+
return {
|
| 1432 |
+
"pool_id": pool["id"],
|
| 1433 |
+
"pool_name": pool["name"],
|
| 1434 |
+
"category": pool["category"],
|
| 1435 |
+
"rotation_strategy": pool["rotation_strategy"],
|
| 1436 |
+
"description": pool.get("description"),
|
| 1437 |
+
"enabled": bool(pool.get("enabled", 1)),
|
| 1438 |
+
"members": members_payload,
|
| 1439 |
+
"current_provider": current_provider,
|
| 1440 |
+
"total_rotations": pool.get("rotation_count", 0),
|
| 1441 |
+
"created_at": pool.get("created_at")
|
| 1442 |
+
}
|
| 1443 |
+
|
| 1444 |
+
|
| 1445 |
+
def transform_rotation_history(entries: List[Dict]) -> List[Dict]:
|
| 1446 |
+
history = []
|
| 1447 |
+
for entry in entries:
|
| 1448 |
+
history.append({
|
| 1449 |
+
"pool_id": entry["pool_id"],
|
| 1450 |
+
"provider_id": entry["provider_id"],
|
| 1451 |
+
"provider_name": entry["provider_name"],
|
| 1452 |
+
"reason": entry["reason"],
|
| 1453 |
+
"timestamp": entry["created_at"]
|
| 1454 |
+
})
|
| 1455 |
+
return history
|
| 1456 |
+
|
| 1457 |
+
|
| 1458 |
+
async def broadcast_pool_update(action: str, pool_id: int, extra: Optional[Dict] = None):
|
| 1459 |
+
payload = {"type": "pool_update", "action": action, "pool_id": pool_id}
|
| 1460 |
+
if extra:
|
| 1461 |
+
payload.update(extra)
|
| 1462 |
+
await manager.broadcast(payload)
|
| 1463 |
+
|
| 1464 |
+
|
| 1465 |
+
@app.get("/api/pools")
|
| 1466 |
+
async def get_pools():
|
| 1467 |
+
"""Get all pools"""
|
| 1468 |
+
providers = await get_provider_stats()
|
| 1469 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1470 |
+
pools = db.get_pools()
|
| 1471 |
+
response = [build_pool_payload(pool, provider_map) for pool in pools]
|
| 1472 |
+
return {"pools": response}
|
| 1473 |
+
|
| 1474 |
+
|
| 1475 |
+
@app.post("/api/pools")
|
| 1476 |
+
async def create_pool(pool: PoolCreate):
|
| 1477 |
+
"""Create a new pool"""
|
| 1478 |
+
valid_strategies = {"round_robin", "priority", "weighted", "least_used"}
|
| 1479 |
+
if pool.rotation_strategy not in valid_strategies:
|
| 1480 |
+
raise HTTPException(status_code=400, detail="Invalid rotation strategy")
|
| 1481 |
+
|
| 1482 |
+
pool_id = db.create_pool(
|
| 1483 |
+
name=pool.name,
|
| 1484 |
+
category=pool.category,
|
| 1485 |
+
rotation_strategy=pool.rotation_strategy,
|
| 1486 |
+
description=pool.description,
|
| 1487 |
+
enabled=True
|
| 1488 |
+
)
|
| 1489 |
+
|
| 1490 |
+
providers = await get_provider_stats()
|
| 1491 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1492 |
+
pool_record = db.get_pool(pool_id)
|
| 1493 |
+
payload = build_pool_payload(pool_record, provider_map)
|
| 1494 |
+
|
| 1495 |
+
await broadcast_pool_update("created", pool_id, {"pool": payload})
|
| 1496 |
+
|
| 1497 |
+
return {
|
| 1498 |
+
"pool_id": pool_id,
|
| 1499 |
+
"message": "Pool created successfully",
|
| 1500 |
+
"pool": payload
|
| 1501 |
+
}
|
| 1502 |
+
|
| 1503 |
+
|
| 1504 |
+
@app.get("/api/pools/{pool_id}")
|
| 1505 |
+
async def get_pool(pool_id: int):
|
| 1506 |
+
"""Get specific pool"""
|
| 1507 |
+
pool = db.get_pool(pool_id)
|
| 1508 |
+
if not pool:
|
| 1509 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 1510 |
+
|
| 1511 |
+
providers = await get_provider_stats()
|
| 1512 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1513 |
+
return build_pool_payload(pool, provider_map)
|
| 1514 |
+
|
| 1515 |
+
|
| 1516 |
+
@app.delete("/api/pools/{pool_id}")
|
| 1517 |
+
async def delete_pool(pool_id: int):
|
| 1518 |
+
"""Delete a pool"""
|
| 1519 |
+
pool = db.get_pool(pool_id)
|
| 1520 |
+
if not pool:
|
| 1521 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 1522 |
+
|
| 1523 |
+
db.delete_pool(pool_id)
|
| 1524 |
+
await broadcast_pool_update("deleted", pool_id)
|
| 1525 |
+
return {"message": "Pool deleted successfully"}
|
| 1526 |
+
|
| 1527 |
+
|
| 1528 |
+
@app.post("/api/pools/{pool_id}/members")
|
| 1529 |
+
async def add_pool_member(pool_id: int, member: PoolMemberAdd):
|
| 1530 |
+
"""Add a member to a pool"""
|
| 1531 |
+
pool = db.get_pool(pool_id)
|
| 1532 |
+
if not pool:
|
| 1533 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 1534 |
+
|
| 1535 |
+
providers = await get_provider_stats()
|
| 1536 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1537 |
+
provider_info = provider_map.get(member.provider_id)
|
| 1538 |
+
if not provider_info:
|
| 1539 |
+
raise HTTPException(status_code=404, detail="Provider not found")
|
| 1540 |
+
|
| 1541 |
+
existing = next((m for m in pool["members"] if m["provider_id"] == member.provider_id), None)
|
| 1542 |
+
if existing:
|
| 1543 |
+
raise HTTPException(status_code=400, detail="Provider already in pool")
|
| 1544 |
+
|
| 1545 |
+
db.add_pool_member(
|
| 1546 |
+
pool_id=pool_id,
|
| 1547 |
+
provider_id=member.provider_id,
|
| 1548 |
+
provider_name=provider_info["name"],
|
| 1549 |
+
priority=max(1, min(member.priority, 10)),
|
| 1550 |
+
weight=max(1, min(member.weight, 100)),
|
| 1551 |
+
success_rate=provider_info.get("uptime", 0),
|
| 1552 |
+
rate_limit_usage=provider_info.get("rate_limit", {}).get("usage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
|
| 1553 |
+
rate_limit_limit=provider_info.get("rate_limit", {}).get("limit", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
|
| 1554 |
+
rate_limit_percentage=provider_info.get("rate_limit", {}).get("percentage", 0) if isinstance(provider_info.get("rate_limit"), dict) else 0,
|
| 1555 |
+
)
|
| 1556 |
+
|
| 1557 |
+
pool_record = db.get_pool(pool_id)
|
| 1558 |
+
payload = build_pool_payload(pool_record, provider_map)
|
| 1559 |
+
await broadcast_pool_update("member_added", pool_id, {"provider_id": member.provider_id})
|
| 1560 |
+
|
| 1561 |
+
return {
|
| 1562 |
+
"message": "Member added successfully",
|
| 1563 |
+
"pool": payload
|
| 1564 |
+
}
|
| 1565 |
+
|
| 1566 |
+
|
| 1567 |
+
@app.delete("/api/pools/{pool_id}/members/{provider_id}")
|
| 1568 |
+
async def remove_pool_member(pool_id: int, provider_id: str):
|
| 1569 |
+
"""Remove a member from a pool"""
|
| 1570 |
+
pool = db.get_pool(pool_id)
|
| 1571 |
+
if not pool:
|
| 1572 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 1573 |
+
|
| 1574 |
+
db.remove_pool_member(pool_id, provider_id)
|
| 1575 |
+
await broadcast_pool_update("member_removed", pool_id, {"provider_id": provider_id})
|
| 1576 |
+
|
| 1577 |
+
providers = await get_provider_stats()
|
| 1578 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1579 |
+
pool_record = db.get_pool(pool_id)
|
| 1580 |
+
payload = build_pool_payload(pool_record, provider_map)
|
| 1581 |
+
|
| 1582 |
+
return {
|
| 1583 |
+
"message": "Member removed successfully",
|
| 1584 |
+
"pool": payload
|
| 1585 |
+
}
|
| 1586 |
+
|
| 1587 |
+
|
| 1588 |
+
@app.post("/api/pools/{pool_id}/rotate")
|
| 1589 |
+
async def rotate_pool(pool_id: int, request: Optional[Dict] = None):
|
| 1590 |
+
"""Rotate pool to next provider"""
|
| 1591 |
+
pool = db.get_pool(pool_id)
|
| 1592 |
+
if not pool:
|
| 1593 |
+
raise HTTPException(status_code=404, detail="Pool not found")
|
| 1594 |
+
|
| 1595 |
+
if not pool["members"]:
|
| 1596 |
+
raise HTTPException(status_code=400, detail="Pool has no members")
|
| 1597 |
+
|
| 1598 |
+
providers = await get_provider_stats(force_refresh=True)
|
| 1599 |
+
provider_map = {provider_slug(p["name"]): p for p in providers}
|
| 1600 |
+
|
| 1601 |
+
members_with_status = []
|
| 1602 |
+
for member in pool["members"]:
|
| 1603 |
+
status_info = provider_map.get(member["provider_id"])
|
| 1604 |
+
if status_info:
|
| 1605 |
+
members_with_status.append((member, status_info))
|
| 1606 |
+
|
| 1607 |
+
online_members = [m for m in members_with_status if m[1]["status"] == "online"]
|
| 1608 |
+
degraded_members = [m for m in members_with_status if m[1]["status"] == "degraded"]
|
| 1609 |
+
|
| 1610 |
+
candidates = online_members or degraded_members
|
| 1611 |
+
if not candidates:
|
| 1612 |
+
raise HTTPException(status_code=400, detail="No healthy providers available for rotation")
|
| 1613 |
+
|
| 1614 |
+
strategy = pool.get("rotation_strategy", "round_robin")
|
| 1615 |
+
|
| 1616 |
+
if strategy == "priority":
|
| 1617 |
+
candidates.sort(key=lambda x: (x[0].get("priority", 1), x[0].get("weight", 1)), reverse=True)
|
| 1618 |
+
selected_member, status_info = candidates[0]
|
| 1619 |
+
elif strategy == "weighted":
|
| 1620 |
+
weights = [max(1, c[0].get("weight", 1)) for c in candidates]
|
| 1621 |
+
total_weight = sum(weights)
|
| 1622 |
+
roll = random.uniform(0, total_weight)
|
| 1623 |
+
cumulative = 0
|
| 1624 |
+
selected_member = candidates[0][0]
|
| 1625 |
+
status_info = candidates[0][1]
|
| 1626 |
+
for (candidate, status), weight in zip(candidates, weights):
|
| 1627 |
+
cumulative += weight
|
| 1628 |
+
if roll <= cumulative:
|
| 1629 |
+
selected_member, status_info = candidate, status
|
| 1630 |
+
break
|
| 1631 |
+
elif strategy == "least_used":
|
| 1632 |
+
candidates.sort(key=lambda x: x[0].get("use_count", 0))
|
| 1633 |
+
selected_member, status_info = candidates[0]
|
| 1634 |
+
else: # round_robin or default
|
| 1635 |
+
candidates.sort(key=lambda x: x[0].get("use_count", 0))
|
| 1636 |
+
selected_member, status_info = candidates[0]
|
| 1637 |
+
|
| 1638 |
+
db.increment_member_use(pool_id, selected_member["provider_id"])
|
| 1639 |
+
db.update_member_stats(
|
| 1640 |
+
pool_id,
|
| 1641 |
+
selected_member["provider_id"],
|
| 1642 |
+
success_rate=status_info.get("uptime", selected_member.get("success_rate")),
|
| 1643 |
+
rate_limit_usage=status_info.get("rate_limit", {}).get("usage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
|
| 1644 |
+
rate_limit_limit=status_info.get("rate_limit", {}).get("limit", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
|
| 1645 |
+
rate_limit_percentage=status_info.get("rate_limit", {}).get("percentage", 0) if isinstance(status_info.get("rate_limit"), dict) else None,
|
| 1646 |
+
)
|
| 1647 |
+
db.log_pool_rotation(
|
| 1648 |
+
pool_id,
|
| 1649 |
+
selected_member["provider_id"],
|
| 1650 |
+
selected_member["provider_name"],
|
| 1651 |
+
request.get("reason", "manual") if request else "manual"
|
| 1652 |
+
)
|
| 1653 |
+
|
| 1654 |
+
pool_record = db.get_pool(pool_id)
|
| 1655 |
+
payload = build_pool_payload(pool_record, provider_map)
|
| 1656 |
+
|
| 1657 |
+
await broadcast_pool_update("rotated", pool_id, {
|
| 1658 |
+
"provider_id": selected_member["provider_id"],
|
| 1659 |
+
"provider_name": selected_member["provider_name"]
|
| 1660 |
+
})
|
| 1661 |
+
|
| 1662 |
+
return {
|
| 1663 |
+
"message": "Pool rotated successfully",
|
| 1664 |
+
"provider_name": selected_member["provider_name"],
|
| 1665 |
+
"provider_id": selected_member["provider_id"],
|
| 1666 |
+
"total_rotations": pool_record.get("rotation_count", 0),
|
| 1667 |
+
"pool": payload
|
| 1668 |
+
}
|
| 1669 |
+
|
| 1670 |
+
|
| 1671 |
+
@app.get("/api/pools/{pool_id}/history")
|
| 1672 |
+
async def get_pool_history(pool_id: int, limit: int = 20):
|
| 1673 |
+
"""Get rotation history for a pool"""
|
| 1674 |
+
history = transform_rotation_history(db.get_pool_rotation_history(pool_id, limit))
|
| 1675 |
+
return {
|
| 1676 |
+
"history": history,
|
| 1677 |
+
"total": len(history)
|
| 1678 |
+
}
|
| 1679 |
+
|
| 1680 |
+
|
| 1681 |
+
@app.get("/api/pools/history")
|
| 1682 |
+
async def get_all_history(limit: int = 50):
|
| 1683 |
+
"""Get all rotation history"""
|
| 1684 |
+
history = transform_rotation_history(db.get_pool_rotation_history(None, limit))
|
| 1685 |
+
return {
|
| 1686 |
+
"history": history,
|
| 1687 |
+
"total": len(history)
|
| 1688 |
+
}
|
| 1689 |
|
| 1690 |
if __name__ == "__main__":
|
| 1691 |
+
print("🚀 Crypto Monitor ULTIMATE")
|
| 1692 |
+
print("📊 Real APIs: CoinGecko, CoinCap, Binance, DeFi Llama, Fear & Greed")
|
| 1693 |
+
print("🌐 http://localhost:8000/dashboard")
|
| 1694 |
print("📡 API Docs: http://localhost:8000/docs")
|
| 1695 |
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
database.py
CHANGED
|
@@ -6,6 +6,7 @@ Stores health metrics, incidents, and historical data
|
|
| 6 |
import sqlite3
|
| 7 |
import json
|
| 8 |
import logging
|
|
|
|
| 9 |
from typing import List, Dict, Optional, Tuple
|
| 10 |
from datetime import datetime, timedelta
|
| 11 |
from pathlib import Path
|
|
@@ -106,28 +107,81 @@ class Database:
|
|
| 106 |
)
|
| 107 |
""")
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
logger.info("Database initialized successfully")
|
| 133 |
|
|
@@ -478,3 +532,245 @@ class Database:
|
|
| 478 |
writer.writerow(dict(row))
|
| 479 |
|
| 480 |
logger.info(f"Exported {len(rows)} rows to {output_path}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
import sqlite3
|
| 7 |
import json
|
| 8 |
import logging
|
| 9 |
+
import time
|
| 10 |
from typing import List, Dict, Optional, Tuple
|
| 11 |
from datetime import datetime, timedelta
|
| 12 |
from pathlib import Path
|
|
|
|
| 107 |
)
|
| 108 |
""")
|
| 109 |
|
| 110 |
+
# Configuration table
|
| 111 |
+
cursor.execute("""
|
| 112 |
+
CREATE TABLE IF NOT EXISTS configuration (
|
| 113 |
+
key TEXT PRIMARY KEY,
|
| 114 |
+
value TEXT NOT NULL,
|
| 115 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 116 |
+
)
|
| 117 |
+
""")
|
| 118 |
+
|
| 119 |
+
# Pools table
|
| 120 |
+
cursor.execute("""
|
| 121 |
+
CREATE TABLE IF NOT EXISTS pools (
|
| 122 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 123 |
+
name TEXT NOT NULL,
|
| 124 |
+
category TEXT NOT NULL,
|
| 125 |
+
rotation_strategy TEXT NOT NULL,
|
| 126 |
+
description TEXT,
|
| 127 |
+
enabled INTEGER DEFAULT 1,
|
| 128 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
| 129 |
+
)
|
| 130 |
+
""")
|
| 131 |
+
|
| 132 |
+
# Pool members table
|
| 133 |
+
cursor.execute("""
|
| 134 |
+
CREATE TABLE IF NOT EXISTS pool_members (
|
| 135 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 136 |
+
pool_id INTEGER NOT NULL,
|
| 137 |
+
provider_id TEXT NOT NULL,
|
| 138 |
+
provider_name TEXT NOT NULL,
|
| 139 |
+
priority INTEGER DEFAULT 1,
|
| 140 |
+
weight INTEGER DEFAULT 1,
|
| 141 |
+
use_count INTEGER DEFAULT 0,
|
| 142 |
+
success_rate REAL DEFAULT 0,
|
| 143 |
+
rate_limit_usage INTEGER DEFAULT 0,
|
| 144 |
+
rate_limit_limit INTEGER DEFAULT 0,
|
| 145 |
+
rate_limit_percentage REAL DEFAULT 0,
|
| 146 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 147 |
+
FOREIGN KEY (pool_id) REFERENCES pools(id) ON DELETE CASCADE
|
| 148 |
+
)
|
| 149 |
+
""")
|
| 150 |
+
|
| 151 |
+
# Pool rotation history
|
| 152 |
+
cursor.execute("""
|
| 153 |
+
CREATE TABLE IF NOT EXISTS pool_rotations (
|
| 154 |
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
| 155 |
+
pool_id INTEGER NOT NULL,
|
| 156 |
+
provider_id TEXT NOT NULL,
|
| 157 |
+
provider_name TEXT NOT NULL,
|
| 158 |
+
reason TEXT NOT NULL,
|
| 159 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
| 160 |
+
FOREIGN KEY (pool_id) REFERENCES pools(id) ON DELETE CASCADE
|
| 161 |
+
)
|
| 162 |
+
""")
|
| 163 |
+
|
| 164 |
+
# Create indexes
|
| 165 |
+
cursor.execute("""
|
| 166 |
+
CREATE INDEX IF NOT EXISTS idx_status_log_provider
|
| 167 |
+
ON status_log(provider_name, timestamp)
|
| 168 |
+
""")
|
| 169 |
+
cursor.execute("""
|
| 170 |
+
CREATE INDEX IF NOT EXISTS idx_status_log_timestamp
|
| 171 |
+
ON status_log(timestamp)
|
| 172 |
+
""")
|
| 173 |
+
cursor.execute("""
|
| 174 |
+
CREATE INDEX IF NOT EXISTS idx_incidents_provider
|
| 175 |
+
ON incidents(provider_name, start_time)
|
| 176 |
+
""")
|
| 177 |
+
cursor.execute("""
|
| 178 |
+
CREATE INDEX IF NOT EXISTS idx_pool_members_pool
|
| 179 |
+
ON pool_members(pool_id, provider_id)
|
| 180 |
+
""")
|
| 181 |
+
cursor.execute("""
|
| 182 |
+
CREATE INDEX IF NOT EXISTS idx_pool_rotations_pool
|
| 183 |
+
ON pool_rotations(pool_id, created_at)
|
| 184 |
+
""")
|
| 185 |
|
| 186 |
logger.info("Database initialized successfully")
|
| 187 |
|
|
|
|
| 532 |
writer.writerow(dict(row))
|
| 533 |
|
| 534 |
logger.info(f"Exported {len(rows)} rows to {output_path}")
|
| 535 |
+
|
| 536 |
+
# ------------------------------------------------------------------
|
| 537 |
+
# Pool management helpers
|
| 538 |
+
# ------------------------------------------------------------------
|
| 539 |
+
|
| 540 |
+
def create_pool(
|
| 541 |
+
self,
|
| 542 |
+
name: str,
|
| 543 |
+
category: str,
|
| 544 |
+
rotation_strategy: str,
|
| 545 |
+
description: Optional[str] = None,
|
| 546 |
+
enabled: bool = True
|
| 547 |
+
) -> int:
|
| 548 |
+
"""Create a new pool and return its ID"""
|
| 549 |
+
with self.get_connection() as conn:
|
| 550 |
+
cursor = conn.cursor()
|
| 551 |
+
cursor.execute("""
|
| 552 |
+
INSERT INTO pools (name, category, rotation_strategy, description, enabled)
|
| 553 |
+
VALUES (?, ?, ?, ?, ?)
|
| 554 |
+
""", (name, category, rotation_strategy, description, int(enabled)))
|
| 555 |
+
return cursor.lastrowid
|
| 556 |
+
|
| 557 |
+
def update_pool_usage(self, pool_id: int, enabled: Optional[bool] = None):
|
| 558 |
+
"""Update pool properties"""
|
| 559 |
+
if enabled is None:
|
| 560 |
+
return
|
| 561 |
+
with self.get_connection() as conn:
|
| 562 |
+
cursor = conn.cursor()
|
| 563 |
+
cursor.execute("""
|
| 564 |
+
UPDATE pools
|
| 565 |
+
SET enabled = ?, created_at = created_at
|
| 566 |
+
WHERE id = ?
|
| 567 |
+
""", (int(enabled), pool_id))
|
| 568 |
+
|
| 569 |
+
def delete_pool(self, pool_id: int):
|
| 570 |
+
"""Delete pool and cascade members/history"""
|
| 571 |
+
with self.get_connection() as conn:
|
| 572 |
+
cursor = conn.cursor()
|
| 573 |
+
cursor.execute("DELETE FROM pools WHERE id = ?", (pool_id,))
|
| 574 |
+
|
| 575 |
+
def add_pool_member(
|
| 576 |
+
self,
|
| 577 |
+
pool_id: int,
|
| 578 |
+
provider_id: str,
|
| 579 |
+
provider_name: str,
|
| 580 |
+
priority: int = 1,
|
| 581 |
+
weight: int = 1,
|
| 582 |
+
success_rate: float = 0.0,
|
| 583 |
+
rate_limit_usage: int = 0,
|
| 584 |
+
rate_limit_limit: int = 0,
|
| 585 |
+
rate_limit_percentage: float = 0.0
|
| 586 |
+
) -> int:
|
| 587 |
+
"""Add a provider to a pool"""
|
| 588 |
+
with self.get_connection() as conn:
|
| 589 |
+
cursor = conn.cursor()
|
| 590 |
+
cursor.execute("""
|
| 591 |
+
INSERT INTO pool_members
|
| 592 |
+
(pool_id, provider_id, provider_name, priority, weight,
|
| 593 |
+
success_rate, rate_limit_usage, rate_limit_limit, rate_limit_percentage)
|
| 594 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 595 |
+
""", (
|
| 596 |
+
pool_id,
|
| 597 |
+
provider_id,
|
| 598 |
+
provider_name,
|
| 599 |
+
priority,
|
| 600 |
+
weight,
|
| 601 |
+
success_rate,
|
| 602 |
+
rate_limit_usage,
|
| 603 |
+
rate_limit_limit,
|
| 604 |
+
rate_limit_percentage
|
| 605 |
+
))
|
| 606 |
+
return cursor.lastrowid
|
| 607 |
+
|
| 608 |
+
def remove_pool_member(self, pool_id: int, provider_id: str):
|
| 609 |
+
"""Remove provider from pool"""
|
| 610 |
+
with self.get_connection() as conn:
|
| 611 |
+
cursor = conn.cursor()
|
| 612 |
+
cursor.execute("""
|
| 613 |
+
DELETE FROM pool_members
|
| 614 |
+
WHERE pool_id = ? AND provider_id = ?
|
| 615 |
+
""", (pool_id, provider_id))
|
| 616 |
+
|
| 617 |
+
def increment_member_use(self, pool_id: int, provider_id: str):
|
| 618 |
+
"""Increment use count for pool member"""
|
| 619 |
+
with self.get_connection() as conn:
|
| 620 |
+
cursor = conn.cursor()
|
| 621 |
+
cursor.execute("""
|
| 622 |
+
UPDATE pool_members
|
| 623 |
+
SET use_count = use_count + 1
|
| 624 |
+
WHERE pool_id = ? AND provider_id = ?
|
| 625 |
+
""", (pool_id, provider_id))
|
| 626 |
+
|
| 627 |
+
def update_member_stats(
|
| 628 |
+
self,
|
| 629 |
+
pool_id: int,
|
| 630 |
+
provider_id: str,
|
| 631 |
+
success_rate: Optional[float] = None,
|
| 632 |
+
rate_limit_usage: Optional[int] = None,
|
| 633 |
+
rate_limit_limit: Optional[int] = None,
|
| 634 |
+
rate_limit_percentage: Optional[float] = None
|
| 635 |
+
):
|
| 636 |
+
"""Update success/rate limit stats"""
|
| 637 |
+
updates = []
|
| 638 |
+
params = []
|
| 639 |
+
|
| 640 |
+
if success_rate is not None:
|
| 641 |
+
updates.append("success_rate = ?")
|
| 642 |
+
params.append(success_rate)
|
| 643 |
+
if rate_limit_usage is not None:
|
| 644 |
+
updates.append("rate_limit_usage = ?")
|
| 645 |
+
params.append(rate_limit_usage)
|
| 646 |
+
if rate_limit_limit is not None:
|
| 647 |
+
updates.append("rate_limit_limit = ?")
|
| 648 |
+
params.append(rate_limit_limit)
|
| 649 |
+
if rate_limit_percentage is not None:
|
| 650 |
+
updates.append("rate_limit_percentage = ?")
|
| 651 |
+
params.append(rate_limit_percentage)
|
| 652 |
+
|
| 653 |
+
if not updates:
|
| 654 |
+
return
|
| 655 |
+
|
| 656 |
+
params.extend([pool_id, provider_id])
|
| 657 |
+
|
| 658 |
+
with self.get_connection() as conn:
|
| 659 |
+
cursor = conn.cursor()
|
| 660 |
+
cursor.execute(f"""
|
| 661 |
+
UPDATE pool_members
|
| 662 |
+
SET {', '.join(updates)}
|
| 663 |
+
WHERE pool_id = ? AND provider_id = ?
|
| 664 |
+
""", params)
|
| 665 |
+
|
| 666 |
+
def log_pool_rotation(
|
| 667 |
+
self,
|
| 668 |
+
pool_id: int,
|
| 669 |
+
provider_id: str,
|
| 670 |
+
provider_name: str,
|
| 671 |
+
reason: str
|
| 672 |
+
):
|
| 673 |
+
"""Log rotation event"""
|
| 674 |
+
with self.get_connection() as conn:
|
| 675 |
+
cursor = conn.cursor()
|
| 676 |
+
cursor.execute("""
|
| 677 |
+
INSERT INTO pool_rotations
|
| 678 |
+
(pool_id, provider_id, provider_name, reason)
|
| 679 |
+
VALUES (?, ?, ?, ?)
|
| 680 |
+
""", (pool_id, provider_id, provider_name, reason))
|
| 681 |
+
|
| 682 |
+
def get_pools(self) -> List[Dict]:
|
| 683 |
+
"""Get all pools with members and stats"""
|
| 684 |
+
with self.get_connection() as conn:
|
| 685 |
+
cursor = conn.cursor()
|
| 686 |
+
cursor.execute("""
|
| 687 |
+
SELECT p.*,
|
| 688 |
+
COALESCE((SELECT COUNT(*) FROM pool_rotations pr WHERE pr.pool_id = p.id), 0) as rotation_count
|
| 689 |
+
FROM pools p
|
| 690 |
+
ORDER BY p.created_at DESC
|
| 691 |
+
""")
|
| 692 |
+
pools = [dict(row) for row in cursor.fetchall()]
|
| 693 |
+
|
| 694 |
+
for pool in pools:
|
| 695 |
+
cursor.execute("""
|
| 696 |
+
SELECT * FROM pool_members
|
| 697 |
+
WHERE pool_id = ?
|
| 698 |
+
ORDER BY priority DESC, weight DESC, provider_name
|
| 699 |
+
""", (pool['id'],))
|
| 700 |
+
pool['members'] = [dict(row) for row in cursor.fetchall()]
|
| 701 |
+
|
| 702 |
+
return pools
|
| 703 |
+
|
| 704 |
+
def get_pool(self, pool_id: int) -> Optional[Dict]:
|
| 705 |
+
"""Get single pool"""
|
| 706 |
+
with self.get_connection() as conn:
|
| 707 |
+
cursor = conn.cursor()
|
| 708 |
+
cursor.execute("""
|
| 709 |
+
SELECT p.*,
|
| 710 |
+
COALESCE((SELECT COUNT(*) FROM pool_rotations pr WHERE pr.pool_id = p.id), 0) as rotation_count
|
| 711 |
+
FROM pools p
|
| 712 |
+
WHERE p.id = ?
|
| 713 |
+
""", (pool_id,))
|
| 714 |
+
row = cursor.fetchone()
|
| 715 |
+
if not row:
|
| 716 |
+
return None
|
| 717 |
+
pool = dict(row)
|
| 718 |
+
cursor.execute("""
|
| 719 |
+
SELECT * FROM pool_members
|
| 720 |
+
WHERE pool_id = ?
|
| 721 |
+
ORDER BY priority DESC, weight DESC, provider_name
|
| 722 |
+
""", (pool_id,))
|
| 723 |
+
pool['members'] = [dict(r) for r in cursor.fetchall()]
|
| 724 |
+
return pool
|
| 725 |
+
|
| 726 |
+
def get_pool_rotation_history(self, pool_id: Optional[int] = None, limit: int = 50) -> List[Dict]:
|
| 727 |
+
"""Get rotation history (optionally filtered by pool)"""
|
| 728 |
+
with self.get_connection() as conn:
|
| 729 |
+
cursor = conn.cursor()
|
| 730 |
+
if pool_id is not None:
|
| 731 |
+
cursor.execute("""
|
| 732 |
+
SELECT * FROM pool_rotations
|
| 733 |
+
WHERE pool_id = ?
|
| 734 |
+
ORDER BY created_at DESC
|
| 735 |
+
LIMIT ?
|
| 736 |
+
""", (pool_id, limit))
|
| 737 |
+
else:
|
| 738 |
+
cursor.execute("""
|
| 739 |
+
SELECT * FROM pool_rotations
|
| 740 |
+
ORDER BY created_at DESC
|
| 741 |
+
LIMIT ?
|
| 742 |
+
""", (limit,))
|
| 743 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 744 |
+
|
| 745 |
+
# ------------------------------------------------------------------
|
| 746 |
+
# Provider health logging
|
| 747 |
+
# ------------------------------------------------------------------
|
| 748 |
+
|
| 749 |
+
def log_provider_status(
|
| 750 |
+
self,
|
| 751 |
+
provider_name: str,
|
| 752 |
+
category: str,
|
| 753 |
+
status: str,
|
| 754 |
+
response_time: Optional[float] = None,
|
| 755 |
+
status_code: Optional[int] = None,
|
| 756 |
+
endpoint_tested: Optional[str] = None,
|
| 757 |
+
error_message: Optional[str] = None
|
| 758 |
+
):
|
| 759 |
+
"""Log provider status in status_log table"""
|
| 760 |
+
with self.get_connection() as conn:
|
| 761 |
+
cursor = conn.cursor()
|
| 762 |
+
cursor.execute("""
|
| 763 |
+
INSERT INTO status_log
|
| 764 |
+
(provider_name, category, status, response_time, status_code,
|
| 765 |
+
error_message, endpoint_tested, timestamp)
|
| 766 |
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
| 767 |
+
""", (
|
| 768 |
+
provider_name,
|
| 769 |
+
category,
|
| 770 |
+
status,
|
| 771 |
+
response_time,
|
| 772 |
+
status_code,
|
| 773 |
+
error_message,
|
| 774 |
+
endpoint_tested,
|
| 775 |
+
time.time()
|
| 776 |
+
))
|
database/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (374 Bytes). View file
|
|
|
database/__pycache__/data_access.cpython-313.pyc
ADDED
|
Binary file (25.1 kB). View file
|
|
|
database/__pycache__/db_manager.cpython-313.pyc
ADDED
|
Binary file (64.4 kB). View file
|
|
|
database/__pycache__/models.cpython-313.pyc
ADDED
|
Binary file (17.5 kB). View file
|
|
|
enhanced_dashboard.html
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Enhanced Crypto Data Tracker</title>
|
| 7 |
+
<style>
|
| 8 |
+
* {
|
| 9 |
+
margin: 0;
|
| 10 |
+
padding: 0;
|
| 11 |
+
box-sizing: border-box;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
body {
|
| 15 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
| 16 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 17 |
+
min-height: 100vh;
|
| 18 |
+
padding: 20px;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
.container {
|
| 22 |
+
max-width: 1600px;
|
| 23 |
+
margin: 0 auto;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
header {
|
| 27 |
+
background: rgba(255, 255, 255, 0.1);
|
| 28 |
+
backdrop-filter: blur(10px);
|
| 29 |
+
border-radius: 15px;
|
| 30 |
+
padding: 20px 30px;
|
| 31 |
+
margin-bottom: 20px;
|
| 32 |
+
display: flex;
|
| 33 |
+
justify-content: space-between;
|
| 34 |
+
align-items: center;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
h1 {
|
| 38 |
+
color: white;
|
| 39 |
+
font-size: 28px;
|
| 40 |
+
display: flex;
|
| 41 |
+
align-items: center;
|
| 42 |
+
gap: 10px;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.connection-status {
|
| 46 |
+
display: flex;
|
| 47 |
+
align-items: center;
|
| 48 |
+
gap: 10px;
|
| 49 |
+
color: white;
|
| 50 |
+
font-size: 14px;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.status-indicator {
|
| 54 |
+
width: 10px;
|
| 55 |
+
height: 10px;
|
| 56 |
+
border-radius: 50%;
|
| 57 |
+
background: #10b981;
|
| 58 |
+
animation: pulse 2s infinite;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.status-indicator.disconnected {
|
| 62 |
+
background: #ef4444;
|
| 63 |
+
animation: none;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
@keyframes pulse {
|
| 67 |
+
0%, 100% { opacity: 1; }
|
| 68 |
+
50% { opacity: 0.5; }
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
.controls {
|
| 72 |
+
background: rgba(255, 255, 255, 0.1);
|
| 73 |
+
backdrop-filter: blur(10px);
|
| 74 |
+
border-radius: 15px;
|
| 75 |
+
padding: 20px;
|
| 76 |
+
margin-bottom: 20px;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.controls-row {
|
| 80 |
+
display: flex;
|
| 81 |
+
gap: 15px;
|
| 82 |
+
flex-wrap: wrap;
|
| 83 |
+
align-items: center;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
.btn {
|
| 87 |
+
padding: 10px 20px;
|
| 88 |
+
border: none;
|
| 89 |
+
border-radius: 8px;
|
| 90 |
+
cursor: pointer;
|
| 91 |
+
font-size: 14px;
|
| 92 |
+
font-weight: 600;
|
| 93 |
+
transition: all 0.3s ease;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
gap: 8px;
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.btn-primary {
|
| 100 |
+
background: white;
|
| 101 |
+
color: #667eea;
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
.btn-primary:hover {
|
| 105 |
+
transform: translateY(-2px);
|
| 106 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.btn-success {
|
| 110 |
+
background: #10b981;
|
| 111 |
+
color: white;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.btn-danger {
|
| 115 |
+
background: #ef4444;
|
| 116 |
+
color: white;
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
.btn-info {
|
| 120 |
+
background: #3b82f6;
|
| 121 |
+
color: white;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.btn:disabled {
|
| 125 |
+
opacity: 0.5;
|
| 126 |
+
cursor: not-allowed;
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
.grid {
|
| 130 |
+
display: grid;
|
| 131 |
+
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
| 132 |
+
gap: 20px;
|
| 133 |
+
margin-bottom: 20px;
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.card {
|
| 137 |
+
background: rgba(255, 255, 255, 0.1);
|
| 138 |
+
backdrop-filter: blur(10px);
|
| 139 |
+
border-radius: 15px;
|
| 140 |
+
padding: 20px;
|
| 141 |
+
color: white;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
.card h2 {
|
| 145 |
+
font-size: 18px;
|
| 146 |
+
margin-bottom: 15px;
|
| 147 |
+
display: flex;
|
| 148 |
+
align-items: center;
|
| 149 |
+
gap: 10px;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
.stat-grid {
|
| 153 |
+
display: grid;
|
| 154 |
+
grid-template-columns: repeat(2, 1fr);
|
| 155 |
+
gap: 15px;
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
.stat-item {
|
| 159 |
+
background: rgba(255, 255, 255, 0.1);
|
| 160 |
+
padding: 15px;
|
| 161 |
+
border-radius: 10px;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.stat-label {
|
| 165 |
+
font-size: 12px;
|
| 166 |
+
opacity: 0.8;
|
| 167 |
+
margin-bottom: 5px;
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
.stat-value {
|
| 171 |
+
font-size: 24px;
|
| 172 |
+
font-weight: 700;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
.api-list {
|
| 176 |
+
max-height: 400px;
|
| 177 |
+
overflow-y: auto;
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
.api-item {
|
| 181 |
+
background: rgba(255, 255, 255, 0.05);
|
| 182 |
+
padding: 15px;
|
| 183 |
+
border-radius: 10px;
|
| 184 |
+
margin-bottom: 10px;
|
| 185 |
+
display: flex;
|
| 186 |
+
justify-content: space-between;
|
| 187 |
+
align-items: center;
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
.api-info {
|
| 191 |
+
flex: 1;
|
| 192 |
+
}
|
| 193 |
+
|
| 194 |
+
.api-name {
|
| 195 |
+
font-weight: 600;
|
| 196 |
+
margin-bottom: 5px;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
.api-meta {
|
| 200 |
+
font-size: 12px;
|
| 201 |
+
opacity: 0.7;
|
| 202 |
+
display: flex;
|
| 203 |
+
gap: 15px;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
.api-controls {
|
| 207 |
+
display: flex;
|
| 208 |
+
gap: 10px;
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
.small-btn {
|
| 212 |
+
padding: 5px 12px;
|
| 213 |
+
font-size: 12px;
|
| 214 |
+
border-radius: 5px;
|
| 215 |
+
border: none;
|
| 216 |
+
cursor: pointer;
|
| 217 |
+
background: rgba(255, 255, 255, 0.2);
|
| 218 |
+
color: white;
|
| 219 |
+
transition: all 0.2s;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.small-btn:hover {
|
| 223 |
+
background: rgba(255, 255, 255, 0.3);
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.status-badge {
|
| 227 |
+
display: inline-block;
|
| 228 |
+
padding: 4px 10px;
|
| 229 |
+
border-radius: 12px;
|
| 230 |
+
font-size: 11px;
|
| 231 |
+
font-weight: 600;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
.status-success {
|
| 235 |
+
background: #10b981;
|
| 236 |
+
color: white;
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
.status-pending {
|
| 240 |
+
background: #f59e0b;
|
| 241 |
+
color: white;
|
| 242 |
+
}
|
| 243 |
+
|
| 244 |
+
.status-failed {
|
| 245 |
+
background: #ef4444;
|
| 246 |
+
color: white;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.log-container {
|
| 250 |
+
background: rgba(0, 0, 0, 0.3);
|
| 251 |
+
border-radius: 10px;
|
| 252 |
+
padding: 15px;
|
| 253 |
+
max-height: 300px;
|
| 254 |
+
overflow-y: auto;
|
| 255 |
+
font-family: 'Courier New', monospace;
|
| 256 |
+
font-size: 12px;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.log-entry {
|
| 260 |
+
margin-bottom: 8px;
|
| 261 |
+
padding: 5px;
|
| 262 |
+
border-left: 3px solid #667eea;
|
| 263 |
+
padding-left: 10px;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.log-time {
|
| 267 |
+
opacity: 0.6;
|
| 268 |
+
margin-right: 10px;
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.modal {
|
| 272 |
+
display: none;
|
| 273 |
+
position: fixed;
|
| 274 |
+
top: 0;
|
| 275 |
+
left: 0;
|
| 276 |
+
width: 100%;
|
| 277 |
+
height: 100%;
|
| 278 |
+
background: rgba(0, 0, 0, 0.7);
|
| 279 |
+
z-index: 1000;
|
| 280 |
+
justify-content: center;
|
| 281 |
+
align-items: center;
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
.modal.active {
|
| 285 |
+
display: flex;
|
| 286 |
+
}
|
| 287 |
+
|
| 288 |
+
.modal-content {
|
| 289 |
+
background: white;
|
| 290 |
+
border-radius: 15px;
|
| 291 |
+
padding: 30px;
|
| 292 |
+
max-width: 500px;
|
| 293 |
+
width: 90%;
|
| 294 |
+
color: #333;
|
| 295 |
+
}
|
| 296 |
+
|
| 297 |
+
.modal-header {
|
| 298 |
+
display: flex;
|
| 299 |
+
justify-content: space-between;
|
| 300 |
+
align-items: center;
|
| 301 |
+
margin-bottom: 20px;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.modal-close {
|
| 305 |
+
background: none;
|
| 306 |
+
border: none;
|
| 307 |
+
font-size: 24px;
|
| 308 |
+
cursor: pointer;
|
| 309 |
+
color: #666;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
.form-group {
|
| 313 |
+
margin-bottom: 15px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.form-label {
|
| 317 |
+
display: block;
|
| 318 |
+
margin-bottom: 5px;
|
| 319 |
+
font-weight: 600;
|
| 320 |
+
color: #333;
|
| 321 |
+
}
|
| 322 |
+
|
| 323 |
+
.form-input {
|
| 324 |
+
width: 100%;
|
| 325 |
+
padding: 10px;
|
| 326 |
+
border: 1px solid #ddd;
|
| 327 |
+
border-radius: 8px;
|
| 328 |
+
font-size: 14px;
|
| 329 |
+
}
|
| 330 |
+
|
| 331 |
+
.form-select {
|
| 332 |
+
width: 100%;
|
| 333 |
+
padding: 10px;
|
| 334 |
+
border: 1px solid #ddd;
|
| 335 |
+
border-radius: 8px;
|
| 336 |
+
font-size: 14px;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
::-webkit-scrollbar {
|
| 340 |
+
width: 8px;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
::-webkit-scrollbar-track {
|
| 344 |
+
background: rgba(255, 255, 255, 0.1);
|
| 345 |
+
border-radius: 4px;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
::-webkit-scrollbar-thumb {
|
| 349 |
+
background: rgba(255, 255, 255, 0.3);
|
| 350 |
+
border-radius: 4px;
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
::-webkit-scrollbar-thumb:hover {
|
| 354 |
+
background: rgba(255, 255, 255, 0.5);
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.toast {
|
| 358 |
+
position: fixed;
|
| 359 |
+
bottom: 20px;
|
| 360 |
+
right: 20px;
|
| 361 |
+
background: white;
|
| 362 |
+
color: #333;
|
| 363 |
+
padding: 15px 20px;
|
| 364 |
+
border-radius: 10px;
|
| 365 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
| 366 |
+
opacity: 0;
|
| 367 |
+
transform: translateY(20px);
|
| 368 |
+
transition: all 0.3s ease;
|
| 369 |
+
z-index: 2000;
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
.toast.show {
|
| 373 |
+
opacity: 1;
|
| 374 |
+
transform: translateY(0);
|
| 375 |
+
}
|
| 376 |
+
</style>
|
| 377 |
+
</head>
|
| 378 |
+
<body>
|
| 379 |
+
<div class="container">
|
| 380 |
+
<header>
|
| 381 |
+
<h1>
|
| 382 |
+
<span>🚀</span>
|
| 383 |
+
Enhanced Crypto Data Tracker
|
| 384 |
+
</h1>
|
| 385 |
+
<div class="connection-status">
|
| 386 |
+
<div class="status-indicator" id="wsStatus"></div>
|
| 387 |
+
<span id="wsStatusText">Connecting...</span>
|
| 388 |
+
</div>
|
| 389 |
+
</header>
|
| 390 |
+
|
| 391 |
+
<div class="controls">
|
| 392 |
+
<div class="controls-row">
|
| 393 |
+
<button class="btn btn-primary" onclick="exportJSON()">
|
| 394 |
+
💾 Export JSON
|
| 395 |
+
</button>
|
| 396 |
+
<button class="btn btn-primary" onclick="exportCSV()">
|
| 397 |
+
📊 Export CSV
|
| 398 |
+
</button>
|
| 399 |
+
<button class="btn btn-success" onclick="createBackup()">
|
| 400 |
+
🔄 Create Backup
|
| 401 |
+
</button>
|
| 402 |
+
<button class="btn btn-info" onclick="showScheduleModal()">
|
| 403 |
+
⏰ Configure Schedule
|
| 404 |
+
</button>
|
| 405 |
+
<button class="btn btn-info" onclick="forceUpdateAll()">
|
| 406 |
+
🔃 Force Update All
|
| 407 |
+
</button>
|
| 408 |
+
<button class="btn btn-danger" onclick="clearCache()">
|
| 409 |
+
🗑️ Clear Cache
|
| 410 |
+
</button>
|
| 411 |
+
</div>
|
| 412 |
+
</div>
|
| 413 |
+
|
| 414 |
+
<div class="grid">
|
| 415 |
+
<div class="card">
|
| 416 |
+
<h2>📊 System Statistics</h2>
|
| 417 |
+
<div class="stat-grid">
|
| 418 |
+
<div class="stat-item">
|
| 419 |
+
<div class="stat-label">Total APIs</div>
|
| 420 |
+
<div class="stat-value" id="totalApis">0</div>
|
| 421 |
+
</div>
|
| 422 |
+
<div class="stat-item">
|
| 423 |
+
<div class="stat-label">Active Tasks</div>
|
| 424 |
+
<div class="stat-value" id="activeTasks">0</div>
|
| 425 |
+
</div>
|
| 426 |
+
<div class="stat-item">
|
| 427 |
+
<div class="stat-label">Cached Data</div>
|
| 428 |
+
<div class="stat-value" id="cachedData">0</div>
|
| 429 |
+
</div>
|
| 430 |
+
<div class="stat-item">
|
| 431 |
+
<div class="stat-label">WS Connections</div>
|
| 432 |
+
<div class="stat-value" id="wsConnections">0</div>
|
| 433 |
+
</div>
|
| 434 |
+
</div>
|
| 435 |
+
</div>
|
| 436 |
+
|
| 437 |
+
<div class="card">
|
| 438 |
+
<h2>📈 Recent Activity</h2>
|
| 439 |
+
<div class="log-container" id="activityLog">
|
| 440 |
+
<div class="log-entry">
|
| 441 |
+
<span class="log-time">--:--:--</span>
|
| 442 |
+
Waiting for updates...
|
| 443 |
+
</div>
|
| 444 |
+
</div>
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
|
| 448 |
+
<div class="card">
|
| 449 |
+
<h2>🔌 API Sources</h2>
|
| 450 |
+
<div class="api-list" id="apiList">
|
| 451 |
+
Loading...
|
| 452 |
+
</div>
|
| 453 |
+
</div>
|
| 454 |
+
</div>
|
| 455 |
+
|
| 456 |
+
<!-- Schedule Modal -->
|
| 457 |
+
<div class="modal" id="scheduleModal">
|
| 458 |
+
<div class="modal-content">
|
| 459 |
+
<div class="modal-header">
|
| 460 |
+
<h2>⏰ Configure Schedule</h2>
|
| 461 |
+
<button class="modal-close" onclick="closeScheduleModal()">×</button>
|
| 462 |
+
</div>
|
| 463 |
+
<div class="form-group">
|
| 464 |
+
<label class="form-label">API Source</label>
|
| 465 |
+
<select class="form-select" id="scheduleApiSelect"></select>
|
| 466 |
+
</div>
|
| 467 |
+
<div class="form-group">
|
| 468 |
+
<label class="form-label">Interval (seconds)</label>
|
| 469 |
+
<input type="number" class="form-input" id="scheduleInterval" value="60" min="10">
|
| 470 |
+
</div>
|
| 471 |
+
<div class="form-group">
|
| 472 |
+
<label class="form-label">Enabled</label>
|
| 473 |
+
<input type="checkbox" id="scheduleEnabled" checked>
|
| 474 |
+
</div>
|
| 475 |
+
<button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button>
|
| 476 |
+
</div>
|
| 477 |
+
</div>
|
| 478 |
+
|
| 479 |
+
<!-- Toast notification -->
|
| 480 |
+
<div class="toast" id="toast"></div>
|
| 481 |
+
|
| 482 |
+
<script>
|
| 483 |
+
let ws = null;
|
| 484 |
+
let reconnectAttempts = 0;
|
| 485 |
+
const maxReconnectAttempts = 5;
|
| 486 |
+
const reconnectDelay = 3000;
|
| 487 |
+
|
| 488 |
+
// Initialize WebSocket connection
|
| 489 |
+
function initWebSocket() {
|
| 490 |
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
| 491 |
+
const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`;
|
| 492 |
+
|
| 493 |
+
ws = new WebSocket(wsUrl);
|
| 494 |
+
|
| 495 |
+
ws.onopen = () => {
|
| 496 |
+
console.log('WebSocket connected');
|
| 497 |
+
updateWSStatus(true);
|
| 498 |
+
reconnectAttempts = 0;
|
| 499 |
+
|
| 500 |
+
// Subscribe to all updates
|
| 501 |
+
ws.send(JSON.stringify({ type: 'subscribe_all' }));
|
| 502 |
+
|
| 503 |
+
// Start heartbeat
|
| 504 |
+
startHeartbeat();
|
| 505 |
+
};
|
| 506 |
+
|
| 507 |
+
ws.onmessage = (event) => {
|
| 508 |
+
const message = JSON.parse(event.data);
|
| 509 |
+
handleWSMessage(message);
|
| 510 |
+
};
|
| 511 |
+
|
| 512 |
+
ws.onerror = (error) => {
|
| 513 |
+
console.error('WebSocket error:', error);
|
| 514 |
+
};
|
| 515 |
+
|
| 516 |
+
ws.onclose = () => {
|
| 517 |
+
console.log('WebSocket disconnected');
|
| 518 |
+
updateWSStatus(false);
|
| 519 |
+
attemptReconnect();
|
| 520 |
+
};
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function attemptReconnect() {
|
| 524 |
+
if (reconnectAttempts < maxReconnectAttempts) {
|
| 525 |
+
reconnectAttempts++;
|
| 526 |
+
console.log(`Reconnecting... Attempt ${reconnectAttempts}`);
|
| 527 |
+
setTimeout(initWebSocket, reconnectDelay);
|
| 528 |
+
}
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
let heartbeatInterval;
|
| 532 |
+
function startHeartbeat() {
|
| 533 |
+
clearInterval(heartbeatInterval);
|
| 534 |
+
heartbeatInterval = setInterval(() => {
|
| 535 |
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
| 536 |
+
ws.send(JSON.stringify({ type: 'ping' }));
|
| 537 |
+
}
|
| 538 |
+
}, 30000);
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
function updateWSStatus(connected) {
|
| 542 |
+
const indicator = document.getElementById('wsStatus');
|
| 543 |
+
const text = document.getElementById('wsStatusText');
|
| 544 |
+
|
| 545 |
+
if (connected) {
|
| 546 |
+
indicator.classList.remove('disconnected');
|
| 547 |
+
text.textContent = 'Connected';
|
| 548 |
+
} else {
|
| 549 |
+
indicator.classList.add('disconnected');
|
| 550 |
+
text.textContent = 'Disconnected';
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
function handleWSMessage(message) {
|
| 555 |
+
console.log('Received:', message);
|
| 556 |
+
|
| 557 |
+
switch (message.type) {
|
| 558 |
+
case 'api_update':
|
| 559 |
+
handleApiUpdate(message);
|
| 560 |
+
break;
|
| 561 |
+
case 'status_update':
|
| 562 |
+
handleStatusUpdate(message);
|
| 563 |
+
break;
|
| 564 |
+
case 'schedule_update':
|
| 565 |
+
handleScheduleUpdate(message);
|
| 566 |
+
break;
|
| 567 |
+
case 'subscribed':
|
| 568 |
+
addLog(`Subscribed to ${message.api_id || 'all updates'}`);
|
| 569 |
+
break;
|
| 570 |
+
}
|
| 571 |
+
}
|
| 572 |
+
|
| 573 |
+
function handleApiUpdate(message) {
|
| 574 |
+
addLog(`Updated: ${message.api_id}`, 'success');
|
| 575 |
+
loadSystemStatus();
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
function handleStatusUpdate(message) {
|
| 579 |
+
addLog('System status updated');
|
| 580 |
+
loadSystemStatus();
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
function handleScheduleUpdate(message) {
|
| 584 |
+
addLog(`Schedule updated for ${message.schedule.api_id}`);
|
| 585 |
+
loadAPIs();
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
function addLog(text, type = 'info') {
|
| 589 |
+
const logContainer = document.getElementById('activityLog');
|
| 590 |
+
const time = new Date().toLocaleTimeString();
|
| 591 |
+
|
| 592 |
+
const entry = document.createElement('div');
|
| 593 |
+
entry.className = 'log-entry';
|
| 594 |
+
entry.innerHTML = `<span class="log-time">${time}</span>${text}`;
|
| 595 |
+
|
| 596 |
+
logContainer.insertBefore(entry, logContainer.firstChild);
|
| 597 |
+
|
| 598 |
+
// Keep only last 50 entries
|
| 599 |
+
while (logContainer.children.length > 50) {
|
| 600 |
+
logContainer.removeChild(logContainer.lastChild);
|
| 601 |
+
}
|
| 602 |
+
}
|
| 603 |
+
|
| 604 |
+
function showToast(message, duration = 3000) {
|
| 605 |
+
const toast = document.getElementById('toast');
|
| 606 |
+
toast.textContent = message;
|
| 607 |
+
toast.classList.add('show');
|
| 608 |
+
|
| 609 |
+
setTimeout(() => {
|
| 610 |
+
toast.classList.remove('show');
|
| 611 |
+
}, duration);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
// Load system status
|
| 615 |
+
async function loadSystemStatus() {
|
| 616 |
+
try {
|
| 617 |
+
const response = await fetch('/api/v2/status');
|
| 618 |
+
const data = await response.json();
|
| 619 |
+
|
| 620 |
+
document.getElementById('totalApis').textContent =
|
| 621 |
+
data.services.config_loader.apis_loaded;
|
| 622 |
+
document.getElementById('activeTasks').textContent =
|
| 623 |
+
data.services.scheduler.total_tasks;
|
| 624 |
+
document.getElementById('cachedData').textContent =
|
| 625 |
+
data.services.persistence.cached_apis;
|
| 626 |
+
document.getElementById('wsConnections').textContent =
|
| 627 |
+
data.services.websocket.total_connections;
|
| 628 |
+
|
| 629 |
+
} catch (error) {
|
| 630 |
+
console.error('Error loading status:', error);
|
| 631 |
+
}
|
| 632 |
+
}
|
| 633 |
+
|
| 634 |
+
// Load APIs
|
| 635 |
+
async function loadAPIs() {
|
| 636 |
+
try {
|
| 637 |
+
const response = await fetch('/api/v2/config/apis');
|
| 638 |
+
const data = await response.json();
|
| 639 |
+
|
| 640 |
+
const scheduleResponse = await fetch('/api/v2/schedule/tasks');
|
| 641 |
+
const schedules = await scheduleResponse.json();
|
| 642 |
+
|
| 643 |
+
displayAPIs(data.apis, schedules);
|
| 644 |
+
|
| 645 |
+
} catch (error) {
|
| 646 |
+
console.error('Error loading APIs:', error);
|
| 647 |
+
}
|
| 648 |
+
}
|
| 649 |
+
|
| 650 |
+
function displayAPIs(apis, schedules) {
|
| 651 |
+
const listElement = document.getElementById('apiList');
|
| 652 |
+
listElement.innerHTML = '';
|
| 653 |
+
|
| 654 |
+
for (const [apiId, api] of Object.entries(apis)) {
|
| 655 |
+
const schedule = schedules[apiId] || {};
|
| 656 |
+
|
| 657 |
+
const item = document.createElement('div');
|
| 658 |
+
item.className = 'api-item';
|
| 659 |
+
item.innerHTML = `
|
| 660 |
+
<div class="api-info">
|
| 661 |
+
<div class="api-name">${api.name}</div>
|
| 662 |
+
<div class="api-meta">
|
| 663 |
+
<span>📂 ${api.category}</span>
|
| 664 |
+
<span>⏱️ ${schedule.interval || 300}s</span>
|
| 665 |
+
<span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}">
|
| 666 |
+
${schedule.last_status || 'pending'}
|
| 667 |
+
</span>
|
| 668 |
+
</div>
|
| 669 |
+
</div>
|
| 670 |
+
<div class="api-controls">
|
| 671 |
+
<button class="small-btn" onclick="forceUpdate('${apiId}')">🔄 Update</button>
|
| 672 |
+
<button class="small-btn" onclick="showScheduleModalFor('${apiId}')">⚙️ Schedule</button>
|
| 673 |
+
</div>
|
| 674 |
+
`;
|
| 675 |
+
|
| 676 |
+
listElement.appendChild(item);
|
| 677 |
+
}
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
// Export functions
|
| 681 |
+
async function exportJSON() {
|
| 682 |
+
try {
|
| 683 |
+
const response = await fetch('/api/v2/export/json', {
|
| 684 |
+
method: 'POST',
|
| 685 |
+
headers: { 'Content-Type': 'application/json' },
|
| 686 |
+
body: JSON.stringify({ include_history: true })
|
| 687 |
+
});
|
| 688 |
+
|
| 689 |
+
const data = await response.json();
|
| 690 |
+
showToast('✅ JSON export created!');
|
| 691 |
+
addLog(`Exported to JSON: ${data.filepath}`);
|
| 692 |
+
|
| 693 |
+
// Trigger download
|
| 694 |
+
window.open(data.download_url, '_blank');
|
| 695 |
+
|
| 696 |
+
} catch (error) {
|
| 697 |
+
showToast('❌ Export failed');
|
| 698 |
+
console.error(error);
|
| 699 |
+
}
|
| 700 |
+
}
|
| 701 |
+
|
| 702 |
+
async function exportCSV() {
|
| 703 |
+
try {
|
| 704 |
+
const response = await fetch('/api/v2/export/csv', {
|
| 705 |
+
method: 'POST',
|
| 706 |
+
headers: { 'Content-Type': 'application/json' },
|
| 707 |
+
body: JSON.stringify({ flatten: true })
|
| 708 |
+
});
|
| 709 |
+
|
| 710 |
+
const data = await response.json();
|
| 711 |
+
showToast('✅ CSV export created!');
|
| 712 |
+
addLog(`Exported to CSV: ${data.filepath}`);
|
| 713 |
+
|
| 714 |
+
// Trigger download
|
| 715 |
+
window.open(data.download_url, '_blank');
|
| 716 |
+
|
| 717 |
+
} catch (error) {
|
| 718 |
+
showToast('❌ Export failed');
|
| 719 |
+
console.error(error);
|
| 720 |
+
}
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
async function createBackup() {
|
| 724 |
+
try {
|
| 725 |
+
const response = await fetch('/api/v2/backup', {
|
| 726 |
+
method: 'POST'
|
| 727 |
+
});
|
| 728 |
+
|
| 729 |
+
const data = await response.json();
|
| 730 |
+
showToast('✅ Backup created!');
|
| 731 |
+
addLog(`Backup created: ${data.backup_file}`);
|
| 732 |
+
|
| 733 |
+
} catch (error) {
|
| 734 |
+
showToast('❌ Backup failed');
|
| 735 |
+
console.error(error);
|
| 736 |
+
}
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
async function forceUpdate(apiId) {
|
| 740 |
+
try {
|
| 741 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, {
|
| 742 |
+
method: 'POST'
|
| 743 |
+
});
|
| 744 |
+
|
| 745 |
+
const data = await response.json();
|
| 746 |
+
showToast(`✅ ${apiId} updated!`);
|
| 747 |
+
addLog(`Forced update: ${apiId}`);
|
| 748 |
+
loadAPIs();
|
| 749 |
+
|
| 750 |
+
} catch (error) {
|
| 751 |
+
showToast('❌ Update failed');
|
| 752 |
+
console.error(error);
|
| 753 |
+
}
|
| 754 |
+
}
|
| 755 |
+
|
| 756 |
+
async function forceUpdateAll() {
|
| 757 |
+
showToast('🔄 Updating all APIs...');
|
| 758 |
+
addLog('Forcing update for all APIs');
|
| 759 |
+
|
| 760 |
+
try {
|
| 761 |
+
const response = await fetch('/api/v2/config/apis');
|
| 762 |
+
const data = await response.json();
|
| 763 |
+
|
| 764 |
+
for (const apiId of Object.keys(data.apis)) {
|
| 765 |
+
await forceUpdate(apiId);
|
| 766 |
+
await new Promise(resolve => setTimeout(resolve, 100)); // Small delay
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
showToast('✅ All APIs updated!');
|
| 770 |
+
} catch (error) {
|
| 771 |
+
showToast('❌ Update failed');
|
| 772 |
+
console.error(error);
|
| 773 |
+
}
|
| 774 |
+
}
|
| 775 |
+
|
| 776 |
+
async function clearCache() {
|
| 777 |
+
if (!confirm('Clear all cached data?')) return;
|
| 778 |
+
|
| 779 |
+
try {
|
| 780 |
+
const response = await fetch('/api/v2/cleanup/cache', {
|
| 781 |
+
method: 'POST'
|
| 782 |
+
});
|
| 783 |
+
|
| 784 |
+
showToast('✅ Cache cleared!');
|
| 785 |
+
addLog('Cache cleared');
|
| 786 |
+
loadSystemStatus();
|
| 787 |
+
|
| 788 |
+
} catch (error) {
|
| 789 |
+
showToast('❌ Failed to clear cache');
|
| 790 |
+
console.error(error);
|
| 791 |
+
}
|
| 792 |
+
}
|
| 793 |
+
|
| 794 |
+
// Schedule modal functions
|
| 795 |
+
function showScheduleModal() {
|
| 796 |
+
loadAPISelectOptions();
|
| 797 |
+
document.getElementById('scheduleModal').classList.add('active');
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
function closeScheduleModal() {
|
| 801 |
+
document.getElementById('scheduleModal').classList.remove('active');
|
| 802 |
+
}
|
| 803 |
+
|
| 804 |
+
async function showScheduleModalFor(apiId) {
|
| 805 |
+
await loadAPISelectOptions();
|
| 806 |
+
document.getElementById('scheduleApiSelect').value = apiId;
|
| 807 |
+
|
| 808 |
+
// Load current schedule
|
| 809 |
+
try {
|
| 810 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}`);
|
| 811 |
+
const schedule = await response.json();
|
| 812 |
+
|
| 813 |
+
document.getElementById('scheduleInterval').value = schedule.interval;
|
| 814 |
+
document.getElementById('scheduleEnabled').checked = schedule.enabled;
|
| 815 |
+
|
| 816 |
+
} catch (error) {
|
| 817 |
+
console.error(error);
|
| 818 |
+
}
|
| 819 |
+
|
| 820 |
+
showScheduleModal();
|
| 821 |
+
}
|
| 822 |
+
|
| 823 |
+
async function loadAPISelectOptions() {
|
| 824 |
+
try {
|
| 825 |
+
const response = await fetch('/api/v2/config/apis');
|
| 826 |
+
const data = await response.json();
|
| 827 |
+
|
| 828 |
+
const select = document.getElementById('scheduleApiSelect');
|
| 829 |
+
select.innerHTML = '';
|
| 830 |
+
|
| 831 |
+
for (const [apiId, api] of Object.entries(data.apis)) {
|
| 832 |
+
const option = document.createElement('option');
|
| 833 |
+
option.value = apiId;
|
| 834 |
+
option.textContent = api.name;
|
| 835 |
+
select.appendChild(option);
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
} catch (error) {
|
| 839 |
+
console.error(error);
|
| 840 |
+
}
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
async function updateSchedule() {
|
| 844 |
+
const apiId = document.getElementById('scheduleApiSelect').value;
|
| 845 |
+
const interval = parseInt(document.getElementById('scheduleInterval').value);
|
| 846 |
+
const enabled = document.getElementById('scheduleEnabled').checked;
|
| 847 |
+
|
| 848 |
+
try {
|
| 849 |
+
const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, {
|
| 850 |
+
method: 'PUT'
|
| 851 |
+
});
|
| 852 |
+
|
| 853 |
+
const data = await response.json();
|
| 854 |
+
showToast('✅ Schedule updated!');
|
| 855 |
+
addLog(`Updated schedule for ${apiId}`);
|
| 856 |
+
closeScheduleModal();
|
| 857 |
+
loadAPIs();
|
| 858 |
+
|
| 859 |
+
} catch (error) {
|
| 860 |
+
showToast('❌ Schedule update failed');
|
| 861 |
+
console.error(error);
|
| 862 |
+
}
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
// Initialize on load
|
| 866 |
+
window.addEventListener('load', () => {
|
| 867 |
+
initWebSocket();
|
| 868 |
+
loadSystemStatus();
|
| 869 |
+
loadAPIs();
|
| 870 |
+
|
| 871 |
+
// Refresh status every 30 seconds
|
| 872 |
+
setInterval(loadSystemStatus, 30000);
|
| 873 |
+
});
|
| 874 |
+
</script>
|
| 875 |
+
</body>
|
| 876 |
+
</html>
|
main.py
CHANGED
|
@@ -1,168 +1,3 @@
|
|
| 1 |
-
from
|
| 2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
import ccxt
|
| 4 |
-
import os
|
| 5 |
-
import logging
|
| 6 |
-
from datetime import datetime
|
| 7 |
-
import pandas as pd
|
| 8 |
-
import numpy as np
|
| 9 |
|
| 10 |
-
|
| 11 |
-
logging.basicConfig(level=logging.INFO)
|
| 12 |
-
logger = logging.getLogger(__name__)
|
| 13 |
-
|
| 14 |
-
# Create data and logs directories
|
| 15 |
-
os.makedirs("data", exist_ok=True)
|
| 16 |
-
os.makedirs("logs", exist_ok=True)
|
| 17 |
-
|
| 18 |
-
# Initialize FastAPI app
|
| 19 |
-
app = FastAPI(
|
| 20 |
-
title="Cryptocurrency Data Source API",
|
| 21 |
-
description="API for fetching cryptocurrency market data and technical indicators",
|
| 22 |
-
version="1.0.0",
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
# Configure CORS
|
| 26 |
-
app.add_middleware(
|
| 27 |
-
CORSMiddleware,
|
| 28 |
-
allow_origins=["*"],
|
| 29 |
-
allow_credentials=True,
|
| 30 |
-
allow_methods=["*"],
|
| 31 |
-
allow_headers=["*"],
|
| 32 |
-
)
|
| 33 |
-
|
| 34 |
-
@app.get("/")
|
| 35 |
-
async def root():
|
| 36 |
-
"""Root endpoint showing API status"""
|
| 37 |
-
return {
|
| 38 |
-
"status": "online",
|
| 39 |
-
"version": "1.0.0",
|
| 40 |
-
"timestamp": datetime.now().isoformat(),
|
| 41 |
-
"ccxt_version": ccxt.__version__
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
@app.get("/exchanges")
|
| 45 |
-
async def get_exchanges():
|
| 46 |
-
"""List available exchanges"""
|
| 47 |
-
# Get list of exchanges that support OHLCV data
|
| 48 |
-
exchanges = []
|
| 49 |
-
for exchange_id in ccxt.exchanges:
|
| 50 |
-
try:
|
| 51 |
-
exchange = getattr(ccxt, exchange_id)()
|
| 52 |
-
if exchange.has.get('fetchOHLCV'):
|
| 53 |
-
exchanges.append({
|
| 54 |
-
"id": exchange_id,
|
| 55 |
-
"name": exchange.name if hasattr(exchange, 'name') else exchange_id,
|
| 56 |
-
"url": exchange.urls.get('www') if hasattr(exchange, 'urls') and exchange.urls else None
|
| 57 |
-
})
|
| 58 |
-
except:
|
| 59 |
-
# Skip exchanges that fail to initialize
|
| 60 |
-
continue
|
| 61 |
-
|
| 62 |
-
return {"total": len(exchanges), "exchanges": exchanges}
|
| 63 |
-
|
| 64 |
-
@app.get("/markets/{exchange_id}")
|
| 65 |
-
async def get_markets(exchange_id: str):
|
| 66 |
-
"""Get markets for a specific exchange"""
|
| 67 |
-
try:
|
| 68 |
-
# Check if exchange exists
|
| 69 |
-
if exchange_id not in ccxt.exchanges:
|
| 70 |
-
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 71 |
-
|
| 72 |
-
# Initialize exchange
|
| 73 |
-
exchange = getattr(ccxt, exchange_id)({
|
| 74 |
-
'enableRateLimit': True
|
| 75 |
-
})
|
| 76 |
-
|
| 77 |
-
# Fetch markets
|
| 78 |
-
markets = exchange.load_markets()
|
| 79 |
-
|
| 80 |
-
# Format response
|
| 81 |
-
result = []
|
| 82 |
-
for symbol, market in markets.items():
|
| 83 |
-
if market.get('active', False):
|
| 84 |
-
result.append({
|
| 85 |
-
"symbol": symbol,
|
| 86 |
-
"base": market.get('base', ''),
|
| 87 |
-
"quote": market.get('quote', ''),
|
| 88 |
-
"type": market.get('type', 'spot')
|
| 89 |
-
})
|
| 90 |
-
|
| 91 |
-
return {"exchange": exchange_id, "total": len(result), "markets": result[:100]}
|
| 92 |
-
|
| 93 |
-
except ccxt.BaseError as e:
|
| 94 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 95 |
-
except Exception as e:
|
| 96 |
-
logger.error(f"Error fetching markets for {exchange_id}: {str(e)}")
|
| 97 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 98 |
-
|
| 99 |
-
@app.get("/ohlcv/{exchange_id}/{symbol}")
|
| 100 |
-
async def get_ohlcv(exchange_id: str, symbol: str, timeframe: str = "1h", limit: int = 100):
|
| 101 |
-
"""Get OHLCV data for a specific market"""
|
| 102 |
-
try:
|
| 103 |
-
# Check if exchange exists
|
| 104 |
-
if exchange_id not in ccxt.exchanges:
|
| 105 |
-
raise HTTPException(status_code=404, detail=f"Exchange {exchange_id} not found")
|
| 106 |
-
|
| 107 |
-
# Initialize exchange
|
| 108 |
-
exchange = getattr(ccxt, exchange_id)({
|
| 109 |
-
'enableRateLimit': True
|
| 110 |
-
})
|
| 111 |
-
|
| 112 |
-
# Check if exchange supports OHLCV
|
| 113 |
-
if not exchange.has.get('fetchOHLCV'):
|
| 114 |
-
raise HTTPException(
|
| 115 |
-
status_code=400,
|
| 116 |
-
detail=f"Exchange {exchange_id} does not support OHLCV data"
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
# Check timeframe
|
| 120 |
-
if timeframe not in exchange.timeframes:
|
| 121 |
-
raise HTTPException(
|
| 122 |
-
status_code=400,
|
| 123 |
-
detail=f"Timeframe {timeframe} not supported by {exchange_id}"
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
# Fetch OHLCV data
|
| 127 |
-
ohlcv = exchange.fetch_ohlcv(symbol=symbol, timeframe=timeframe, limit=limit)
|
| 128 |
-
|
| 129 |
-
# Convert to readable format
|
| 130 |
-
result = []
|
| 131 |
-
for candle in ohlcv:
|
| 132 |
-
timestamp, open_price, high, low, close, volume = candle
|
| 133 |
-
result.append({
|
| 134 |
-
"timestamp": timestamp,
|
| 135 |
-
"datetime": datetime.fromtimestamp(timestamp / 1000).isoformat(),
|
| 136 |
-
"open": open_price,
|
| 137 |
-
"high": high,
|
| 138 |
-
"low": low,
|
| 139 |
-
"close": close,
|
| 140 |
-
"volume": volume
|
| 141 |
-
})
|
| 142 |
-
|
| 143 |
-
return {
|
| 144 |
-
"exchange": exchange_id,
|
| 145 |
-
"symbol": symbol,
|
| 146 |
-
"timeframe": timeframe,
|
| 147 |
-
"data": result
|
| 148 |
-
}
|
| 149 |
-
|
| 150 |
-
except ccxt.BaseError as e:
|
| 151 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 152 |
-
except Exception as e:
|
| 153 |
-
logger.error(f"Error fetching OHLCV data: {str(e)}")
|
| 154 |
-
raise HTTPException(status_code=500, detail=str(e))
|
| 155 |
-
|
| 156 |
-
@app.get("/health")
|
| 157 |
-
async def health_check():
|
| 158 |
-
"""Health check endpoint"""
|
| 159 |
-
return {
|
| 160 |
-
"status": "healthy",
|
| 161 |
-
"timestamp": datetime.now().isoformat(),
|
| 162 |
-
"version": "1.0.0"
|
| 163 |
-
}
|
| 164 |
-
|
| 165 |
-
if __name__ == "__main__":
|
| 166 |
-
import uvicorn
|
| 167 |
-
port = int(os.getenv("PORT", 7860))
|
| 168 |
-
uvicorn.run("main:app", host="0.0.0.0", port=port, log_level="info")
|
|
|
|
| 1 |
+
from app import app as fastapi_app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
app = fastapi_app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
fastapi==0.104.1
|
| 2 |
uvicorn[standard]==0.24.0
|
|
|
|
| 3 |
websockets==12.0
|
| 4 |
-
python-multipart==0.0.6
|
| 5 |
-
pydantic==2.5.0
|
|
|
|
| 1 |
fastapi==0.104.1
|
| 2 |
uvicorn[standard]==0.24.0
|
| 3 |
+
aiohttp==3.9.1
|
| 4 |
websockets==12.0
|
|
|
|
|
|
start.bat
CHANGED
|
@@ -1,81 +1,53 @@
|
|
| 1 |
@echo off
|
| 2 |
chcp 65001 > nul
|
| 3 |
-
title Crypto
|
| 4 |
|
| 5 |
echo ========================================
|
| 6 |
-
echo 🚀 Crypto
|
| 7 |
-
echo Real-time
|
| 8 |
echo ========================================
|
| 9 |
echo.
|
| 10 |
|
| 11 |
-
REM Check if Python is installed
|
| 12 |
python --version > nul 2>&1
|
| 13 |
if %errorlevel% neq 0 (
|
| 14 |
-
echo ❌ Python
|
| 15 |
-
echo.
|
| 16 |
-
echo Please install Python 3.8 or higher from:
|
| 17 |
-
echo https://www.python.org/downloads/
|
| 18 |
-
echo.
|
| 19 |
-
echo Make sure to check "Add Python to PATH" during installation
|
| 20 |
pause
|
| 21 |
exit /b 1
|
| 22 |
)
|
| 23 |
|
| 24 |
-
echo ✅ Python
|
| 25 |
echo.
|
| 26 |
|
| 27 |
-
REM Check if virtual environment exists
|
| 28 |
if not exist "venv" (
|
| 29 |
echo 📦 Creating virtual environment...
|
| 30 |
python -m venv venv
|
| 31 |
-
if %errorlevel% neq 0 (
|
| 32 |
-
echo ❌ Failed to create virtual environment
|
| 33 |
-
pause
|
| 34 |
-
exit /b 1
|
| 35 |
-
)
|
| 36 |
-
echo ✅ Virtual environment created
|
| 37 |
-
echo.
|
| 38 |
)
|
| 39 |
|
| 40 |
-
|
| 41 |
-
echo 🔧 Activating virtual environment...
|
| 42 |
call venv\Scripts\activate.bat
|
| 43 |
-
if %errorlevel% neq 0 (
|
| 44 |
-
echo ❌ Failed to activate virtual environment
|
| 45 |
-
pause
|
| 46 |
-
exit /b 1
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
-
echo ✅ Virtual environment activated
|
| 50 |
-
echo.
|
| 51 |
|
| 52 |
-
|
| 53 |
-
echo 📥 Installing dependencies...
|
| 54 |
pip install -q -r requirements.txt
|
| 55 |
-
if %errorlevel% neq 0 (
|
| 56 |
-
echo ❌ Failed to install dependencies
|
| 57 |
-
pause
|
| 58 |
-
exit /b 1
|
| 59 |
-
)
|
| 60 |
|
| 61 |
-
echo ✅ Dependencies installed
|
| 62 |
echo.
|
| 63 |
echo ========================================
|
| 64 |
-
echo 🎯 Starting Server...
|
| 65 |
echo ========================================
|
| 66 |
echo.
|
| 67 |
-
echo 📊 Dashboard
|
| 68 |
-
echo
|
| 69 |
-
echo.
|
| 70 |
-
echo 📡 API Documentation:
|
| 71 |
-
echo http://localhost:8000/docs
|
| 72 |
echo.
|
| 73 |
-
echo 💡
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
echo.
|
|
|
|
| 75 |
echo ========================================
|
| 76 |
echo.
|
| 77 |
|
| 78 |
-
REM Start the application
|
| 79 |
python app.py
|
| 80 |
|
| 81 |
pause
|
|
|
|
| 1 |
@echo off
|
| 2 |
chcp 65001 > nul
|
| 3 |
+
title Crypto Monitor ULTIMATE - Real APIs
|
| 4 |
|
| 5 |
echo ========================================
|
| 6 |
+
echo 🚀 Crypto Monitor ULTIMATE
|
| 7 |
+
echo Real-time Data from 100+ Free APIs
|
| 8 |
echo ========================================
|
| 9 |
echo.
|
| 10 |
|
|
|
|
| 11 |
python --version > nul 2>&1
|
| 12 |
if %errorlevel% neq 0 (
|
| 13 |
+
echo ❌ Python not found!
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
pause
|
| 15 |
exit /b 1
|
| 16 |
)
|
| 17 |
|
| 18 |
+
echo ✅ Python found
|
| 19 |
echo.
|
| 20 |
|
|
|
|
| 21 |
if not exist "venv" (
|
| 22 |
echo 📦 Creating virtual environment...
|
| 23 |
python -m venv venv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
|
| 26 |
+
echo 🔧 Activating environment...
|
|
|
|
| 27 |
call venv\Scripts\activate.bat
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
echo 📥 Installing packages...
|
|
|
|
| 30 |
pip install -q -r requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
|
|
|
| 32 |
echo.
|
| 33 |
echo ========================================
|
| 34 |
+
echo 🎯 Starting Real-time Server...
|
| 35 |
echo ========================================
|
| 36 |
echo.
|
| 37 |
+
echo 📊 Dashboard: http://localhost:8000/dashboard
|
| 38 |
+
echo 📡 API Docs: http://localhost:8000/docs
|
|
|
|
|
|
|
|
|
|
| 39 |
echo.
|
| 40 |
+
echo 💡 Real APIs:
|
| 41 |
+
echo ✓ CoinGecko - Market Data
|
| 42 |
+
echo ✓ CoinCap - Price Data
|
| 43 |
+
echo ✓ Binance - Exchange Data
|
| 44 |
+
echo ✓ Fear & Greed Index
|
| 45 |
+
echo ✓ DeFi Llama - TVL Data
|
| 46 |
echo.
|
| 47 |
+
echo Press Ctrl+C to stop
|
| 48 |
echo ========================================
|
| 49 |
echo.
|
| 50 |
|
|
|
|
| 51 |
python app.py
|
| 52 |
|
| 53 |
pause
|
unified_dashboard.html
ADDED
|
@@ -0,0 +1,2107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>Crypto Monitor ULTIMATE - Unified Dashboard</title>
|
| 7 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
| 8 |
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
| 9 |
+
<style>
|
| 10 |
+
* {
|
| 11 |
+
margin: 0;
|
| 12 |
+
padding: 0;
|
| 13 |
+
box-sizing: border-box;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
:root {
|
| 17 |
+
--bg-dark: #0a0e1a;
|
| 18 |
+
--bg-card: #111827;
|
| 19 |
+
--bg-card-hover: #1f2937;
|
| 20 |
+
--text-primary: #f9fafb;
|
| 21 |
+
--text-secondary: #9ca3af;
|
| 22 |
+
--accent-blue: #3b82f6;
|
| 23 |
+
--accent-green: #10b981;
|
| 24 |
+
--accent-red: #ef4444;
|
| 25 |
+
--accent-yellow: #f59e0b;
|
| 26 |
+
--accent-purple: #8b5cf6;
|
| 27 |
+
--accent-pink: #ec4899;
|
| 28 |
+
--border: rgba(255, 255, 255, 0.1);
|
| 29 |
+
--shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
| 34 |
+
background: var(--bg-dark);
|
| 35 |
+
color: var(--text-primary);
|
| 36 |
+
line-height: 1.6;
|
| 37 |
+
overflow-x: hidden;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
body::before {
|
| 41 |
+
content: '';
|
| 42 |
+
position: fixed;
|
| 43 |
+
top: 0;
|
| 44 |
+
left: 0;
|
| 45 |
+
right: 0;
|
| 46 |
+
bottom: 0;
|
| 47 |
+
background:
|
| 48 |
+
radial-gradient(circle at 20% 30%, rgba(59, 130, 246, 0.15) 0%, transparent 40%),
|
| 49 |
+
radial-gradient(circle at 80% 70%, rgba(139, 92, 246, 0.15) 0%, transparent 40%),
|
| 50 |
+
radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.1) 0%, transparent 30%);
|
| 51 |
+
pointer-events: none;
|
| 52 |
+
z-index: 0;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
.container {
|
| 56 |
+
max-width: 1920px;
|
| 57 |
+
margin: 0 auto;
|
| 58 |
+
padding: 20px;
|
| 59 |
+
position: relative;
|
| 60 |
+
z-index: 1;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
/* Header */
|
| 64 |
+
.header {
|
| 65 |
+
background: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.4) 100%);
|
| 66 |
+
backdrop-filter: blur(20px);
|
| 67 |
+
border: 1px solid var(--border);
|
| 68 |
+
border-radius: 24px;
|
| 69 |
+
padding: 30px;
|
| 70 |
+
margin-bottom: 30px;
|
| 71 |
+
box-shadow: var(--shadow);
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.header-top {
|
| 75 |
+
display: flex;
|
| 76 |
+
align-items: center;
|
| 77 |
+
justify-content: space-between;
|
| 78 |
+
flex-wrap: wrap;
|
| 79 |
+
gap: 20px;
|
| 80 |
+
margin-bottom: 20px;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.logo {
|
| 84 |
+
display: flex;
|
| 85 |
+
align-items: center;
|
| 86 |
+
gap: 15px;
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
.logo-icon {
|
| 90 |
+
width: 60px;
|
| 91 |
+
height: 60px;
|
| 92 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899);
|
| 93 |
+
border-radius: 16px;
|
| 94 |
+
display: flex;
|
| 95 |
+
align-items: center;
|
| 96 |
+
justify-content: center;
|
| 97 |
+
font-size: 32px;
|
| 98 |
+
box-shadow: 0 10px 40px rgba(59, 130, 246, 0.4);
|
| 99 |
+
animation: pulse-glow 3s ease-in-out infinite;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
@keyframes pulse-glow {
|
| 103 |
+
0%, 100% { box-shadow: 0 10px 40px rgba(59, 130, 246, 0.4); }
|
| 104 |
+
50% { box-shadow: 0 10px 60px rgba(139, 92, 246, 0.6); }
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
.logo-text h1 {
|
| 108 |
+
font-size: 32px;
|
| 109 |
+
font-weight: 900;
|
| 110 |
+
background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899);
|
| 111 |
+
-webkit-background-clip: text;
|
| 112 |
+
-webkit-text-fill-color: transparent;
|
| 113 |
+
background-clip: text;
|
| 114 |
+
letter-spacing: -1px;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
.logo-text p {
|
| 118 |
+
font-size: 14px;
|
| 119 |
+
color: var(--text-secondary);
|
| 120 |
+
font-weight: 500;
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
.header-actions {
|
| 124 |
+
display: flex;
|
| 125 |
+
gap: 12px;
|
| 126 |
+
align-items: center;
|
| 127 |
+
flex-wrap: wrap;
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.status-badge {
|
| 131 |
+
display: flex;
|
| 132 |
+
align-items: center;
|
| 133 |
+
gap: 8px;
|
| 134 |
+
padding: 12px 24px;
|
| 135 |
+
background: rgba(16, 185, 129, 0.15);
|
| 136 |
+
border: 1px solid rgba(16, 185, 129, 0.3);
|
| 137 |
+
border-radius: 12px;
|
| 138 |
+
font-size: 14px;
|
| 139 |
+
font-weight: 600;
|
| 140 |
+
}
|
| 141 |
+
|
| 142 |
+
.status-dot {
|
| 143 |
+
width: 10px;
|
| 144 |
+
height: 10px;
|
| 145 |
+
background: var(--accent-green);
|
| 146 |
+
border-radius: 50%;
|
| 147 |
+
animation: pulse 2s infinite;
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
@keyframes pulse {
|
| 151 |
+
0%, 100% { opacity: 1; transform: scale(1); }
|
| 152 |
+
50% { opacity: 0.5; transform: scale(1.2); }
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
.live-indicator {
|
| 156 |
+
padding: 8px 16px;
|
| 157 |
+
background: rgba(239, 68, 68, 0.15);
|
| 158 |
+
border: 1px solid rgba(239, 68, 68, 0.3);
|
| 159 |
+
border-radius: 8px;
|
| 160 |
+
font-size: 12px;
|
| 161 |
+
font-weight: 700;
|
| 162 |
+
text-transform: uppercase;
|
| 163 |
+
color: var(--accent-red);
|
| 164 |
+
animation: blink 2s infinite;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
@keyframes blink {
|
| 168 |
+
0%, 50%, 100% { opacity: 1; }
|
| 169 |
+
25%, 75% { opacity: 0.3; }
|
| 170 |
+
}
|
| 171 |
+
|
| 172 |
+
/* Tabs */
|
| 173 |
+
.tabs {
|
| 174 |
+
display: flex;
|
| 175 |
+
gap: 10px;
|
| 176 |
+
margin-bottom: 30px;
|
| 177 |
+
flex-wrap: wrap;
|
| 178 |
+
border-bottom: 2px solid var(--border);
|
| 179 |
+
padding-bottom: 10px;
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
.tab {
|
| 183 |
+
padding: 12px 24px;
|
| 184 |
+
background: transparent;
|
| 185 |
+
border: none;
|
| 186 |
+
border-radius: 12px 12px 0 0;
|
| 187 |
+
color: var(--text-secondary);
|
| 188 |
+
font-weight: 600;
|
| 189 |
+
cursor: pointer;
|
| 190 |
+
transition: all 0.3s;
|
| 191 |
+
font-size: 14px;
|
| 192 |
+
position: relative;
|
| 193 |
+
}
|
| 194 |
+
|
| 195 |
+
.tab:hover {
|
| 196 |
+
color: var(--text-primary);
|
| 197 |
+
background: rgba(255, 255, 255, 0.05);
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
.tab.active {
|
| 201 |
+
color: var(--accent-blue);
|
| 202 |
+
background: rgba(59, 130, 246, 0.1);
|
| 203 |
+
}
|
| 204 |
+
|
| 205 |
+
.tab.active::after {
|
| 206 |
+
content: '';
|
| 207 |
+
position: absolute;
|
| 208 |
+
bottom: -12px;
|
| 209 |
+
left: 0;
|
| 210 |
+
right: 0;
|
| 211 |
+
height: 3px;
|
| 212 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple));
|
| 213 |
+
border-radius: 2px;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
.tab-content {
|
| 217 |
+
display: none;
|
| 218 |
+
animation: fadeIn 0.3s;
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
.tab-content.active {
|
| 222 |
+
display: block;
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
@keyframes fadeIn {
|
| 226 |
+
from { opacity: 0; transform: translateY(10px); }
|
| 227 |
+
to { opacity: 1; transform: translateY(0); }
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
/* Stats Grid */
|
| 231 |
+
.stats-grid {
|
| 232 |
+
display: grid;
|
| 233 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 234 |
+
gap: 20px;
|
| 235 |
+
margin-bottom: 30px;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
.stat-card {
|
| 239 |
+
background: rgba(17, 24, 39, 0.6);
|
| 240 |
+
backdrop-filter: blur(10px);
|
| 241 |
+
border: 1px solid var(--border);
|
| 242 |
+
border-radius: 20px;
|
| 243 |
+
padding: 25px;
|
| 244 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 245 |
+
position: relative;
|
| 246 |
+
overflow: hidden;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.stat-card::before {
|
| 250 |
+
content: '';
|
| 251 |
+
position: absolute;
|
| 252 |
+
top: 0;
|
| 253 |
+
left: 0;
|
| 254 |
+
right: 0;
|
| 255 |
+
height: 4px;
|
| 256 |
+
background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink));
|
| 257 |
+
transform: scaleX(0);
|
| 258 |
+
transition: transform 0.3s ease;
|
| 259 |
+
}
|
| 260 |
+
|
| 261 |
+
.stat-card:hover {
|
| 262 |
+
transform: translateY(-8px) scale(1.02);
|
| 263 |
+
border-color: rgba(59, 130, 246, 0.5);
|
| 264 |
+
box-shadow: 0 20px 50px rgba(59, 130, 246, 0.3);
|
| 265 |
+
}
|
| 266 |
+
|
| 267 |
+
.stat-card:hover::before {
|
| 268 |
+
transform: scaleX(1);
|
| 269 |
+
}
|
| 270 |
+
|
| 271 |
+
.stat-header {
|
| 272 |
+
display: flex;
|
| 273 |
+
align-items: center;
|
| 274 |
+
justify-content: space-between;
|
| 275 |
+
margin-bottom: 15px;
|
| 276 |
+
}
|
| 277 |
+
|
| 278 |
+
.stat-icon {
|
| 279 |
+
width: 48px;
|
| 280 |
+
height: 48px;
|
| 281 |
+
border-radius: 12px;
|
| 282 |
+
display: flex;
|
| 283 |
+
align-items: center;
|
| 284 |
+
justify-content: center;
|
| 285 |
+
font-size: 24px;
|
| 286 |
+
background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
.stat-value {
|
| 290 |
+
font-size: 36px;
|
| 291 |
+
font-weight: 900;
|
| 292 |
+
margin-bottom: 5px;
|
| 293 |
+
background: linear-gradient(135deg, var(--text-primary), var(--text-secondary));
|
| 294 |
+
-webkit-background-clip: text;
|
| 295 |
+
-webkit-text-fill-color: transparent;
|
| 296 |
+
background-clip: text;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.stat-label {
|
| 300 |
+
font-size: 13px;
|
| 301 |
+
color: var(--text-secondary);
|
| 302 |
+
text-transform: uppercase;
|
| 303 |
+
letter-spacing: 1px;
|
| 304 |
+
font-weight: 600;
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
.stat-change {
|
| 308 |
+
font-size: 14px;
|
| 309 |
+
font-weight: 700;
|
| 310 |
+
margin-top: 12px;
|
| 311 |
+
display: flex;
|
| 312 |
+
align-items: center;
|
| 313 |
+
gap: 6px;
|
| 314 |
+
}
|
| 315 |
+
|
| 316 |
+
.stat-change.positive { color: var(--accent-green); }
|
| 317 |
+
.stat-change.negative { color: var(--accent-red); }
|
| 318 |
+
|
| 319 |
+
/* Market Table */
|
| 320 |
+
.market-section {
|
| 321 |
+
background: rgba(17, 24, 39, 0.6);
|
| 322 |
+
backdrop-filter: blur(10px);
|
| 323 |
+
border: 1px solid var(--border);
|
| 324 |
+
border-radius: 24px;
|
| 325 |
+
padding: 30px;
|
| 326 |
+
margin-bottom: 30px;
|
| 327 |
+
box-shadow: var(--shadow);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.section-header {
|
| 331 |
+
display: flex;
|
| 332 |
+
align-items: center;
|
| 333 |
+
justify-content: space-between;
|
| 334 |
+
margin-bottom: 25px;
|
| 335 |
+
flex-wrap: wrap;
|
| 336 |
+
gap: 15px;
|
| 337 |
+
}
|
| 338 |
+
|
| 339 |
+
.section-title {
|
| 340 |
+
font-size: 24px;
|
| 341 |
+
font-weight: 800;
|
| 342 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 343 |
+
-webkit-background-clip: text;
|
| 344 |
+
-webkit-text-fill-color: transparent;
|
| 345 |
+
background-clip: text;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.refresh-btn {
|
| 349 |
+
padding: 10px 20px;
|
| 350 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 351 |
+
border: none;
|
| 352 |
+
border-radius: 10px;
|
| 353 |
+
color: white;
|
| 354 |
+
font-weight: 600;
|
| 355 |
+
cursor: pointer;
|
| 356 |
+
transition: all 0.3s ease;
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
.refresh-btn:hover {
|
| 360 |
+
transform: scale(1.05);
|
| 361 |
+
box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4);
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
table {
|
| 365 |
+
width: 100%;
|
| 366 |
+
border-collapse: collapse;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
thead {
|
| 370 |
+
background: rgba(59, 130, 246, 0.1);
|
| 371 |
+
}
|
| 372 |
+
|
| 373 |
+
th {
|
| 374 |
+
padding: 16px;
|
| 375 |
+
text-align: left;
|
| 376 |
+
font-size: 12px;
|
| 377 |
+
font-weight: 700;
|
| 378 |
+
color: var(--text-secondary);
|
| 379 |
+
text-transform: uppercase;
|
| 380 |
+
letter-spacing: 1px;
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
td {
|
| 384 |
+
padding: 16px;
|
| 385 |
+
border-top: 1px solid var(--border);
|
| 386 |
+
font-size: 14px;
|
| 387 |
+
}
|
| 388 |
+
|
| 389 |
+
tr:hover {
|
| 390 |
+
background: rgba(59, 130, 246, 0.05);
|
| 391 |
+
}
|
| 392 |
+
|
| 393 |
+
.crypto-name {
|
| 394 |
+
display: flex;
|
| 395 |
+
align-items: center;
|
| 396 |
+
gap: 12px;
|
| 397 |
+
}
|
| 398 |
+
|
| 399 |
+
.crypto-img {
|
| 400 |
+
width: 36px;
|
| 401 |
+
height: 36px;
|
| 402 |
+
border-radius: 10px;
|
| 403 |
+
object-fit: cover;
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
.price {
|
| 407 |
+
font-weight: 700;
|
| 408 |
+
font-size: 15px;
|
| 409 |
+
}
|
| 410 |
+
|
| 411 |
+
.change {
|
| 412 |
+
padding: 6px 12px;
|
| 413 |
+
border-radius: 8px;
|
| 414 |
+
font-weight: 700;
|
| 415 |
+
font-size: 13px;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
.change.positive {
|
| 419 |
+
background: rgba(16, 185, 129, 0.15);
|
| 420 |
+
color: var(--accent-green);
|
| 421 |
+
}
|
| 422 |
+
|
| 423 |
+
.change.negative {
|
| 424 |
+
background: rgba(239, 68, 68, 0.15);
|
| 425 |
+
color: var(--accent-red);
|
| 426 |
+
}
|
| 427 |
+
|
| 428 |
+
/* Charts */
|
| 429 |
+
.chart-container {
|
| 430 |
+
background: rgba(17, 24, 39, 0.6);
|
| 431 |
+
backdrop-filter: blur(10px);
|
| 432 |
+
border: 1px solid var(--border);
|
| 433 |
+
border-radius: 24px;
|
| 434 |
+
padding: 30px;
|
| 435 |
+
margin-bottom: 30px;
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
canvas {
|
| 439 |
+
max-height: 350px;
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
/* Loading */
|
| 443 |
+
.loading {
|
| 444 |
+
display: flex;
|
| 445 |
+
align-items: center;
|
| 446 |
+
justify-content: center;
|
| 447 |
+
padding: 60px;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.spinner {
|
| 451 |
+
width: 50px;
|
| 452 |
+
height: 50px;
|
| 453 |
+
border: 4px solid var(--border);
|
| 454 |
+
border-top-color: var(--accent-blue);
|
| 455 |
+
border-radius: 50%;
|
| 456 |
+
animation: spin 1s linear infinite;
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
@keyframes spin {
|
| 460 |
+
to { transform: rotate(360deg); }
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
/* Forms */
|
| 464 |
+
.form-group {
|
| 465 |
+
margin-bottom: 20px;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.form-label {
|
| 469 |
+
display: block;
|
| 470 |
+
margin-bottom: 8px;
|
| 471 |
+
font-weight: 600;
|
| 472 |
+
color: var(--text-primary);
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.form-input, .form-select, .form-textarea {
|
| 476 |
+
width: 100%;
|
| 477 |
+
padding: 12px;
|
| 478 |
+
background: rgba(17, 24, 39, 0.6);
|
| 479 |
+
border: 1px solid var(--border);
|
| 480 |
+
border-radius: 8px;
|
| 481 |
+
color: var(--text-primary);
|
| 482 |
+
font-family: inherit;
|
| 483 |
+
font-size: 14px;
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
.form-input:focus, .form-select:focus, .form-textarea:focus {
|
| 487 |
+
outline: none;
|
| 488 |
+
border-color: var(--accent-blue);
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.form-textarea {
|
| 492 |
+
resize: vertical;
|
| 493 |
+
min-height: 100px;
|
| 494 |
+
}
|
| 495 |
+
|
| 496 |
+
/* Badges */
|
| 497 |
+
.badge {
|
| 498 |
+
display: inline-block;
|
| 499 |
+
padding: 4px 12px;
|
| 500 |
+
border-radius: 12px;
|
| 501 |
+
font-size: 12px;
|
| 502 |
+
font-weight: 600;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.badge-success {
|
| 506 |
+
background: rgba(16, 185, 129, 0.15);
|
| 507 |
+
color: var(--accent-green);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.badge-warning {
|
| 511 |
+
background: rgba(245, 158, 11, 0.15);
|
| 512 |
+
color: var(--accent-yellow);
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.badge-danger {
|
| 516 |
+
background: rgba(239, 68, 68, 0.15);
|
| 517 |
+
color: var(--accent-red);
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
.badge-info {
|
| 521 |
+
background: rgba(59, 130, 246, 0.15);
|
| 522 |
+
color: var(--accent-blue);
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
/* Alert */
|
| 526 |
+
.alert {
|
| 527 |
+
padding: 12px 16px;
|
| 528 |
+
border-radius: 8px;
|
| 529 |
+
margin-bottom: 16px;
|
| 530 |
+
font-size: 14px;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
.alert-success {
|
| 534 |
+
background: rgba(16, 185, 129, 0.15);
|
| 535 |
+
color: var(--accent-green);
|
| 536 |
+
border-left: 4px solid var(--accent-green);
|
| 537 |
+
}
|
| 538 |
+
|
| 539 |
+
.alert-error {
|
| 540 |
+
background: rgba(239, 68, 68, 0.15);
|
| 541 |
+
color: var(--accent-red);
|
| 542 |
+
border-left: 4px solid var(--accent-red);
|
| 543 |
+
}
|
| 544 |
+
|
| 545 |
+
/* Responsive */
|
| 546 |
+
@media (max-width: 768px) {
|
| 547 |
+
.stats-grid {
|
| 548 |
+
grid-template-columns: 1fr;
|
| 549 |
+
}
|
| 550 |
+
|
| 551 |
+
.header-top {
|
| 552 |
+
flex-direction: column;
|
| 553 |
+
align-items: flex-start;
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
table {
|
| 557 |
+
font-size: 12px;
|
| 558 |
+
}
|
| 559 |
+
|
| 560 |
+
th, td {
|
| 561 |
+
padding: 10px;
|
| 562 |
+
}
|
| 563 |
+
|
| 564 |
+
.tabs {
|
| 565 |
+
overflow-x: auto;
|
| 566 |
+
}
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
/* Modal Styles */
|
| 570 |
+
.modal {
|
| 571 |
+
display: none;
|
| 572 |
+
position: fixed;
|
| 573 |
+
top: 0;
|
| 574 |
+
left: 0;
|
| 575 |
+
width: 100%;
|
| 576 |
+
height: 100%;
|
| 577 |
+
background: rgba(0, 0, 0, 0.8);
|
| 578 |
+
backdrop-filter: blur(5px);
|
| 579 |
+
z-index: 1000;
|
| 580 |
+
justify-content: center;
|
| 581 |
+
align-items: center;
|
| 582 |
+
animation: fadeIn 0.3s;
|
| 583 |
+
}
|
| 584 |
+
|
| 585 |
+
.modal.active {
|
| 586 |
+
display: flex;
|
| 587 |
+
}
|
| 588 |
+
|
| 589 |
+
.modal-content {
|
| 590 |
+
background: var(--bg-card);
|
| 591 |
+
border-radius: 20px;
|
| 592 |
+
padding: 30px;
|
| 593 |
+
max-width: 600px;
|
| 594 |
+
width: 90%;
|
| 595 |
+
max-height: 90vh;
|
| 596 |
+
overflow-y: auto;
|
| 597 |
+
border: 1px solid var(--border);
|
| 598 |
+
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5);
|
| 599 |
+
animation: slideUp 0.3s;
|
| 600 |
+
}
|
| 601 |
+
|
| 602 |
+
@keyframes slideUp {
|
| 603 |
+
from {
|
| 604 |
+
opacity: 0;
|
| 605 |
+
transform: translateY(30px);
|
| 606 |
+
}
|
| 607 |
+
to {
|
| 608 |
+
opacity: 1;
|
| 609 |
+
transform: translateY(0);
|
| 610 |
+
}
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
/* Improved Button Styles */
|
| 614 |
+
.btn-icon {
|
| 615 |
+
padding: 8px 12px;
|
| 616 |
+
border-radius: 8px;
|
| 617 |
+
border: 1px solid;
|
| 618 |
+
cursor: pointer;
|
| 619 |
+
font-size: 12px;
|
| 620 |
+
font-weight: 600;
|
| 621 |
+
transition: all 0.3s;
|
| 622 |
+
display: inline-flex;
|
| 623 |
+
align-items: center;
|
| 624 |
+
gap: 6px;
|
| 625 |
+
}
|
| 626 |
+
|
| 627 |
+
.btn-icon:hover {
|
| 628 |
+
transform: translateY(-2px);
|
| 629 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
/* Pool Card Hover Effect */
|
| 633 |
+
.pool-card-hover {
|
| 634 |
+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.pool-card-hover:hover {
|
| 638 |
+
transform: translateY(-5px);
|
| 639 |
+
box-shadow: 0 20px 40px rgba(59, 130, 246, 0.2);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
/* Improved Empty States */
|
| 643 |
+
.empty-state {
|
| 644 |
+
text-align: center;
|
| 645 |
+
padding: 60px 20px;
|
| 646 |
+
background: rgba(17, 24, 39, 0.6);
|
| 647 |
+
border-radius: 20px;
|
| 648 |
+
border: 2px dashed var(--border);
|
| 649 |
+
}
|
| 650 |
+
|
| 651 |
+
.empty-state-icon {
|
| 652 |
+
font-size: 64px;
|
| 653 |
+
margin-bottom: 20px;
|
| 654 |
+
opacity: 0.5;
|
| 655 |
+
}
|
| 656 |
+
|
| 657 |
+
/* Loading States */
|
| 658 |
+
.skeleton {
|
| 659 |
+
background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.05) 75%);
|
| 660 |
+
background-size: 200% 100%;
|
| 661 |
+
animation: loading 1.5s infinite;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
@keyframes loading {
|
| 665 |
+
0% { background-position: 200% 0; }
|
| 666 |
+
100% { background-position: -200% 0; }
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
/* Scrollbar */
|
| 670 |
+
::-webkit-scrollbar {
|
| 671 |
+
width: 10px;
|
| 672 |
+
height: 10px;
|
| 673 |
+
}
|
| 674 |
+
|
| 675 |
+
::-webkit-scrollbar-track {
|
| 676 |
+
background: var(--bg-dark);
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
::-webkit-scrollbar-thumb {
|
| 680 |
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
| 681 |
+
border-radius: 5px;
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
::-webkit-scrollbar-thumb:hover {
|
| 685 |
+
background: linear-gradient(135deg, var(--accent-purple), var(--accent-pink));
|
| 686 |
+
}
|
| 687 |
+
|
| 688 |
+
/* Toast Notifications */
|
| 689 |
+
.toast {
|
| 690 |
+
position: fixed;
|
| 691 |
+
bottom: 20px;
|
| 692 |
+
right: 20px;
|
| 693 |
+
background: var(--bg-card);
|
| 694 |
+
border: 1px solid var(--border);
|
| 695 |
+
border-radius: 12px;
|
| 696 |
+
padding: 16px 20px;
|
| 697 |
+
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5);
|
| 698 |
+
z-index: 2000;
|
| 699 |
+
display: flex;
|
| 700 |
+
align-items: center;
|
| 701 |
+
gap: 12px;
|
| 702 |
+
min-width: 300px;
|
| 703 |
+
animation: slideInRight 0.3s;
|
| 704 |
+
}
|
| 705 |
+
|
| 706 |
+
@keyframes slideInRight {
|
| 707 |
+
from {
|
| 708 |
+
opacity: 0;
|
| 709 |
+
transform: translateX(100%);
|
| 710 |
+
}
|
| 711 |
+
to {
|
| 712 |
+
opacity: 1;
|
| 713 |
+
transform: translateX(0);
|
| 714 |
+
}
|
| 715 |
+
}
|
| 716 |
+
|
| 717 |
+
.toast-success {
|
| 718 |
+
border-left: 4px solid var(--accent-green);
|
| 719 |
+
}
|
| 720 |
+
|
| 721 |
+
.toast-error {
|
| 722 |
+
border-left: 4px solid var(--accent-red);
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
.toast-info {
|
| 726 |
+
border-left: 4px solid var(--accent-blue);
|
| 727 |
+
}
|
| 728 |
+
</style>
|
| 729 |
+
</head>
|
| 730 |
+
<body>
|
| 731 |
+
<div class="container">
|
| 732 |
+
<!-- Header -->
|
| 733 |
+
<div class="header">
|
| 734 |
+
<div class="header-top">
|
| 735 |
+
<div class="logo">
|
| 736 |
+
<div class="logo-icon">₿</div>
|
| 737 |
+
<div class="logo-text">
|
| 738 |
+
<h1>Crypto Monitor ULTIMATE</h1>
|
| 739 |
+
<p>Unified Dashboard • Real-time data from 100+ free APIs</p>
|
| 740 |
+
</div>
|
| 741 |
+
</div>
|
| 742 |
+
<div class="header-actions">
|
| 743 |
+
<div class="live-indicator">● LIVE</div>
|
| 744 |
+
<div class="status-badge">
|
| 745 |
+
<div class="status-dot"></div>
|
| 746 |
+
<span id="statusText">All Systems Operational</span>
|
| 747 |
+
</div>
|
| 748 |
+
</div>
|
| 749 |
+
</div>
|
| 750 |
+
|
| 751 |
+
<!-- Tabs -->
|
| 752 |
+
<div class="tabs">
|
| 753 |
+
<button class="tab active" onclick="switchTab('market')">📊 Market</button>
|
| 754 |
+
<button class="tab" onclick="switchTab('monitor')">📡 API Monitor</button>
|
| 755 |
+
<button class="tab" onclick="switchTab('advanced')">⚡ Advanced</button>
|
| 756 |
+
<button class="tab" onclick="switchTab('admin')">⚙️ Admin</button>
|
| 757 |
+
<button class="tab" onclick="switchTab('hf')">🤗 HuggingFace</button>
|
| 758 |
+
<button class="tab" onclick="switchTab('pools')">🔄 Pools</button>
|
| 759 |
+
</div>
|
| 760 |
+
</div>
|
| 761 |
+
|
| 762 |
+
<!-- Market Tab -->
|
| 763 |
+
<div id="tab-market" class="tab-content active">
|
| 764 |
+
<!-- Stats Grid -->
|
| 765 |
+
<div class="stats-grid">
|
| 766 |
+
<div class="stat-card">
|
| 767 |
+
<div class="stat-header">
|
| 768 |
+
<div class="stat-icon">💰</div>
|
| 769 |
+
</div>
|
| 770 |
+
<div class="stat-value" id="totalMarketCap">$0.00T</div>
|
| 771 |
+
<div class="stat-label">Total Market Cap</div>
|
| 772 |
+
<div class="stat-change positive" id="mcapChange">
|
| 773 |
+
<span>↑</span> <span>0.0%</span>
|
| 774 |
+
</div>
|
| 775 |
+
</div>
|
| 776 |
+
|
| 777 |
+
<div class="stat-card">
|
| 778 |
+
<div class="stat-header">
|
| 779 |
+
<div class="stat-icon">📊</div>
|
| 780 |
+
</div>
|
| 781 |
+
<div class="stat-value" id="totalVolume">$0.00B</div>
|
| 782 |
+
<div class="stat-label">24h Trading Volume</div>
|
| 783 |
+
<div class="stat-change positive">
|
| 784 |
+
<span>↑</span> <span>Volume spike</span>
|
| 785 |
+
</div>
|
| 786 |
+
</div>
|
| 787 |
+
|
| 788 |
+
<div class="stat-card">
|
| 789 |
+
<div class="stat-header">
|
| 790 |
+
<div class="stat-icon">₿</div>
|
| 791 |
+
</div>
|
| 792 |
+
<div class="stat-value" id="btcDominance">0.0%</div>
|
| 793 |
+
<div class="stat-label">BTC Dominance</div>
|
| 794 |
+
<div class="stat-change">
|
| 795 |
+
<span id="btcDomIcon">↑</span> <span id="btcDomChange">0.0%</span>
|
| 796 |
+
</div>
|
| 797 |
+
</div>
|
| 798 |
+
|
| 799 |
+
<div class="stat-card">
|
| 800 |
+
<div class="stat-header">
|
| 801 |
+
<div class="stat-icon">🔥</div>
|
| 802 |
+
</div>
|
| 803 |
+
<div class="stat-value" id="fearGreed">50</div>
|
| 804 |
+
<div class="stat-label">Fear & Greed Index</div>
|
| 805 |
+
<div class="stat-change" id="sentimentLabel">
|
| 806 |
+
<span>Neutral</span>
|
| 807 |
+
</div>
|
| 808 |
+
</div>
|
| 809 |
+
</div>
|
| 810 |
+
|
| 811 |
+
<!-- Market Table -->
|
| 812 |
+
<div class="market-section">
|
| 813 |
+
<div class="section-header">
|
| 814 |
+
<div class="section-title">💎 Live Market Data</div>
|
| 815 |
+
<button class="refresh-btn" onclick="loadMarketData()">↻ Refresh</button>
|
| 816 |
+
</div>
|
| 817 |
+
<div style="overflow-x: auto;">
|
| 818 |
+
<table id="marketTable">
|
| 819 |
+
<thead>
|
| 820 |
+
<tr>
|
| 821 |
+
<th>#</th>
|
| 822 |
+
<th>Name</th>
|
| 823 |
+
<th>Price</th>
|
| 824 |
+
<th>24h Change</th>
|
| 825 |
+
<th>Market Cap</th>
|
| 826 |
+
<th>Volume 24h</th>
|
| 827 |
+
</tr>
|
| 828 |
+
</thead>
|
| 829 |
+
<tbody id="marketTableBody">
|
| 830 |
+
<tr>
|
| 831 |
+
<td colspan="6">
|
| 832 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 833 |
+
</td>
|
| 834 |
+
</tr>
|
| 835 |
+
</tbody>
|
| 836 |
+
</table>
|
| 837 |
+
</div>
|
| 838 |
+
</div>
|
| 839 |
+
|
| 840 |
+
<!-- Charts Row -->
|
| 841 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 30px; margin-bottom: 30px;">
|
| 842 |
+
<div class="chart-container">
|
| 843 |
+
<div class="section-title" style="margin-bottom: 20px;">📈 Market Dominance</div>
|
| 844 |
+
<canvas id="dominanceChart"></canvas>
|
| 845 |
+
</div>
|
| 846 |
+
|
| 847 |
+
<div class="chart-container">
|
| 848 |
+
<div class="section-title" style="margin-bottom: 20px;">😱 Fear & Greed Index</div>
|
| 849 |
+
<div style="text-align: center;">
|
| 850 |
+
<canvas id="gaugeChart"></canvas>
|
| 851 |
+
<div style="font-size: 48px; font-weight: 900; margin: 20px 0 10px;" id="sentimentValue">50</div>
|
| 852 |
+
<div style="color: var(--text-secondary); font-size: 16px; font-weight: 600;" id="sentimentText">Neutral</div>
|
| 853 |
+
</div>
|
| 854 |
+
</div>
|
| 855 |
+
</div>
|
| 856 |
+
|
| 857 |
+
<!-- Trending Section -->
|
| 858 |
+
<div class="market-section">
|
| 859 |
+
<div class="section-title" style="margin-bottom: 20px;">🔥 Trending Now</div>
|
| 860 |
+
<div id="trendingGrid" style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
| 861 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 862 |
+
</div>
|
| 863 |
+
</div>
|
| 864 |
+
|
| 865 |
+
<!-- DeFi Section -->
|
| 866 |
+
<div class="market-section">
|
| 867 |
+
<div class="section-title" style="margin-bottom: 20px;">🏦 Top DeFi Protocols</div>
|
| 868 |
+
<div id="defiList">
|
| 869 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 870 |
+
</div>
|
| 871 |
+
</div>
|
| 872 |
+
</div>
|
| 873 |
+
|
| 874 |
+
<!-- API Monitor Tab -->
|
| 875 |
+
<div id="tab-monitor" class="tab-content">
|
| 876 |
+
<div class="stats-grid">
|
| 877 |
+
<div class="stat-card">
|
| 878 |
+
<div class="stat-header">
|
| 879 |
+
<div class="stat-icon">📡</div>
|
| 880 |
+
</div>
|
| 881 |
+
<div class="stat-value" id="totalAPIs">0</div>
|
| 882 |
+
<div class="stat-label">Total APIs</div>
|
| 883 |
+
</div>
|
| 884 |
+
<div class="stat-card">
|
| 885 |
+
<div class="stat-header">
|
| 886 |
+
<div class="stat-icon">✅</div>
|
| 887 |
+
</div>
|
| 888 |
+
<div class="stat-value" id="onlineAPIs" style="color: var(--accent-green);">0</div>
|
| 889 |
+
<div class="stat-label">Online</div>
|
| 890 |
+
</div>
|
| 891 |
+
<div class="stat-card">
|
| 892 |
+
<div class="stat-header">
|
| 893 |
+
<div class="stat-icon">❌</div>
|
| 894 |
+
</div>
|
| 895 |
+
<div class="stat-value" id="offlineAPIs" style="color: var(--accent-red);">0</div>
|
| 896 |
+
<div class="stat-label">Offline</div>
|
| 897 |
+
</div>
|
| 898 |
+
<div class="stat-card">
|
| 899 |
+
<div class="stat-header">
|
| 900 |
+
<div class="stat-icon">⚡</div>
|
| 901 |
+
</div>
|
| 902 |
+
<div class="stat-value" id="avgResponse" style="font-size: 28px;">0ms</div>
|
| 903 |
+
<div class="stat-label">Avg Response</div>
|
| 904 |
+
</div>
|
| 905 |
+
</div>
|
| 906 |
+
|
| 907 |
+
<div class="market-section">
|
| 908 |
+
<div class="section-header">
|
| 909 |
+
<div class="section-title">📊 API Providers Status</div>
|
| 910 |
+
<button class="refresh-btn" onclick="loadMonitorData()">↻ Refresh</button>
|
| 911 |
+
</div>
|
| 912 |
+
<div style="overflow-x: auto;">
|
| 913 |
+
<table>
|
| 914 |
+
<thead>
|
| 915 |
+
<tr>
|
| 916 |
+
<th>Provider</th>
|
| 917 |
+
<th>Category</th>
|
| 918 |
+
<th>Status</th>
|
| 919 |
+
<th>Response Time</th>
|
| 920 |
+
<th>Last Check</th>
|
| 921 |
+
</tr>
|
| 922 |
+
</thead>
|
| 923 |
+
<tbody id="providersTable">
|
| 924 |
+
<tr><td colspan="5" style="text-align: center;">Loading...</td></tr>
|
| 925 |
+
</tbody>
|
| 926 |
+
</table>
|
| 927 |
+
</div>
|
| 928 |
+
</div>
|
| 929 |
+
|
| 930 |
+
<div class="market-section">
|
| 931 |
+
<div class="section-title" style="margin-bottom: 20px;">🤗 HuggingFace Sentiment Analysis</div>
|
| 932 |
+
<div class="form-group">
|
| 933 |
+
<label class="form-label">Enter crypto-related text (one per line):</label>
|
| 934 |
+
<textarea class="form-textarea" id="sentimentText" rows="5" placeholder="BTC strong breakout ETH looks weak Market is bullish">BTC strong breakout
|
| 935 |
+
ETH looks weak
|
| 936 |
+
Market is bullish today</textarea>
|
| 937 |
+
</div>
|
| 938 |
+
<button class="refresh-btn" onclick="runSentiment()">🧠 Analyze Sentiment</button>
|
| 939 |
+
<div id="sentimentResult" style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;">—</div>
|
| 940 |
+
<pre id="sentimentDetails" style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin-top: 15px; max-height: 300px; overflow-y: auto;"></pre>
|
| 941 |
+
</div>
|
| 942 |
+
</div>
|
| 943 |
+
|
| 944 |
+
<!-- Advanced Tab -->
|
| 945 |
+
<div id="tab-advanced" class="tab-content">
|
| 946 |
+
<div class="stats-grid">
|
| 947 |
+
<div class="stat-card">
|
| 948 |
+
<div class="stat-header">
|
| 949 |
+
<div class="stat-icon">📡</div>
|
| 950 |
+
</div>
|
| 951 |
+
<div class="stat-value" id="totalApis">0</div>
|
| 952 |
+
<div class="stat-label">Total APIs</div>
|
| 953 |
+
</div>
|
| 954 |
+
<div class="stat-card">
|
| 955 |
+
<div class="stat-header">
|
| 956 |
+
<div class="stat-icon">⚙️</div>
|
| 957 |
+
</div>
|
| 958 |
+
<div class="stat-value" id="activeTasks">0</div>
|
| 959 |
+
<div class="stat-label">Active Tasks</div>
|
| 960 |
+
</div>
|
| 961 |
+
<div class="stat-card">
|
| 962 |
+
<div class="stat-header">
|
| 963 |
+
<div class="stat-icon">💾</div>
|
| 964 |
+
</div>
|
| 965 |
+
<div class="stat-value" id="cachedData">0</div>
|
| 966 |
+
<div class="stat-label">Cached Data</div>
|
| 967 |
+
</div>
|
| 968 |
+
<div class="stat-card">
|
| 969 |
+
<div class="stat-header">
|
| 970 |
+
<div class="stat-icon">🔌</div>
|
| 971 |
+
</div>
|
| 972 |
+
<div class="stat-value" id="wsConnections">0</div>
|
| 973 |
+
<div class="stat-label">WS Connections</div>
|
| 974 |
+
</div>
|
| 975 |
+
</div>
|
| 976 |
+
|
| 977 |
+
<div class="market-section">
|
| 978 |
+
<div class="section-header">
|
| 979 |
+
<div class="section-title">🔧 Advanced Actions</div>
|
| 980 |
+
</div>
|
| 981 |
+
<div style="display: flex; gap: 15px; flex-wrap: wrap;">
|
| 982 |
+
<button class="refresh-btn" onclick="exportJSON()">💾 Export JSON</button>
|
| 983 |
+
<button class="refresh-btn" onclick="exportCSV()">📊 Export CSV</button>
|
| 984 |
+
<button class="refresh-btn" onclick="createBackup()">🔄 Create Backup</button>
|
| 985 |
+
<button class="refresh-btn" onclick="clearCache()">🗑️ Clear Cache</button>
|
| 986 |
+
<button class="refresh-btn" onclick="forceUpdateAll()">🔃 Force Update All</button>
|
| 987 |
+
</div>
|
| 988 |
+
</div>
|
| 989 |
+
|
| 990 |
+
<div class="market-section">
|
| 991 |
+
<div class="section-title" style="margin-bottom: 20px;">📈 Recent Activity</div>
|
| 992 |
+
<div id="activityLog" style="max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 12px;">
|
| 993 |
+
<div style="padding: 10px; border-left: 3px solid var(--accent-blue); margin-bottom: 8px;">
|
| 994 |
+
<span style="opacity: 0.6;">--:--:--</span> Waiting for updates...
|
| 995 |
+
</div>
|
| 996 |
+
</div>
|
| 997 |
+
</div>
|
| 998 |
+
|
| 999 |
+
<div class="market-section">
|
| 1000 |
+
<div class="section-title" style="margin-bottom: 20px;">🔌 API Sources</div>
|
| 1001 |
+
<div id="apiList">
|
| 1002 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 1003 |
+
</div>
|
| 1004 |
+
</div>
|
| 1005 |
+
</div>
|
| 1006 |
+
|
| 1007 |
+
<!-- Admin Tab -->
|
| 1008 |
+
<div id="tab-admin" class="tab-content">
|
| 1009 |
+
<div class="market-section">
|
| 1010 |
+
<div class="section-title" style="margin-bottom: 20px;">➕ Add New API Source</div>
|
| 1011 |
+
<div class="form-group">
|
| 1012 |
+
<label class="form-label">API Name</label>
|
| 1013 |
+
<input type="text" class="form-input" id="newApiName" placeholder="e.g., CoinGecko">
|
| 1014 |
+
</div>
|
| 1015 |
+
<div class="form-group">
|
| 1016 |
+
<label class="form-label">API URL</label>
|
| 1017 |
+
<input type="text" class="form-input" id="newApiUrl" placeholder="https://api.example.com/endpoint">
|
| 1018 |
+
</div>
|
| 1019 |
+
<div class="form-group">
|
| 1020 |
+
<label class="form-label">Category</label>
|
| 1021 |
+
<select class="form-select" id="newApiCategory">
|
| 1022 |
+
<option value="market_data">Market Data</option>
|
| 1023 |
+
<option value="blockchain_explorers">Blockchain Explorers</option>
|
| 1024 |
+
<option value="news">News & Social</option>
|
| 1025 |
+
<option value="sentiment">Sentiment</option>
|
| 1026 |
+
<option value="defi">DeFi</option>
|
| 1027 |
+
<option value="nft">NFT</option>
|
| 1028 |
+
</select>
|
| 1029 |
+
</div>
|
| 1030 |
+
<button class="refresh-btn" onclick="addNewAPI()">➕ Add API Source</button>
|
| 1031 |
+
</div>
|
| 1032 |
+
|
| 1033 |
+
<div class="market-section">
|
| 1034 |
+
<div class="section-title" style="margin-bottom: 20px;">📋 Current API Sources</div>
|
| 1035 |
+
<div id="apisList">Loading...</div>
|
| 1036 |
+
</div>
|
| 1037 |
+
|
| 1038 |
+
<div class="market-section">
|
| 1039 |
+
<div class="section-title" style="margin-bottom: 20px;">⚙️ Settings</div>
|
| 1040 |
+
<div class="form-group">
|
| 1041 |
+
<label class="form-label">API Check Interval (seconds)</label>
|
| 1042 |
+
<input type="number" class="form-input" id="checkInterval" value="30" min="10" max="300">
|
| 1043 |
+
</div>
|
| 1044 |
+
<div class="form-group">
|
| 1045 |
+
<label class="form-label">Dashboard Auto-Refresh (seconds)</label>
|
| 1046 |
+
<input type="number" class="form-input" id="dashboardRefresh" value="30" min="5" max="300">
|
| 1047 |
+
</div>
|
| 1048 |
+
<button class="refresh-btn" onclick="saveSettings()">💾 Save Settings</button>
|
| 1049 |
+
</div>
|
| 1050 |
+
|
| 1051 |
+
<div class="market-section">
|
| 1052 |
+
<div class="section-title" style="margin-bottom: 20px;">📊 Statistics</div>
|
| 1053 |
+
<div class="stats-grid">
|
| 1054 |
+
<div class="stat-card">
|
| 1055 |
+
<div class="stat-value" id="statTotal">0</div>
|
| 1056 |
+
<div class="stat-label">Total API Sources</div>
|
| 1057 |
+
</div>
|
| 1058 |
+
<div class="stat-card">
|
| 1059 |
+
<div class="stat-value" id="statOnline" style="color: var(--accent-green);">0</div>
|
| 1060 |
+
<div class="stat-label">Currently Online</div>
|
| 1061 |
+
</div>
|
| 1062 |
+
<div class="stat-card">
|
| 1063 |
+
<div class="stat-value" id="statOffline" style="color: var(--accent-red);">0</div>
|
| 1064 |
+
<div class="stat-label">Currently Offline</div>
|
| 1065 |
+
</div>
|
| 1066 |
+
</div>
|
| 1067 |
+
</div>
|
| 1068 |
+
</div>
|
| 1069 |
+
|
| 1070 |
+
<!-- HuggingFace Tab -->
|
| 1071 |
+
<div id="tab-hf" class="tab-content">
|
| 1072 |
+
<div class="market-section">
|
| 1073 |
+
<div class="section-header">
|
| 1074 |
+
<div class="section-title">📊 Health Status</div>
|
| 1075 |
+
<button class="refresh-btn" onclick="loadHFHealth()">🔄 Refresh</button>
|
| 1076 |
+
</div>
|
| 1077 |
+
<pre id="healthOutput" style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; max-height: 200px; overflow-y: auto;">Loading...</pre>
|
| 1078 |
+
</div>
|
| 1079 |
+
|
| 1080 |
+
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px;">
|
| 1081 |
+
<div class="market-section">
|
| 1082 |
+
<div class="section-title" style="margin-bottom: 20px;">🤖 Models Registry</div>
|
| 1083 |
+
<button class="refresh-btn" onclick="loadModels()" style="margin-bottom: 15px;">Load Models</button>
|
| 1084 |
+
<div id="modelsList" style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;">
|
| 1085 |
+
<p style="color: var(--text-secondary);">Click "Load Models" to fetch...</p>
|
| 1086 |
+
</div>
|
| 1087 |
+
</div>
|
| 1088 |
+
|
| 1089 |
+
<div class="market-section">
|
| 1090 |
+
<div class="section-title" style="margin-bottom: 20px;">📚 Datasets Registry</div>
|
| 1091 |
+
<button class="refresh-btn" onclick="loadDatasets()" style="margin-bottom: 15px;">Load Datasets</button>
|
| 1092 |
+
<div id="datasetsList" style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;">
|
| 1093 |
+
<p style="color: var(--text-secondary);">Click "Load Datasets" to fetch...</p>
|
| 1094 |
+
</div>
|
| 1095 |
+
</div>
|
| 1096 |
+
</div>
|
| 1097 |
+
|
| 1098 |
+
<div class="market-section">
|
| 1099 |
+
<div class="section-title" style="margin-bottom: 20px;">🔍 Search Registry</div>
|
| 1100 |
+
<div class="form-group">
|
| 1101 |
+
<input type="text" class="form-input" id="searchQuery" placeholder="Search query (e.g., crypto, bitcoin, sentiment)" value="crypto">
|
| 1102 |
+
</div>
|
| 1103 |
+
<div style="display: flex; gap: 10px; margin-bottom: 15px;">
|
| 1104 |
+
<button class="refresh-btn" onclick="doSearch()">Search Models</button>
|
| 1105 |
+
<button class="refresh-btn" onclick="doSearchDatasets()">Search Datasets</button>
|
| 1106 |
+
</div>
|
| 1107 |
+
<div id="searchResults" style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;">
|
| 1108 |
+
<p style="color: var(--text-secondary);">Enter a query and click search...</p>
|
| 1109 |
+
</div>
|
| 1110 |
+
</div>
|
| 1111 |
+
|
| 1112 |
+
<div class="market-section">
|
| 1113 |
+
<div class="section-title" style="margin-bottom: 20px;">💭 Sentiment Analysis</div>
|
| 1114 |
+
<div class="form-group">
|
| 1115 |
+
<label class="form-label">Enter text samples (one per line):</label>
|
| 1116 |
+
<textarea class="form-textarea" id="sentimentTexts" rows="5" placeholder="BTC strong breakout ETH looks weak Market sentiment is bullish">BTC strong breakout
|
| 1117 |
+
ETH looks weak
|
| 1118 |
+
Crypto market is bullish today</textarea>
|
| 1119 |
+
</div>
|
| 1120 |
+
<button class="refresh-btn" onclick="doSentiment()">🧠 Run Sentiment Analysis</button>
|
| 1121 |
+
<div id="voteDisplay" style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;">—</div>
|
| 1122 |
+
<pre id="sentimentOutput" style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin-top: 15px; max-height: 300px; overflow-y: auto;">Results will appear here...</pre>
|
| 1123 |
+
</div>
|
| 1124 |
+
</div>
|
| 1125 |
+
|
| 1126 |
+
<!-- Pools Tab -->
|
| 1127 |
+
<div id="tab-pools" class="tab-content">
|
| 1128 |
+
<div class="market-section">
|
| 1129 |
+
<div class="section-header">
|
| 1130 |
+
<div class="section-title">🔄 Source Pool Management</div>
|
| 1131 |
+
<div style="display: flex; gap: 10px;">
|
| 1132 |
+
<button class="refresh-btn" onclick="showCreatePoolModal()">➕ Create Pool</button>
|
| 1133 |
+
<button class="refresh-btn" onclick="loadPools()">🔄 Refresh</button>
|
| 1134 |
+
</div>
|
| 1135 |
+
</div>
|
| 1136 |
+
<div id="poolsContainer" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 20px;">
|
| 1137 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 1138 |
+
</div>
|
| 1139 |
+
</div>
|
| 1140 |
+
|
| 1141 |
+
<div class="market-section">
|
| 1142 |
+
<div class="section-title" style="margin-bottom: 20px;">📜 Rotation History</div>
|
| 1143 |
+
<div id="rotationHistory" style="max-height: 400px; overflow-y: auto;">
|
| 1144 |
+
<div class="loading"><div class="spinner"></div></div>
|
| 1145 |
+
</div>
|
| 1146 |
+
</div>
|
| 1147 |
+
</div>
|
| 1148 |
+
|
| 1149 |
+
<!-- Create Pool Modal -->
|
| 1150 |
+
<div id="createPoolModal" class="modal">
|
| 1151 |
+
<div class="modal-content">
|
| 1152 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 1153 |
+
<h2 style="font-size: 24px; font-weight: 800;">➕ Create New Pool</h2>
|
| 1154 |
+
<button onclick="closeCreatePoolModal()" style="background: none; border: none; color: var(--text-secondary); font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">×</button>
|
| 1155 |
+
</div>
|
| 1156 |
+
<form id="createPoolForm">
|
| 1157 |
+
<div class="form-group">
|
| 1158 |
+
<label class="form-label">Pool Name</label>
|
| 1159 |
+
<input type="text" class="form-input" id="poolName" required placeholder="e.g., Market Data Pool">
|
| 1160 |
+
</div>
|
| 1161 |
+
<div class="form-group">
|
| 1162 |
+
<label class="form-label">Category</label>
|
| 1163 |
+
<select class="form-select" id="poolCategory" required>
|
| 1164 |
+
<option value="market_data">Market Data</option>
|
| 1165 |
+
<option value="blockchain_explorers">Blockchain Explorers</option>
|
| 1166 |
+
<option value="news">News & Social</option>
|
| 1167 |
+
<option value="sentiment">Sentiment</option>
|
| 1168 |
+
<option value="defi">DeFi</option>
|
| 1169 |
+
<option value="nft">NFT</option>
|
| 1170 |
+
</select>
|
| 1171 |
+
</div>
|
| 1172 |
+
<div class="form-group">
|
| 1173 |
+
<label class="form-label">Rotation Strategy</label>
|
| 1174 |
+
<select class="form-select" id="rotationStrategy" required>
|
| 1175 |
+
<option value="round_robin">Round Robin</option>
|
| 1176 |
+
<option value="priority">Priority Based</option>
|
| 1177 |
+
<option value="weighted">Weighted</option>
|
| 1178 |
+
<option value="least_used">Least Used</option>
|
| 1179 |
+
</select>
|
| 1180 |
+
</div>
|
| 1181 |
+
<div class="form-group">
|
| 1182 |
+
<label class="form-label">Description (optional)</label>
|
| 1183 |
+
<textarea class="form-textarea" id="poolDescription" rows="3" placeholder="Pool description..."></textarea>
|
| 1184 |
+
</div>
|
| 1185 |
+
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
| 1186 |
+
<button type="button" class="refresh-btn" onclick="closeCreatePoolModal()" style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);">Cancel</button>
|
| 1187 |
+
<button type="submit" class="refresh-btn">Create Pool</button>
|
| 1188 |
+
</div>
|
| 1189 |
+
</form>
|
| 1190 |
+
</div>
|
| 1191 |
+
</div>
|
| 1192 |
+
|
| 1193 |
+
<!-- Add Member Modal -->
|
| 1194 |
+
<div id="addMemberModal" class="modal">
|
| 1195 |
+
<div class="modal-content">
|
| 1196 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
| 1197 |
+
<h2 style="font-size: 24px; font-weight: 800;">➕ Add Provider to Pool</h2>
|
| 1198 |
+
<button onclick="closeAddMemberModal()" style="background: none; border: none; color: var(--text-secondary); font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">×</button>
|
| 1199 |
+
</div>
|
| 1200 |
+
<form id="addMemberForm">
|
| 1201 |
+
<div class="form-group">
|
| 1202 |
+
<label class="form-label">Provider</label>
|
| 1203 |
+
<select class="form-select" id="memberProvider" required>
|
| 1204 |
+
<option value="">Select a provider...</option>
|
| 1205 |
+
</select>
|
| 1206 |
+
</div>
|
| 1207 |
+
<div class="form-group">
|
| 1208 |
+
<label class="form-label">Priority (1-10, higher = better)</label>
|
| 1209 |
+
<input type="number" class="form-input" id="memberPriority" value="1" min="1" max="10">
|
| 1210 |
+
</div>
|
| 1211 |
+
<div class="form-group">
|
| 1212 |
+
<label class="form-label">Weight (1-100, for weighted strategy)</label>
|
| 1213 |
+
<input type="number" class="form-input" id="memberWeight" value="1" min="1" max="100">
|
| 1214 |
+
</div>
|
| 1215 |
+
<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;">
|
| 1216 |
+
<button type="button" class="refresh-btn" onclick="closeAddMemberModal()" style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);">Cancel</button>
|
| 1217 |
+
<button type="submit" class="refresh-btn">Add Member</button>
|
| 1218 |
+
</div>
|
| 1219 |
+
</form>
|
| 1220 |
+
</div>
|
| 1221 |
+
</div>
|
| 1222 |
+
</div>
|
| 1223 |
+
|
| 1224 |
+
<script>
|
| 1225 |
+
// Global variables
|
| 1226 |
+
let charts = {};
|
| 1227 |
+
let wsConnection = null;
|
| 1228 |
+
let currentTab = 'market';
|
| 1229 |
+
|
| 1230 |
+
// Tab switching
|
| 1231 |
+
function switchTab(tabName) {
|
| 1232 |
+
currentTab = tabName;
|
| 1233 |
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
| 1234 |
+
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
| 1235 |
+
|
| 1236 |
+
event.target.classList.add('active');
|
| 1237 |
+
document.getElementById(`tab-${tabName}`).classList.add('active');
|
| 1238 |
+
|
| 1239 |
+
// Load data for specific tabs
|
| 1240 |
+
if (tabName === 'market') {
|
| 1241 |
+
loadMarketData();
|
| 1242 |
+
} else if (tabName === 'monitor') {
|
| 1243 |
+
loadMonitorData();
|
| 1244 |
+
} else if (tabName === 'advanced') {
|
| 1245 |
+
loadAdvancedData();
|
| 1246 |
+
} else if (tabName === 'admin') {
|
| 1247 |
+
loadAdminData();
|
| 1248 |
+
} else if (tabName === 'hf') {
|
| 1249 |
+
loadHFHealth();
|
| 1250 |
+
} else if (tabName === 'pools') {
|
| 1251 |
+
loadPools();
|
| 1252 |
+
}
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
// Initialize
|
| 1256 |
+
document.addEventListener('DOMContentLoaded', async () => {
|
| 1257 |
+
await loadMarketData();
|
| 1258 |
+
initCharts();
|
| 1259 |
+
connectWebSocket();
|
| 1260 |
+
setInterval(() => {
|
| 1261 |
+
if (currentTab === 'market') loadMarketData();
|
| 1262 |
+
else if (currentTab === 'monitor') loadMonitorData();
|
| 1263 |
+
}, 60000);
|
| 1264 |
+
});
|
| 1265 |
+
|
| 1266 |
+
// Market Data Functions
|
| 1267 |
+
async function loadMarketData() {
|
| 1268 |
+
try {
|
| 1269 |
+
const [market, stats, sentiment, trending, defi] = await Promise.all([
|
| 1270 |
+
fetch('/api/market').then(r => r.json()),
|
| 1271 |
+
fetch('/api/stats').then(r => r.json()),
|
| 1272 |
+
fetch('/api/sentiment').then(r => r.json()),
|
| 1273 |
+
fetch('/api/trending').then(r => r.json()),
|
| 1274 |
+
fetch('/api/defi').then(r => r.json())
|
| 1275 |
+
]);
|
| 1276 |
+
|
| 1277 |
+
updateStats(stats, sentiment);
|
| 1278 |
+
updateMarketTable(market.cryptocurrencies);
|
| 1279 |
+
updateTrending(trending.trending);
|
| 1280 |
+
updateDeFi(defi);
|
| 1281 |
+
updateCharts(market, sentiment);
|
| 1282 |
+
} catch (error) {
|
| 1283 |
+
console.error('Error loading market data:', error);
|
| 1284 |
+
}
|
| 1285 |
+
}
|
| 1286 |
+
|
| 1287 |
+
function updateStats(stats, sentiment) {
|
| 1288 |
+
const mcap = stats.market.total_market_cap;
|
| 1289 |
+
document.getElementById('totalMarketCap').textContent = '$' + (mcap / 1e12).toFixed(2) + 'T';
|
| 1290 |
+
|
| 1291 |
+
const volume = stats.market.total_volume;
|
| 1292 |
+
document.getElementById('totalVolume').textContent = '$' + (volume / 1e9).toFixed(2) + 'B';
|
| 1293 |
+
|
| 1294 |
+
const btcDom = stats.market.btc_dominance;
|
| 1295 |
+
document.getElementById('btcDominance').textContent = btcDom.toFixed(1) + '%';
|
| 1296 |
+
|
| 1297 |
+
const fg = sentiment.fear_greed_index.value;
|
| 1298 |
+
const classification = sentiment.fear_greed_index.classification;
|
| 1299 |
+
document.getElementById('fearGreed').textContent = fg;
|
| 1300 |
+
document.getElementById('sentimentLabel').innerHTML = `<span>${classification}</span>`;
|
| 1301 |
+
|
| 1302 |
+
const sentEl = document.getElementById('sentimentLabel');
|
| 1303 |
+
if (fg < 25) {
|
| 1304 |
+
sentEl.style.color = 'var(--accent-red)';
|
| 1305 |
+
} else if (fg < 45) {
|
| 1306 |
+
sentEl.style.color = 'var(--accent-yellow)';
|
| 1307 |
+
} else if (fg < 55) {
|
| 1308 |
+
sentEl.style.color = 'var(--text-secondary)';
|
| 1309 |
+
} else if (fg < 75) {
|
| 1310 |
+
sentEl.style.color = 'var(--accent-blue)';
|
| 1311 |
+
} else {
|
| 1312 |
+
sentEl.style.color = 'var(--accent-green)';
|
| 1313 |
+
}
|
| 1314 |
+
}
|
| 1315 |
+
|
| 1316 |
+
function updateMarketTable(cryptos) {
|
| 1317 |
+
const tbody = document.getElementById('marketTableBody');
|
| 1318 |
+
tbody.innerHTML = cryptos.map((crypto, index) => `
|
| 1319 |
+
<tr>
|
| 1320 |
+
<td style="font-weight: 700; color: var(--text-secondary);">${crypto.rank || index + 1}</td>
|
| 1321 |
+
<td>
|
| 1322 |
+
<div class="crypto-name">
|
| 1323 |
+
${crypto.image ? `<img src="${crypto.image}" class="crypto-img" alt="${crypto.symbol}">` :
|
| 1324 |
+
`<div class="crypto-img" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; font-weight: 700;">${crypto.symbol[0]}</div>`}
|
| 1325 |
+
<div>
|
| 1326 |
+
<div style="font-weight: 600;">${crypto.name}</div>
|
| 1327 |
+
<div class="crypto-symbol">${crypto.symbol}</div>
|
| 1328 |
+
</div>
|
| 1329 |
+
</div>
|
| 1330 |
+
</td>
|
| 1331 |
+
<td class="price">$${crypto.price.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 6})}</td>
|
| 1332 |
+
<td><span class="change ${crypto.change_24h >= 0 ? 'positive' : 'negative'}">${crypto.change_24h >= 0 ? '+' : ''}${crypto.change_24h.toFixed(2)}%</span></td>
|
| 1333 |
+
<td style="font-weight: 600;">$${(crypto.market_cap / 1e9).toFixed(2)}B</td>
|
| 1334 |
+
<td style="color: var(--text-secondary);">$${(crypto.volume_24h / 1e9).toFixed(2)}B</td>
|
| 1335 |
+
</tr>
|
| 1336 |
+
`).join('');
|
| 1337 |
+
}
|
| 1338 |
+
|
| 1339 |
+
function updateTrending(trending) {
|
| 1340 |
+
const grid = document.getElementById('trendingGrid');
|
| 1341 |
+
grid.innerHTML = trending.map((coin, index) => `
|
| 1342 |
+
<div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; padding: 15px; display: flex; align-items: center; gap: 12px;">
|
| 1343 |
+
<div style="font-size: 20px; font-weight: 900; color: var(--accent-yellow);">#${index + 1}</div>
|
| 1344 |
+
${coin.thumb ? `<img src="${coin.thumb}" style="width: 32px; height: 32px; border-radius: 8px;">` : ''}
|
| 1345 |
+
<div>
|
| 1346 |
+
<div style="font-weight: 600;">${coin.name}</div>
|
| 1347 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${coin.symbol}</div>
|
| 1348 |
+
</div>
|
| 1349 |
+
</div>
|
| 1350 |
+
`).join('');
|
| 1351 |
+
}
|
| 1352 |
+
|
| 1353 |
+
function updateDeFi(defi) {
|
| 1354 |
+
const list = document.getElementById('defiList');
|
| 1355 |
+
const protocols = defi.protocols || [];
|
| 1356 |
+
|
| 1357 |
+
list.innerHTML = `
|
| 1358 |
+
<div style="margin-bottom: 20px; padding: 20px; background: rgba(59, 130, 246, 0.1); border-radius: 16px; text-align: center;">
|
| 1359 |
+
<div style="font-size: 36px; font-weight: 900; margin-bottom: 5px;">$${(defi.total_tvl / 1e9).toFixed(2)}B</div>
|
| 1360 |
+
<div style="color: var(--text-secondary); font-size: 14px;">Total Value Locked</div>
|
| 1361 |
+
</div>
|
| 1362 |
+
<div style="display: grid; gap: 12px;">
|
| 1363 |
+
${protocols.map((p, i) => `
|
| 1364 |
+
<div style="display: flex; justify-content: space-between; align-items: center; padding: 15px; background: rgba(255, 255, 255, 0.05); border-radius: 12px;">
|
| 1365 |
+
<div>
|
| 1366 |
+
<div style="font-weight: 600;">${i + 1}. ${p.name}</div>
|
| 1367 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${p.chain}</div>
|
| 1368 |
+
</div>
|
| 1369 |
+
<div style="text-align: right;">
|
| 1370 |
+
<div style="font-weight: 700; font-size: 16px;">$${(p.tvl / 1e9).toFixed(2)}B</div>
|
| 1371 |
+
<div style="font-size: 12px; color: ${p.change_24h >= 0 ? 'var(--accent-green)' : 'var(--accent-red)'};">
|
| 1372 |
+
${p.change_24h >= 0 ? '+' : ''}${p.change_24h.toFixed(2)}%
|
| 1373 |
+
</div>
|
| 1374 |
+
</div>
|
| 1375 |
+
</div>
|
| 1376 |
+
`).join('')}
|
| 1377 |
+
</div>
|
| 1378 |
+
`;
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
function initCharts() {
|
| 1382 |
+
Chart.defaults.color = '#9ca3af';
|
| 1383 |
+
Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)';
|
| 1384 |
+
|
| 1385 |
+
charts.dominance = new Chart(document.getElementById('dominanceChart'), {
|
| 1386 |
+
type: 'doughnut',
|
| 1387 |
+
data: {
|
| 1388 |
+
labels: ['Bitcoin', 'Ethereum', 'Others'],
|
| 1389 |
+
datasets: [{
|
| 1390 |
+
data: [45, 18, 37],
|
| 1391 |
+
backgroundColor: ['#f7931a', '#627eea', '#8b5cf6'],
|
| 1392 |
+
borderWidth: 0
|
| 1393 |
+
}]
|
| 1394 |
+
},
|
| 1395 |
+
options: {
|
| 1396 |
+
responsive: true,
|
| 1397 |
+
plugins: {
|
| 1398 |
+
legend: { position: 'bottom', labels: { padding: 15, font: { size: 12, weight: 600 } } }
|
| 1399 |
+
}
|
| 1400 |
+
}
|
| 1401 |
+
});
|
| 1402 |
+
|
| 1403 |
+
charts.gauge = new Chart(document.getElementById('gaugeChart'), {
|
| 1404 |
+
type: 'doughnut',
|
| 1405 |
+
data: {
|
| 1406 |
+
datasets: [{
|
| 1407 |
+
data: [50, 50],
|
| 1408 |
+
backgroundColor: ['#3b82f6', 'rgba(255, 255, 255, 0.1)'],
|
| 1409 |
+
borderWidth: 0
|
| 1410 |
+
}]
|
| 1411 |
+
},
|
| 1412 |
+
options: {
|
| 1413 |
+
rotation: -90,
|
| 1414 |
+
circumference: 180,
|
| 1415 |
+
cutout: '75%',
|
| 1416 |
+
plugins: { legend: { display: false }, tooltip: { enabled: false } }
|
| 1417 |
+
}
|
| 1418 |
+
});
|
| 1419 |
+
}
|
| 1420 |
+
|
| 1421 |
+
function updateCharts(market, sentiment) {
|
| 1422 |
+
const btcDom = market.global.btc_dominance;
|
| 1423 |
+
const ethDom = market.global.eth_dominance;
|
| 1424 |
+
charts.dominance.data.datasets[0].data = [btcDom, ethDom, 100 - btcDom - ethDom];
|
| 1425 |
+
charts.dominance.update();
|
| 1426 |
+
|
| 1427 |
+
const fg = sentiment.fear_greed_index.value;
|
| 1428 |
+
charts.gauge.data.datasets[0].data = [fg, 100 - fg];
|
| 1429 |
+
|
| 1430 |
+
if (fg < 25) {
|
| 1431 |
+
charts.gauge.data.datasets[0].backgroundColor[0] = '#ef4444';
|
| 1432 |
+
} else if (fg < 45) {
|
| 1433 |
+
charts.gauge.data.datasets[0].backgroundColor[0] = '#f59e0b';
|
| 1434 |
+
} else if (fg < 55) {
|
| 1435 |
+
charts.gauge.data.datasets[0].backgroundColor[0] = '#6b7280';
|
| 1436 |
+
} else if (fg < 75) {
|
| 1437 |
+
charts.gauge.data.datasets[0].backgroundColor[0] = '#3b82f6';
|
| 1438 |
+
} else {
|
| 1439 |
+
charts.gauge.data.datasets[0].backgroundColor[0] = '#10b981';
|
| 1440 |
+
}
|
| 1441 |
+
|
| 1442 |
+
charts.gauge.update();
|
| 1443 |
+
document.getElementById('sentimentValue').textContent = fg;
|
| 1444 |
+
document.getElementById('sentimentText').textContent = sentiment.fear_greed_index.classification;
|
| 1445 |
+
}
|
| 1446 |
+
|
| 1447 |
+
function connectWebSocket() {
|
| 1448 |
+
const ws = new WebSocket(`ws://${window.location.host}/ws/live`);
|
| 1449 |
+
|
| 1450 |
+
ws.onopen = () => {
|
| 1451 |
+
console.log('✅ WebSocket connected');
|
| 1452 |
+
};
|
| 1453 |
+
|
| 1454 |
+
ws.onmessage = (event) => {
|
| 1455 |
+
const data = JSON.parse(event.data);
|
| 1456 |
+
if (data.type === 'market_update') {
|
| 1457 |
+
console.log('📊 Market update received');
|
| 1458 |
+
}
|
| 1459 |
+
if (data.type === 'sentiment_update') {
|
| 1460 |
+
updateCharts({global: {}}, {fear_greed_index: data.data});
|
| 1461 |
+
}
|
| 1462 |
+
};
|
| 1463 |
+
|
| 1464 |
+
ws.onerror = () => {
|
| 1465 |
+
console.log('❌ WebSocket error');
|
| 1466 |
+
};
|
| 1467 |
+
|
| 1468 |
+
ws.onclose = () => {
|
| 1469 |
+
console.log('⚠️ WebSocket disconnected, reconnecting...');
|
| 1470 |
+
setTimeout(connectWebSocket, 5000);
|
| 1471 |
+
};
|
| 1472 |
+
}
|
| 1473 |
+
|
| 1474 |
+
// Monitor Functions
|
| 1475 |
+
async function loadMonitorData() {
|
| 1476 |
+
try {
|
| 1477 |
+
const [status, providers] = await Promise.all([
|
| 1478 |
+
fetch('/api/status').then(r => r.json()),
|
| 1479 |
+
fetch('/api/providers').then(r => r.json())
|
| 1480 |
+
]);
|
| 1481 |
+
|
| 1482 |
+
document.getElementById('totalAPIs').textContent = status.total_providers;
|
| 1483 |
+
document.getElementById('onlineAPIs').textContent = status.online;
|
| 1484 |
+
document.getElementById('offlineAPIs').textContent = status.offline;
|
| 1485 |
+
document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms';
|
| 1486 |
+
|
| 1487 |
+
const tbody = document.getElementById('providersTable');
|
| 1488 |
+
tbody.innerHTML = providers.map(p => {
|
| 1489 |
+
let statusClass = 'badge-success';
|
| 1490 |
+
if (p.status === 'offline') statusClass = 'badge-danger';
|
| 1491 |
+
else if (p.status === 'degraded') statusClass = 'badge-warning';
|
| 1492 |
+
|
| 1493 |
+
return `
|
| 1494 |
+
<tr>
|
| 1495 |
+
<td><strong>${p.name}</strong></td>
|
| 1496 |
+
<td><span class="badge badge-info">${p.category}</span></td>
|
| 1497 |
+
<td><span class="badge ${statusClass}">${p.status.toUpperCase()}</span></td>
|
| 1498 |
+
<td>${p.response_time_ms}ms</td>
|
| 1499 |
+
<td style="color: var(--text-secondary); font-size: 13px;">${new Date(p.last_fetch).toLocaleTimeString()}</td>
|
| 1500 |
+
</tr>
|
| 1501 |
+
`;
|
| 1502 |
+
}).join('');
|
| 1503 |
+
} catch (error) {
|
| 1504 |
+
console.error('Error loading monitor data:', error);
|
| 1505 |
+
}
|
| 1506 |
+
}
|
| 1507 |
+
|
| 1508 |
+
async function runSentiment() {
|
| 1509 |
+
const text = document.getElementById('sentimentText').value;
|
| 1510 |
+
const texts = text.split('\n').filter(t => t.trim());
|
| 1511 |
+
|
| 1512 |
+
if (texts.length === 0) {
|
| 1513 |
+
showToast('Please enter at least one line of text', 'info');
|
| 1514 |
+
return;
|
| 1515 |
+
}
|
| 1516 |
+
|
| 1517 |
+
try {
|
| 1518 |
+
document.getElementById('sentimentResult').textContent = '⏳ Analyzing...';
|
| 1519 |
+
document.getElementById('sentimentDetails').textContent = '';
|
| 1520 |
+
|
| 1521 |
+
const res = await fetch('/api/hf/run-sentiment', {
|
| 1522 |
+
method: 'POST',
|
| 1523 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1524 |
+
body: JSON.stringify({ texts })
|
| 1525 |
+
});
|
| 1526 |
+
|
| 1527 |
+
const data = await res.json();
|
| 1528 |
+
const vote = data.vote || 0;
|
| 1529 |
+
let emoji = '😐';
|
| 1530 |
+
let color = 'var(--text-secondary)';
|
| 1531 |
+
|
| 1532 |
+
if (vote > 0.2) {
|
| 1533 |
+
emoji = '📈';
|
| 1534 |
+
color = 'var(--accent-green)';
|
| 1535 |
+
} else if (vote < -0.2) {
|
| 1536 |
+
emoji = '📉';
|
| 1537 |
+
color = 'var(--accent-red)';
|
| 1538 |
+
}
|
| 1539 |
+
|
| 1540 |
+
document.getElementById('sentimentResult').innerHTML = `<span style="color: ${color};">${emoji} ${vote.toFixed(3)}</span>`;
|
| 1541 |
+
document.getElementById('sentimentDetails').textContent = JSON.stringify(data, null, 2);
|
| 1542 |
+
} catch (error) {
|
| 1543 |
+
document.getElementById('sentimentResult').innerHTML = '<span style="color: var(--accent-red);">❌ Error</span>';
|
| 1544 |
+
document.getElementById('sentimentDetails').textContent = 'Error: ' + error.message;
|
| 1545 |
+
}
|
| 1546 |
+
}
|
| 1547 |
+
|
| 1548 |
+
// Advanced Functions
|
| 1549 |
+
async function loadAdvancedData() {
|
| 1550 |
+
try {
|
| 1551 |
+
const response = await fetch('/api/v2/status');
|
| 1552 |
+
const data = await response.json();
|
| 1553 |
+
|
| 1554 |
+
document.getElementById('totalApis').textContent = data.services.config_loader.apis_loaded;
|
| 1555 |
+
document.getElementById('activeTasks').textContent = data.services.scheduler.total_tasks;
|
| 1556 |
+
document.getElementById('cachedData').textContent = data.services.persistence.cached_apis;
|
| 1557 |
+
document.getElementById('wsConnections').textContent = data.services.websocket.total_connections;
|
| 1558 |
+
|
| 1559 |
+
const apisResponse = await fetch('/api/v2/config/apis');
|
| 1560 |
+
const apisData = await apisResponse.json();
|
| 1561 |
+
displayAPIs(apisData.apis);
|
| 1562 |
+
} catch (error) {
|
| 1563 |
+
console.error('Error loading advanced data:', error);
|
| 1564 |
+
}
|
| 1565 |
+
}
|
| 1566 |
+
|
| 1567 |
+
function displayAPIs(apis) {
|
| 1568 |
+
const listElement = document.getElementById('apiList');
|
| 1569 |
+
listElement.innerHTML = '';
|
| 1570 |
+
|
| 1571 |
+
for (const [apiId, api] of Object.entries(apis)) {
|
| 1572 |
+
const item = document.createElement('div');
|
| 1573 |
+
item.style.cssText = 'background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;';
|
| 1574 |
+
item.innerHTML = `
|
| 1575 |
+
<div>
|
| 1576 |
+
<div style="font-weight: 600;">${api.name}</div>
|
| 1577 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${api.category}</div>
|
| 1578 |
+
</div>
|
| 1579 |
+
<button class="refresh-btn" onclick="forceUpdate('${apiId}')" style="padding: 6px 12px; font-size: 12px;">🔄 Update</button>
|
| 1580 |
+
`;
|
| 1581 |
+
listElement.appendChild(item);
|
| 1582 |
+
}
|
| 1583 |
+
}
|
| 1584 |
+
|
| 1585 |
+
async function exportJSON() {
|
| 1586 |
+
try {
|
| 1587 |
+
const response = await fetch('/api/v2/export/json', {
|
| 1588 |
+
method: 'POST',
|
| 1589 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1590 |
+
body: JSON.stringify({ include_history: true })
|
| 1591 |
+
});
|
| 1592 |
+
const data = await response.json();
|
| 1593 |
+
showToast('✅ JSON export created!', 'success');
|
| 1594 |
+
addLog(`Exported to JSON: ${data.filepath}`);
|
| 1595 |
+
} catch (error) {
|
| 1596 |
+
showToast('❌ Export failed', 'error');
|
| 1597 |
+
console.error(error);
|
| 1598 |
+
}
|
| 1599 |
+
}
|
| 1600 |
+
|
| 1601 |
+
async function exportCSV() {
|
| 1602 |
+
try {
|
| 1603 |
+
const response = await fetch('/api/v2/export/csv', {
|
| 1604 |
+
method: 'POST',
|
| 1605 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1606 |
+
body: JSON.stringify({ flatten: true })
|
| 1607 |
+
});
|
| 1608 |
+
const data = await response.json();
|
| 1609 |
+
showToast('✅ CSV export created!', 'success');
|
| 1610 |
+
addLog(`Exported to CSV: ${data.filepath}`);
|
| 1611 |
+
} catch (error) {
|
| 1612 |
+
showToast('❌ Export failed', 'error');
|
| 1613 |
+
console.error(error);
|
| 1614 |
+
}
|
| 1615 |
+
}
|
| 1616 |
+
|
| 1617 |
+
async function createBackup() {
|
| 1618 |
+
try {
|
| 1619 |
+
const response = await fetch('/api/v2/backup', { method: 'POST' });
|
| 1620 |
+
const data = await response.json();
|
| 1621 |
+
showToast('✅ Backup created!', 'success');
|
| 1622 |
+
addLog(`Backup created: ${data.backup_file}`);
|
| 1623 |
+
} catch (error) {
|
| 1624 |
+
showToast('❌ Backup failed', 'error');
|
| 1625 |
+
console.error(error);
|
| 1626 |
+
}
|
| 1627 |
+
}
|
| 1628 |
+
|
| 1629 |
+
async function clearCache() {
|
| 1630 |
+
if (!confirm('Clear all cached data?')) return;
|
| 1631 |
+
try {
|
| 1632 |
+
await fetch('/api/v2/cleanup/cache', { method: 'POST' });
|
| 1633 |
+
showToast('✅ Cache cleared!', 'success');
|
| 1634 |
+
addLog('Cache cleared');
|
| 1635 |
+
} catch (error) {
|
| 1636 |
+
showToast('❌ Failed to clear cache', 'error');
|
| 1637 |
+
console.error(error);
|
| 1638 |
+
}
|
| 1639 |
+
}
|
| 1640 |
+
|
| 1641 |
+
async function forceUpdateAll() {
|
| 1642 |
+
try {
|
| 1643 |
+
const response = await fetch('/api/v2/config/apis');
|
| 1644 |
+
const data = await response.json();
|
| 1645 |
+
for (const apiId of Object.keys(data.apis)) {
|
| 1646 |
+
await forceUpdate(apiId);
|
| 1647 |
+
await new Promise(resolve => setTimeout(resolve, 100));
|
| 1648 |
+
}
|
| 1649 |
+
showToast('✅ All APIs updated!', 'success');
|
| 1650 |
+
} catch (error) {
|
| 1651 |
+
showToast('❌ Update failed', 'error');
|
| 1652 |
+
console.error(error);
|
| 1653 |
+
}
|
| 1654 |
+
}
|
| 1655 |
+
|
| 1656 |
+
async function forceUpdate(apiId) {
|
| 1657 |
+
try {
|
| 1658 |
+
await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, { method: 'POST' });
|
| 1659 |
+
addLog(`Forced update: ${apiId}`);
|
| 1660 |
+
} catch (error) {
|
| 1661 |
+
console.error(error);
|
| 1662 |
+
}
|
| 1663 |
+
}
|
| 1664 |
+
|
| 1665 |
+
function addLog(text) {
|
| 1666 |
+
const logContainer = document.getElementById('activityLog');
|
| 1667 |
+
const time = new Date().toLocaleTimeString();
|
| 1668 |
+
const entry = document.createElement('div');
|
| 1669 |
+
entry.style.cssText = 'padding: 10px; border-left: 3px solid var(--accent-blue); margin-bottom: 8px;';
|
| 1670 |
+
entry.innerHTML = `<span style="opacity: 0.6;">${time}</span> ${text}`;
|
| 1671 |
+
logContainer.insertBefore(entry, logContainer.firstChild);
|
| 1672 |
+
while (logContainer.children.length > 50) {
|
| 1673 |
+
logContainer.removeChild(logContainer.lastChild);
|
| 1674 |
+
}
|
| 1675 |
+
}
|
| 1676 |
+
|
| 1677 |
+
// Admin Functions
|
| 1678 |
+
async function loadAdminData() {
|
| 1679 |
+
try {
|
| 1680 |
+
const [status, providers] = await Promise.all([
|
| 1681 |
+
fetch('/api/status').then(r => r.json()),
|
| 1682 |
+
fetch('/api/providers').then(r => r.json())
|
| 1683 |
+
]);
|
| 1684 |
+
|
| 1685 |
+
document.getElementById('statTotal').textContent = status.total_providers;
|
| 1686 |
+
document.getElementById('statOnline').textContent = status.online;
|
| 1687 |
+
document.getElementById('statOffline').textContent = status.offline;
|
| 1688 |
+
|
| 1689 |
+
const list = document.getElementById('apisList');
|
| 1690 |
+
list.innerHTML = providers.map(api => `
|
| 1691 |
+
<div style="background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;">
|
| 1692 |
+
<div>
|
| 1693 |
+
<div style="font-weight: 600;">${api.name}</div>
|
| 1694 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${api.category}</div>
|
| 1695 |
+
</div>
|
| 1696 |
+
<span class="badge ${api.status === 'online' ? 'badge-success' : 'badge-danger'}">${api.status.toUpperCase()}</span>
|
| 1697 |
+
</div>
|
| 1698 |
+
`).join('');
|
| 1699 |
+
} catch (error) {
|
| 1700 |
+
console.error('Error loading admin data:', error);
|
| 1701 |
+
}
|
| 1702 |
+
}
|
| 1703 |
+
|
| 1704 |
+
async function addNewAPI() {
|
| 1705 |
+
const name = document.getElementById('newApiName').value;
|
| 1706 |
+
const url = document.getElementById('newApiUrl').value;
|
| 1707 |
+
const category = document.getElementById('newApiCategory').value;
|
| 1708 |
+
|
| 1709 |
+
if (!name || !url) {
|
| 1710 |
+
showToast('Please fill in API name and URL', 'info');
|
| 1711 |
+
return;
|
| 1712 |
+
}
|
| 1713 |
+
|
| 1714 |
+
showToast('✅ API added! Note: Restart server to activate.', 'success');
|
| 1715 |
+
document.getElementById('newApiName').value = '';
|
| 1716 |
+
document.getElementById('newApiUrl').value = '';
|
| 1717 |
+
loadAdminData();
|
| 1718 |
+
}
|
| 1719 |
+
|
| 1720 |
+
function saveSettings() {
|
| 1721 |
+
const interval = document.getElementById('checkInterval').value;
|
| 1722 |
+
const refresh = document.getElementById('dashboardRefresh').value;
|
| 1723 |
+
localStorage.setItem('monitorSettings', JSON.stringify({ interval, refresh }));
|
| 1724 |
+
showToast('✅ Settings saved!', 'success');
|
| 1725 |
+
}
|
| 1726 |
+
|
| 1727 |
+
// HuggingFace Functions
|
| 1728 |
+
async function loadHFHealth() {
|
| 1729 |
+
try {
|
| 1730 |
+
const data = await fetch('/api/hf/health').then(r => r.json());
|
| 1731 |
+
document.getElementById('healthOutput').textContent = JSON.stringify(data, null, 2);
|
| 1732 |
+
} catch (err) {
|
| 1733 |
+
document.getElementById('healthOutput').textContent = `Error: ${err.message}`;
|
| 1734 |
+
}
|
| 1735 |
+
}
|
| 1736 |
+
|
| 1737 |
+
async function loadModels() {
|
| 1738 |
+
try {
|
| 1739 |
+
document.getElementById('modelsList').innerHTML = '<p style="color: var(--text-secondary);">Loading...</p>';
|
| 1740 |
+
// Mock data for now
|
| 1741 |
+
document.getElementById('modelsList').innerHTML = '<p style="color: var(--text-secondary);">Models registry endpoint not implemented</p>';
|
| 1742 |
+
} catch (err) {
|
| 1743 |
+
document.getElementById('modelsList').innerHTML = `<p style="color: var(--accent-red);">Error: ${err.message}</p>`;
|
| 1744 |
+
}
|
| 1745 |
+
}
|
| 1746 |
+
|
| 1747 |
+
async function loadDatasets() {
|
| 1748 |
+
try {
|
| 1749 |
+
document.getElementById('datasetsList').innerHTML = '<p style="color: var(--text-secondary);">Loading...</p>';
|
| 1750 |
+
// Mock data for now
|
| 1751 |
+
document.getElementById('datasetsList').innerHTML = '<p style="color: var(--text-secondary);">Datasets registry endpoint not implemented</p>';
|
| 1752 |
+
} catch (err) {
|
| 1753 |
+
document.getElementById('datasetsList').innerHTML = `<p style="color: var(--accent-red);">Error: ${err.message}</p>`;
|
| 1754 |
+
}
|
| 1755 |
+
}
|
| 1756 |
+
|
| 1757 |
+
async function doSearch() {
|
| 1758 |
+
const q = document.getElementById('searchQuery').value;
|
| 1759 |
+
document.getElementById('searchResults').innerHTML = `<p style="color: var(--text-secondary);">Searching for "${q}"...</p>`;
|
| 1760 |
+
// Mock search
|
| 1761 |
+
document.getElementById('searchResults').innerHTML = '<p style="color: var(--text-secondary);">Search endpoint not implemented</p>';
|
| 1762 |
+
}
|
| 1763 |
+
|
| 1764 |
+
async function doSearchDatasets() {
|
| 1765 |
+
const q = document.getElementById('searchQuery').value;
|
| 1766 |
+
document.getElementById('searchResults').innerHTML = `<p style="color: var(--text-secondary);">Searching datasets for "${q}"...</p>`;
|
| 1767 |
+
// Mock search
|
| 1768 |
+
document.getElementById('searchResults').innerHTML = '<p style="color: var(--text-secondary);">Search endpoint not implemented</p>';
|
| 1769 |
+
}
|
| 1770 |
+
|
| 1771 |
+
async function doSentiment() {
|
| 1772 |
+
const texts = document.getElementById('sentimentTexts').value.split('\n').filter(t => t.trim());
|
| 1773 |
+
if (texts.length === 0) {
|
| 1774 |
+
showToast('Please enter at least one text sample', 'info');
|
| 1775 |
+
return;
|
| 1776 |
+
}
|
| 1777 |
+
try {
|
| 1778 |
+
document.getElementById('voteDisplay').innerHTML = '<span>⏳ Analyzing...</span>';
|
| 1779 |
+
document.getElementById('sentimentOutput').textContent = 'Running sentiment analysis...';
|
| 1780 |
+
|
| 1781 |
+
const data = await fetch('/api/hf/run-sentiment', {
|
| 1782 |
+
method: 'POST',
|
| 1783 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1784 |
+
body: JSON.stringify({ texts })
|
| 1785 |
+
}).then(r => r.json());
|
| 1786 |
+
|
| 1787 |
+
const vote = data.vote || 0;
|
| 1788 |
+
let voteClass = 'vote-neutral';
|
| 1789 |
+
let voteEmoji = '😐';
|
| 1790 |
+
if (vote > 0.2) { voteClass = 'vote-positive'; voteEmoji = '📈'; }
|
| 1791 |
+
else if (vote < -0.2) { voteClass = 'vote-negative'; voteEmoji = '📉'; }
|
| 1792 |
+
|
| 1793 |
+
document.getElementById('voteDisplay').innerHTML = `<span style="color: ${voteClass === 'vote-positive' ? 'var(--accent-green)' : voteClass === 'vote-negative' ? 'var(--accent-red)' : 'var(--text-secondary)'};">${voteEmoji} ${vote.toFixed(3)}</span>`;
|
| 1794 |
+
document.getElementById('sentimentOutput').textContent = JSON.stringify(data, null, 2);
|
| 1795 |
+
} catch (err) {
|
| 1796 |
+
document.getElementById('voteDisplay').innerHTML = '<span style="color: var(--accent-red);">Error</span>';
|
| 1797 |
+
document.getElementById('sentimentOutput').textContent = `Error: ${err.message}`;
|
| 1798 |
+
}
|
| 1799 |
+
}
|
| 1800 |
+
|
| 1801 |
+
// Pools Functions
|
| 1802 |
+
let currentPoolId = null;
|
| 1803 |
+
let allProvidersList = [];
|
| 1804 |
+
|
| 1805 |
+
async function loadPools() {
|
| 1806 |
+
try {
|
| 1807 |
+
const [poolsRes, historyRes] = await Promise.all([
|
| 1808 |
+
fetch('/api/pools').then(r => r.json()),
|
| 1809 |
+
fetch('/api/pools/history?limit=20').then(r => r.json())
|
| 1810 |
+
]);
|
| 1811 |
+
|
| 1812 |
+
const pools = poolsRes.pools || [];
|
| 1813 |
+
const container = document.getElementById('poolsContainer');
|
| 1814 |
+
|
| 1815 |
+
if (pools.length === 0) {
|
| 1816 |
+
container.innerHTML = `
|
| 1817 |
+
<div style="grid-column: 1/-1; text-align: center; padding: 40px; background: rgba(17, 24, 39, 0.6); border-radius: 20px; border: 2px dashed var(--border);">
|
| 1818 |
+
<div style="font-size: 48px; margin-bottom: 15px;">🔄</div>
|
| 1819 |
+
<div style="font-size: 18px; font-weight: 600; margin-bottom: 10px; color: var(--text-primary);">No pools configured</div>
|
| 1820 |
+
<div style="color: var(--text-secondary); margin-bottom: 20px;">Create your first pool to get started with API source rotation</div>
|
| 1821 |
+
<button class="refresh-btn" onclick="showCreatePoolModal()">➕ Create Pool</button>
|
| 1822 |
+
</div>
|
| 1823 |
+
`;
|
| 1824 |
+
} else {
|
| 1825 |
+
container.innerHTML = pools.map(pool => createPoolCard(pool)).join('');
|
| 1826 |
+
}
|
| 1827 |
+
|
| 1828 |
+
// Load rotation history
|
| 1829 |
+
const history = historyRes.history || [];
|
| 1830 |
+
const historyContainer = document.getElementById('rotationHistory');
|
| 1831 |
+
if (history.length === 0) {
|
| 1832 |
+
historyContainer.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No rotation history yet</p>';
|
| 1833 |
+
} else {
|
| 1834 |
+
historyContainer.innerHTML = history.map(h => `
|
| 1835 |
+
<div style="padding: 15px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; margin-bottom: 10px; border-left: 3px solid var(--accent-blue);">
|
| 1836 |
+
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
| 1837 |
+
<div>
|
| 1838 |
+
<div style="font-weight: 600; margin-bottom: 5px;">${h.pool_name}</div>
|
| 1839 |
+
<div style="font-size: 12px; color: var(--text-secondary);">Rotated to: <strong>${h.provider_name}</strong></div>
|
| 1840 |
+
</div>
|
| 1841 |
+
<div style="text-align: right;">
|
| 1842 |
+
<div style="font-size: 12px; color: var(--text-secondary);">${new Date(h.timestamp).toLocaleString()}</div>
|
| 1843 |
+
<span class="badge badge-info" style="margin-top: 5px; display: inline-block;">${h.reason}</span>
|
| 1844 |
+
</div>
|
| 1845 |
+
</div>
|
| 1846 |
+
</div>
|
| 1847 |
+
`).join('');
|
| 1848 |
+
}
|
| 1849 |
+
} catch (error) {
|
| 1850 |
+
console.error('Error loading pools:', error);
|
| 1851 |
+
document.getElementById('poolsContainer').innerHTML = `
|
| 1852 |
+
<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--accent-red);">
|
| 1853 |
+
<div>❌ Error loading pools: ${error.message}</div>
|
| 1854 |
+
</div>
|
| 1855 |
+
`;
|
| 1856 |
+
}
|
| 1857 |
+
}
|
| 1858 |
+
|
| 1859 |
+
function createPoolCard(pool) {
|
| 1860 |
+
const currentProvider = pool.current_provider
|
| 1861 |
+
? `<div style="margin-bottom: 15px; padding: 12px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border: 1px solid rgba(16, 185, 129, 0.3);">
|
| 1862 |
+
<div style="display: flex; align-items: center; gap: 8px;">
|
| 1863 |
+
<span style="width: 10px; height: 10px; background: var(--accent-green); border-radius: 50%; display: inline-block;"></span>
|
| 1864 |
+
<span style="font-weight: 600;">Current: ${pool.current_provider.name}</span>
|
| 1865 |
+
</div>
|
| 1866 |
+
</div>`
|
| 1867 |
+
: '<div style="margin-bottom: 15px; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 10px; border: 1px solid rgba(239, 68, 68, 0.3); color: var(--text-secondary);">No active provider</div>';
|
| 1868 |
+
|
| 1869 |
+
const membersHTML = pool.members && pool.members.length > 0
|
| 1870 |
+
? pool.members.map(member => {
|
| 1871 |
+
const successRate = member.success_rate || 0;
|
| 1872 |
+
const statusClass = successRate >= 90 ? 'badge-success' : successRate >= 70 ? 'badge-warning' : 'badge-danger';
|
| 1873 |
+
const rateLimit = member.rate_limit || { usage: 0, limit: 100, percentage: 0 };
|
| 1874 |
+
|
| 1875 |
+
return `
|
| 1876 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px; margin-bottom: 8px;">
|
| 1877 |
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
| 1878 |
+
<div style="font-weight: 600;">${member.provider_name}</div>
|
| 1879 |
+
<span class="badge ${statusClass}">${successRate.toFixed(1)}%</span>
|
| 1880 |
+
</div>
|
| 1881 |
+
<div style="display: flex; gap: 15px; font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;">
|
| 1882 |
+
<span>Used: ${member.use_count || 0}</span>
|
| 1883 |
+
<span>Priority: ${member.priority || 1}</span>
|
| 1884 |
+
<span>Weight: ${member.weight || 1}</span>
|
| 1885 |
+
</div>
|
| 1886 |
+
<div>
|
| 1887 |
+
<div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;">
|
| 1888 |
+
Rate Limit: ${rateLimit.usage}/${rateLimit.limit} (${rateLimit.percentage}%)
|
| 1889 |
+
</div>
|
| 1890 |
+
<div style="height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; overflow: hidden;">
|
| 1891 |
+
<div style="height: 100%; background: ${rateLimit.percentage < 70 ? 'var(--accent-green)' : rateLimit.percentage < 90 ? 'var(--accent-yellow)' : 'var(--accent-red)'}; width: ${rateLimit.percentage}%; transition: width 0.3s;"></div>
|
| 1892 |
+
</div>
|
| 1893 |
+
</div>
|
| 1894 |
+
</div>
|
| 1895 |
+
`;
|
| 1896 |
+
}).join('')
|
| 1897 |
+
: '<div style="color: var(--text-secondary); font-size: 14px; padding: 20px; text-align: center;">No members in pool</div>';
|
| 1898 |
+
|
| 1899 |
+
return `
|
| 1900 |
+
<div class="pool-card-hover" style="background: rgba(17, 24, 39, 0.6); backdrop-filter: blur(10px); border: 1px solid var(--border); border-radius: 20px; padding: 25px;">
|
| 1901 |
+
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;">
|
| 1902 |
+
<div>
|
| 1903 |
+
<div style="font-size: 20px; font-weight: 700; margin-bottom: 8px;">${pool.pool_name}</div>
|
| 1904 |
+
<span class="badge badge-info">${pool.category}</span>
|
| 1905 |
+
</div>
|
| 1906 |
+
<div style="display: flex; gap: 8px;">
|
| 1907 |
+
<button onclick="addMemberToPool(${pool.pool_id})" style="padding: 8px 12px; background: rgba(59, 130, 246, 0.2); border: 1px solid var(--accent-blue); border-radius: 8px; color: var(--accent-blue); cursor: pointer; font-size: 12px; font-weight: 600;">➕</button>
|
| 1908 |
+
<button onclick="rotatePool(${pool.pool_id})" style="padding: 8px 12px; background: rgba(16, 185, 129, 0.2); border: 1px solid var(--accent-green); border-radius: 8px; color: var(--accent-green); cursor: pointer; font-size: 12px; font-weight: 600;">🔄</button>
|
| 1909 |
+
<button onclick="deletePool(${pool.pool_id}, '${pool.pool_name}')" style="padding: 8px 12px; background: rgba(239, 68, 68, 0.2); border: 1px solid var(--accent-red); border-radius: 8px; color: var(--accent-red); cursor: pointer; font-size: 12px; font-weight: 600;">🗑️</button>
|
| 1910 |
+
</div>
|
| 1911 |
+
</div>
|
| 1912 |
+
|
| 1913 |
+
${currentProvider}
|
| 1914 |
+
|
| 1915 |
+
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 20px 0;">
|
| 1916 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;">
|
| 1917 |
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Strategy</div>
|
| 1918 |
+
<div style="font-weight: 600; font-size: 14px;">${pool.rotation_strategy.replace('_', ' ')}</div>
|
| 1919 |
+
</div>
|
| 1920 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;">
|
| 1921 |
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Rotations</div>
|
| 1922 |
+
<div style="font-weight: 600; font-size: 14px;">${pool.total_rotations || 0}</div>
|
| 1923 |
+
</div>
|
| 1924 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;">
|
| 1925 |
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Members</div>
|
| 1926 |
+
<div style="font-weight: 600; font-size: 14px;">${pool.members ? pool.members.length : 0}</div>
|
| 1927 |
+
</div>
|
| 1928 |
+
<div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;">
|
| 1929 |
+
<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Status</div>
|
| 1930 |
+
<span class="badge ${pool.enabled ? 'badge-success' : 'badge-danger'}">${pool.enabled ? 'Enabled' : 'Disabled'}</span>
|
| 1931 |
+
</div>
|
| 1932 |
+
</div>
|
| 1933 |
+
|
| 1934 |
+
<div style="margin-top: 20px;">
|
| 1935 |
+
<div style="font-weight: 600; margin-bottom: 12px; font-size: 14px;">Pool Members</div>
|
| 1936 |
+
<div style="max-height: 300px; overflow-y: auto;">
|
| 1937 |
+
${membersHTML}
|
| 1938 |
+
</div>
|
| 1939 |
+
</div>
|
| 1940 |
+
</div>
|
| 1941 |
+
`;
|
| 1942 |
+
}
|
| 1943 |
+
|
| 1944 |
+
async function loadProvidersForPool() {
|
| 1945 |
+
try {
|
| 1946 |
+
const providers = await fetch('/api/providers').then(r => r.json());
|
| 1947 |
+
allProvidersList = providers;
|
| 1948 |
+
const select = document.getElementById('memberProvider');
|
| 1949 |
+
select.innerHTML = '<option value="">Select a provider...</option>' + providers.map(p => {
|
| 1950 |
+
const providerId = p.name.toLowerCase().replace(/\s+/g, '_');
|
| 1951 |
+
return `<option value="${providerId}">${p.name} (${p.category})</option>`;
|
| 1952 |
+
}).join('');
|
| 1953 |
+
} catch (error) {
|
| 1954 |
+
console.error('Error loading providers:', error);
|
| 1955 |
+
}
|
| 1956 |
+
}
|
| 1957 |
+
|
| 1958 |
+
function showCreatePoolModal() {
|
| 1959 |
+
document.getElementById('createPoolModal').classList.add('active');
|
| 1960 |
+
}
|
| 1961 |
+
|
| 1962 |
+
function closeCreatePoolModal() {
|
| 1963 |
+
document.getElementById('createPoolModal').classList.remove('active');
|
| 1964 |
+
document.getElementById('createPoolForm').reset();
|
| 1965 |
+
}
|
| 1966 |
+
|
| 1967 |
+
function addMemberToPool(poolId) {
|
| 1968 |
+
currentPoolId = poolId;
|
| 1969 |
+
loadProvidersForPool();
|
| 1970 |
+
document.getElementById('addMemberModal').classList.add('active');
|
| 1971 |
+
}
|
| 1972 |
+
|
| 1973 |
+
function closeAddMemberModal() {
|
| 1974 |
+
document.getElementById('addMemberModal').classList.remove('active');
|
| 1975 |
+
document.getElementById('addMemberForm').reset();
|
| 1976 |
+
currentPoolId = null;
|
| 1977 |
+
}
|
| 1978 |
+
|
| 1979 |
+
document.getElementById('createPoolForm').addEventListener('submit', async (e) => {
|
| 1980 |
+
e.preventDefault();
|
| 1981 |
+
const data = {
|
| 1982 |
+
name: document.getElementById('poolName').value,
|
| 1983 |
+
category: document.getElementById('poolCategory').value,
|
| 1984 |
+
rotation_strategy: document.getElementById('rotationStrategy').value,
|
| 1985 |
+
description: document.getElementById('poolDescription').value
|
| 1986 |
+
};
|
| 1987 |
+
|
| 1988 |
+
try {
|
| 1989 |
+
const response = await fetch('/api/pools', {
|
| 1990 |
+
method: 'POST',
|
| 1991 |
+
headers: { 'Content-Type': 'application/json' },
|
| 1992 |
+
body: JSON.stringify(data)
|
| 1993 |
+
});
|
| 1994 |
+
|
| 1995 |
+
if (response.ok) {
|
| 1996 |
+
showToast('✅ Pool created successfully!', 'success');
|
| 1997 |
+
closeCreatePoolModal();
|
| 1998 |
+
loadPools();
|
| 1999 |
+
} else {
|
| 2000 |
+
const error = await response.json();
|
| 2001 |
+
showToast('❌ Error: ' + (error.detail || 'Failed to create pool'), 'error');
|
| 2002 |
+
}
|
| 2003 |
+
} catch (error) {
|
| 2004 |
+
showToast('❌ Error: ' + error.message, 'error');
|
| 2005 |
+
console.error(error);
|
| 2006 |
+
}
|
| 2007 |
+
});
|
| 2008 |
+
|
| 2009 |
+
document.getElementById('addMemberForm').addEventListener('submit', async (e) => {
|
| 2010 |
+
e.preventDefault();
|
| 2011 |
+
const data = {
|
| 2012 |
+
provider_id: document.getElementById('memberProvider').value,
|
| 2013 |
+
priority: parseInt(document.getElementById('memberPriority').value),
|
| 2014 |
+
weight: parseInt(document.getElementById('memberWeight').value)
|
| 2015 |
+
};
|
| 2016 |
+
|
| 2017 |
+
try {
|
| 2018 |
+
const response = await fetch(`/api/pools/${currentPoolId}/members`, {
|
| 2019 |
+
method: 'POST',
|
| 2020 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2021 |
+
body: JSON.stringify(data)
|
| 2022 |
+
});
|
| 2023 |
+
|
| 2024 |
+
if (response.ok) {
|
| 2025 |
+
showToast('✅ Member added successfully!', 'success');
|
| 2026 |
+
closeAddMemberModal();
|
| 2027 |
+
loadPools();
|
| 2028 |
+
} else {
|
| 2029 |
+
const error = await response.json();
|
| 2030 |
+
showToast('❌ Error: ' + (error.detail || 'Failed to add member'), 'error');
|
| 2031 |
+
}
|
| 2032 |
+
} catch (error) {
|
| 2033 |
+
showToast('❌ Error: ' + error.message, 'error');
|
| 2034 |
+
console.error(error);
|
| 2035 |
+
}
|
| 2036 |
+
});
|
| 2037 |
+
|
| 2038 |
+
async function rotatePool(poolId) {
|
| 2039 |
+
try {
|
| 2040 |
+
const response = await fetch(`/api/pools/${poolId}/rotate`, {
|
| 2041 |
+
method: 'POST',
|
| 2042 |
+
headers: { 'Content-Type': 'application/json' },
|
| 2043 |
+
body: JSON.stringify({ reason: 'manual' })
|
| 2044 |
+
});
|
| 2045 |
+
|
| 2046 |
+
if (response.ok) {
|
| 2047 |
+
const result = await response.json();
|
| 2048 |
+
showToast(`✅ Rotated to ${result.provider_name}`, 'success');
|
| 2049 |
+
loadPools();
|
| 2050 |
+
} else {
|
| 2051 |
+
const error = await response.json();
|
| 2052 |
+
showToast('❌ Error: ' + (error.detail || 'Failed to rotate'), 'error');
|
| 2053 |
+
}
|
| 2054 |
+
} catch (error) {
|
| 2055 |
+
showToast('❌ Error: ' + error.message, 'error');
|
| 2056 |
+
console.error(error);
|
| 2057 |
+
}
|
| 2058 |
+
}
|
| 2059 |
+
|
| 2060 |
+
async function deletePool(poolId, poolName) {
|
| 2061 |
+
if (!confirm(`Are you sure you want to delete pool "${poolName}"?`)) {
|
| 2062 |
+
return;
|
| 2063 |
+
}
|
| 2064 |
+
|
| 2065 |
+
try {
|
| 2066 |
+
const response = await fetch(`/api/pools/${poolId}`, {
|
| 2067 |
+
method: 'DELETE'
|
| 2068 |
+
});
|
| 2069 |
+
|
| 2070 |
+
if (response.ok) {
|
| 2071 |
+
showToast('✅ Pool deleted successfully!', 'success');
|
| 2072 |
+
loadPools();
|
| 2073 |
+
} else {
|
| 2074 |
+
const error = await response.json();
|
| 2075 |
+
showToast('❌ Error: ' + (error.detail || 'Failed to delete pool'), 'error');
|
| 2076 |
+
}
|
| 2077 |
+
} catch (error) {
|
| 2078 |
+
alert('❌ Error: ' + error.message);
|
| 2079 |
+
console.error(error);
|
| 2080 |
+
}
|
| 2081 |
+
}
|
| 2082 |
+
|
| 2083 |
+
// Toast notification function
|
| 2084 |
+
function showToast(message, type = 'info') {
|
| 2085 |
+
const toast = document.createElement('div');
|
| 2086 |
+
toast.className = `toast toast-${type}`;
|
| 2087 |
+
toast.innerHTML = `
|
| 2088 |
+
<span style="font-size: 20px;">${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'}</span>
|
| 2089 |
+
<span>${message}</span>
|
| 2090 |
+
`;
|
| 2091 |
+
document.body.appendChild(toast);
|
| 2092 |
+
|
| 2093 |
+
setTimeout(() => {
|
| 2094 |
+
toast.style.animation = 'slideInRight 0.3s reverse';
|
| 2095 |
+
setTimeout(() => toast.remove(), 300);
|
| 2096 |
+
}, 3000);
|
| 2097 |
+
}
|
| 2098 |
+
|
| 2099 |
+
// Close modals when clicking outside
|
| 2100 |
+
document.addEventListener('click', (e) => {
|
| 2101 |
+
if (e.target.classList.contains('modal')) {
|
| 2102 |
+
e.target.style.display = 'none';
|
| 2103 |
+
}
|
| 2104 |
+
});
|
| 2105 |
+
</script>
|
| 2106 |
+
</body>
|
| 2107 |
+
</html>
|
utils/__pycache__/__init__.cpython-313.pyc
ADDED
|
Binary file (192 Bytes). View file
|
|
|
utils/__pycache__/logger.cpython-313.pyc
ADDED
|
Binary file (5.22 kB). View file
|
|
|