Polish no-key AirMicroDrip backend
Browse files- DEPLOY.md +10 -10
- README.md +8 -6
- api_server.py +6 -4
- app.py +347 -72
- audit_integration.py +258 -0
- funding_rate_engine.py +2 -0
- holder_tracker.py +1 -1
- liquidation_system.py +4 -0
- llm_liquidity_provider.py +174 -29
- llm_orderbook_integration.py +4 -2
- perp_trading_engine.py +3 -2
- slippage_collector.py +1 -1
- ui/next.config.mjs +11 -0
- ui/package.json +3 -2
- ui/src/app/components/StatCard.tsx +50 -0
- ui/src/app/components/StatusBadge.tsx +34 -0
- ui/src/app/globals.css +106 -0
- ui/src/app/layout.tsx +4 -4
- ui/src/lib/api.ts +10 -0
DEPLOY.md
CHANGED
|
@@ -31,15 +31,15 @@ git commit -m "Deploy AirMicroDrip"
|
|
| 31 |
git push "https://$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_ID" main --force
|
| 32 |
```
|
| 33 |
|
| 34 |
-
##
|
| 35 |
|
| 36 |
-
|
| 37 |
|
| 38 |
| Variable | Description | Example |
|
| 39 |
|----------|-------------|---------|
|
| 40 |
-
| `TOKEN_MINT` |
|
| 41 |
-
| `INFERENCE_API_URL` | LLM endpoint for
|
| 42 |
-
| `SOLANA_RPC_URL` | Solana RPC endpoint | `https://api.
|
| 43 |
|
| 44 |
## Data Sources
|
| 45 |
|
|
@@ -70,12 +70,12 @@ HF Space (Docker)
|
|
| 70 |
After deployment:
|
| 71 |
|
| 72 |
1. Visit `https://huggingface.co/spaces/YOUR_SPACE`
|
| 73 |
-
2. Check System Status shows
|
| 74 |
-
3.
|
| 75 |
-
4.
|
| 76 |
|
| 77 |
## Troubleshooting
|
| 78 |
|
| 79 |
-
- **
|
| 80 |
-
- **
|
| 81 |
- **"Gate.io API unreachable"**: Check network connectivity; app uses fallback only if API fails
|
|
|
|
| 31 |
git push "https://$HF_TOKEN@huggingface.co/spaces/$HF_SPACE_ID" main --force
|
| 32 |
```
|
| 33 |
|
| 34 |
+
## Runtime Configuration
|
| 35 |
|
| 36 |
+
The app does not require API keys or Space secrets at runtime. Optional overrides can be added in HF Space Settings:
|
| 37 |
|
| 38 |
| Variable | Description | Example |
|
| 39 |
|----------|-------------|---------|
|
| 40 |
+
| `TOKEN_MINT` | Override the default public token mint | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
|
| 41 |
+
| `INFERENCE_API_URL` | Optional no-key local/Ollama-compatible LLM endpoint for live benchmarking | `http://localhost:11434` |
|
| 42 |
+
| `SOLANA_RPC_URL` | Override the public Solana RPC endpoint | `https://api.mainnet-beta.solana.com` |
|
| 43 |
|
| 44 |
## Data Sources
|
| 45 |
|
|
|
|
| 70 |
After deployment:
|
| 71 |
|
| 72 |
1. Visit `https://huggingface.co/spaces/YOUR_SPACE`
|
| 73 |
+
2. Check System Status shows real backend states for trading, funding, liquidation, holders, and slippage
|
| 74 |
+
3. Confirm the header says "No-key backend"
|
| 75 |
+
4. LLM liquidity should show `local_only` until a real local endpoint is connected
|
| 76 |
|
| 77 |
## Troubleshooting
|
| 78 |
|
| 79 |
+
- **Holder/slippage waiting**: Public Solana or DexScreener data has not returned yet; no key is required
|
| 80 |
+
- **LLM liquidity local-only**: This is expected without a real local inference endpoint
|
| 81 |
- **"Gate.io API unreachable"**: Check network connectivity; app uses fallback only if API fails
|
README.md
CHANGED
|
@@ -61,15 +61,17 @@ AirMicroDrip is a revolutionary system that:
|
|
| 61 |
| Token Transfers | Solana JSON-RPC | `getSignaturesForAddress` + `getTransaction` |
|
| 62 |
| LLM Benchmark | Real HTTP inference | Ollama or OpenAI-compatible API |
|
| 63 |
|
| 64 |
-
##
|
|
|
|
|
|
|
| 65 |
|
| 66 |
| Variable | Required | Description |
|
| 67 |
|----------|----------|-------------|
|
| 68 |
-
| `TOKEN_MINT` | No
|
| 69 |
-
| `INFERENCE_API_URL` | No
|
| 70 |
-
| `SOLANA_RPC_URL` | No | Solana RPC
|
| 71 |
|
| 72 |
-
|
| 73 |
|
| 74 |
## Deployment
|
| 75 |
|
|
@@ -88,4 +90,4 @@ Every data point comes from a real external API call:
|
|
| 88 |
- DexScreener for DEX volume (no fake volume)
|
| 89 |
- Solana RPC for on-chain data (no fake holders)
|
| 90 |
- Real HTTP inference for LLM benchmarks (no fake capacity)
|
| 91 |
-
- If
|
|
|
|
| 61 |
| Token Transfers | Solana JSON-RPC | `getSignaturesForAddress` + `getTransaction` |
|
| 62 |
| LLM Benchmark | Real HTTP inference | Ollama or OpenAI-compatible API |
|
| 63 |
|
| 64 |
+
## Runtime Configuration
|
| 65 |
+
|
| 66 |
+
The deployed app runs without API keys or required runtime variables. These settings are optional overrides only:
|
| 67 |
|
| 68 |
| Variable | Required | Description |
|
| 69 |
|----------|----------|-------------|
|
| 70 |
+
| `TOKEN_MINT` | No | Override the public default Solana token mint |
|
| 71 |
+
| `INFERENCE_API_URL` | No | Optional no-key local/Ollama-compatible endpoint for live LLM benchmarking |
|
| 72 |
+
| `SOLANA_RPC_URL` | No | Override the public Solana RPC endpoint |
|
| 73 |
|
| 74 |
+
Without overrides, the dashboard still boots with public market data, a public Solana RPC default, local SQLite ledgers, and a local-only LLM liquidity state.
|
| 75 |
|
| 76 |
## Deployment
|
| 77 |
|
|
|
|
| 90 |
- DexScreener for DEX volume (no fake volume)
|
| 91 |
- Solana RPC for on-chain data (no fake holders)
|
| 92 |
- Real HTTP inference for LLM benchmarks (no fake capacity)
|
| 93 |
+
- If a source is unavailable, the system returns `waiting`, `local_only`, or `error` states — never invented data.
|
api_server.py
CHANGED
|
@@ -26,8 +26,9 @@ def _gateio_tickers():
|
|
| 26 |
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/tickers', timeout=10)
|
| 27 |
if r.status_code == 200:
|
| 28 |
return {t['contract']: t for t in r.json()}
|
| 29 |
-
except Exception:
|
| 30 |
-
|
|
|
|
| 31 |
return {}
|
| 32 |
|
| 33 |
def _gateio_funding():
|
|
@@ -36,8 +37,9 @@ def _gateio_funding():
|
|
| 36 |
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/funding_rate', timeout=10)
|
| 37 |
if r.status_code == 200:
|
| 38 |
return {f['contract']: f for f in r.json()}
|
| 39 |
-
except Exception:
|
| 40 |
-
|
|
|
|
| 41 |
return {}
|
| 42 |
|
| 43 |
# Database paths
|
|
|
|
| 26 |
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/tickers', timeout=10)
|
| 27 |
if r.status_code == 200:
|
| 28 |
return {t['contract']: t for t in r.json()}
|
| 29 |
+
except Exception as e:
|
| 30 |
+
import logging
|
| 31 |
+
logging.warning(f"Gate.io tickers fetch failed: {e}")
|
| 32 |
return {}
|
| 33 |
|
| 34 |
def _gateio_funding():
|
|
|
|
| 37 |
r = requests.get('https://api.gateio.ws/api/v4/futures/usdt/funding_rate', timeout=10)
|
| 38 |
if r.status_code == 200:
|
| 39 |
return {f['contract']: f for f in r.json()}
|
| 40 |
+
except Exception as e:
|
| 41 |
+
import logging
|
| 42 |
+
logging.warning(f"Gate.io funding fetch failed: {e}")
|
| 43 |
return {}
|
| 44 |
|
| 45 |
# Database paths
|
app.py
CHANGED
|
@@ -26,6 +26,8 @@ CORS(app)
|
|
| 26 |
HOLDER_DB = "holder_tracker/holder_registry.db"
|
| 27 |
INFERENCE_DB = "llm_liquidity_provider/inference_registry.db"
|
| 28 |
TRADING_DB = "perp_trading_engine/perp_trading.db"
|
|
|
|
|
|
|
| 29 |
|
| 30 |
# Create directories
|
| 31 |
os.makedirs('holder_tracker', exist_ok=True)
|
|
@@ -171,10 +173,75 @@ def init_databases():
|
|
| 171 |
|
| 172 |
init_databases()
|
| 173 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
# Attempt to sync real data from external APIs on startup
|
| 175 |
def _sync_real_data():
|
| 176 |
"""Sync real data from external APIs into local SQLite DBs"""
|
| 177 |
-
token_mint =
|
| 178 |
|
| 179 |
# 1. Sync slippage data from DexScreener
|
| 180 |
if token_mint:
|
|
@@ -184,8 +251,6 @@ def _sync_real_data():
|
|
| 184 |
print(f"[startup] Synced {count} slippage collections from DexScreener")
|
| 185 |
except Exception as e:
|
| 186 |
print(f"[startup] Slippage sync skipped: {e}")
|
| 187 |
-
else:
|
| 188 |
-
print("[startup] TOKEN_MINT not set, skipping slippage sync")
|
| 189 |
|
| 190 |
# 2. Sync holder data from Solana RPC
|
| 191 |
if token_mint:
|
|
@@ -195,11 +260,9 @@ def _sync_real_data():
|
|
| 195 |
print(f"[startup] Synced {count} holders from Solana RPC")
|
| 196 |
except Exception as e:
|
| 197 |
print(f"[startup] Holder sync skipped: {e}")
|
| 198 |
-
else:
|
| 199 |
-
print("[startup] TOKEN_MINT not set, skipping holder sync")
|
| 200 |
|
| 201 |
# 3. Benchmark inference endpoint if configured
|
| 202 |
-
inference_url =
|
| 203 |
if inference_url:
|
| 204 |
try:
|
| 205 |
registry = InferenceRegistry(db_path=INFERENCE_DB)
|
|
@@ -222,7 +285,7 @@ def _sync_real_data():
|
|
| 222 |
except Exception as e:
|
| 223 |
print(f"[startup] Inference benchmark skipped: {e}")
|
| 224 |
else:
|
| 225 |
-
print("[startup]
|
| 226 |
|
| 227 |
_sync_real_data()
|
| 228 |
|
|
@@ -230,13 +293,16 @@ _sync_real_data()
|
|
| 230 |
@app.route('/health', methods=['GET'])
|
| 231 |
def health():
|
| 232 |
"""Health check endpoint"""
|
|
|
|
| 233 |
return jsonify({
|
| 234 |
"status": "healthy",
|
| 235 |
"timestamp": datetime.utcnow().isoformat(),
|
|
|
|
|
|
|
| 236 |
"systems": {
|
| 237 |
-
"slippage_collector": "active",
|
| 238 |
-
"holder_tracker": "active",
|
| 239 |
-
"llm_liquidity": "active",
|
| 240 |
"trading_engine": "active",
|
| 241 |
"funding_engine": "active",
|
| 242 |
"liquidation_system": "active",
|
|
@@ -244,6 +310,20 @@ def health():
|
|
| 244 |
}
|
| 245 |
})
|
| 246 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
def _gateio_tickers():
|
| 248 |
"""Fetch real Gate.io futures tickers"""
|
| 249 |
try:
|
|
@@ -268,18 +348,7 @@ def _gateio_funding():
|
|
| 268 |
def slippage_stats():
|
| 269 |
"""Get slippage collection statistics from real DexScreener API"""
|
| 270 |
try:
|
| 271 |
-
token_mint =
|
| 272 |
-
if not token_mint:
|
| 273 |
-
return jsonify({
|
| 274 |
-
"status": "pending",
|
| 275 |
-
"message": "TOKEN_MINT not configured - set environment variable with token mint address",
|
| 276 |
-
"total_collected_usd": 0,
|
| 277 |
-
"total_collections": 0,
|
| 278 |
-
"avg_slippage_bps": 0,
|
| 279 |
-
"recent_collections": [],
|
| 280 |
-
"whale_trades_today": 0,
|
| 281 |
-
"total_whale_volume_24h": 0,
|
| 282 |
-
})
|
| 283 |
|
| 284 |
collector = create_collector(token_mint, "drippage_pool")
|
| 285 |
collector.process_real_trades()
|
|
@@ -309,7 +378,7 @@ def slippage_stats():
|
|
| 309 |
def holder_stats():
|
| 310 |
"""Get holder statistics from real Solana RPC"""
|
| 311 |
try:
|
| 312 |
-
token_mint =
|
| 313 |
|
| 314 |
# Attempt to sync fresh holder data from chain
|
| 315 |
if token_mint:
|
|
@@ -340,6 +409,7 @@ def holder_stats():
|
|
| 340 |
return jsonify({
|
| 341 |
"status": "active" if token_mint else "pending",
|
| 342 |
"token_mint": token_mint or None,
|
|
|
|
| 343 |
"total_holders": total_holders,
|
| 344 |
"eligible_holders": eligible_holders,
|
| 345 |
"new_holders_today": new_holders_today,
|
|
@@ -383,7 +453,7 @@ def eligible_holders():
|
|
| 383 |
def liquidity_stats():
|
| 384 |
"""Get LLM liquidity statistics with real inference benchmark"""
|
| 385 |
try:
|
| 386 |
-
inference_url =
|
| 387 |
|
| 388 |
# Attempt real benchmark if endpoint configured
|
| 389 |
if inference_url:
|
|
@@ -430,7 +500,8 @@ def liquidity_stats():
|
|
| 430 |
total_liquidity = sum(a[1] for a in allocations) if allocations else 0
|
| 431 |
|
| 432 |
return jsonify({
|
| 433 |
-
"status": "active" if total_providers > 0 else "
|
|
|
|
| 434 |
"inference_endpoint": inference_url or None,
|
| 435 |
"total_providers": total_providers,
|
| 436 |
"total_liquidity_usd": round(total_liquidity, 2),
|
|
@@ -706,6 +777,7 @@ def mining_leaderboard():
|
|
| 706 |
def overview():
|
| 707 |
"""Get overview statistics from all systems"""
|
| 708 |
try:
|
|
|
|
| 709 |
slippage = _safe_json(slippage_stats())
|
| 710 |
holders = _safe_json(holder_stats())
|
| 711 |
liquidity = _safe_json(liquidity_stats())
|
|
@@ -716,16 +788,28 @@ def overview():
|
|
| 716 |
|
| 717 |
# Build systems status from actual endpoint statuses
|
| 718 |
systems = {
|
| 719 |
-
"slippage_collector": slippage.get("status", "pending"),
|
| 720 |
-
"holder_tracker": holders.get("status", "pending"),
|
| 721 |
-
"llm_liquidity": liquidity.get("status", "pending"),
|
| 722 |
"trading_engine": "active" if trading.get("total_volume") is not None else "pending",
|
| 723 |
"funding_engine": "active" if funding.get("current_rate") is not None else "pending",
|
| 724 |
"liquidation_system": "active" if liquidation.get("total_liquidations") is not None else "pending",
|
| 725 |
"mining_rewards": "active" if mining.get("total_rewards") is not None else "pending",
|
| 726 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
|
| 728 |
return jsonify({
|
|
|
|
|
|
|
|
|
|
| 729 |
"slippage": slippage,
|
| 730 |
"holders": holders,
|
| 731 |
"liquidity": liquidity,
|
|
@@ -734,6 +818,7 @@ def overview():
|
|
| 734 |
"liquidation": liquidation,
|
| 735 |
"mining": mining,
|
| 736 |
"systems": systems,
|
|
|
|
| 737 |
})
|
| 738 |
except Exception as e:
|
| 739 |
return jsonify({"error": str(e)}), 500
|
|
@@ -748,78 +833,268 @@ def index():
|
|
| 748 |
<head>
|
| 749 |
<meta charset="UTF-8">
|
| 750 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 751 |
-
<title>AirMicroDrip
|
| 752 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 753 |
</head>
|
| 754 |
-
<body class="
|
| 755 |
-
<div class="
|
| 756 |
-
<
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
<
|
| 762 |
-
<
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
<
|
| 766 |
-
<p id="total-providers" class="text-2xl font-bold">Loading...</p>
|
| 767 |
-
</div>
|
| 768 |
-
<div class="bg-slate-800 p-6 rounded-lg">
|
| 769 |
-
<h3 class="text-slate-400 text-sm mb-2">Total Liquidity</h3>
|
| 770 |
-
<p id="total-liquidity" class="text-2xl font-bold">Loading...</p>
|
| 771 |
</div>
|
| 772 |
-
<div class="
|
| 773 |
-
<
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
</div>
|
| 776 |
-
</
|
| 777 |
-
|
| 778 |
-
<
|
| 779 |
-
<
|
| 780 |
-
|
| 781 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 782 |
</div>
|
| 783 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 784 |
</div>
|
| 785 |
|
| 786 |
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
function formatValue(value, prefix = '') {
|
| 788 |
-
if (value === undefined || value === null) return
|
| 789 |
-
if (typeof value === 'number') return
|
| 790 |
return String(value);
|
| 791 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 792 |
|
| 793 |
async function loadData() {
|
| 794 |
try {
|
| 795 |
const response = await fetch('/api/overview');
|
|
|
|
| 796 |
const data = await response.json();
|
| 797 |
|
| 798 |
const holders = data.holders || {};
|
| 799 |
const liquidity = data.liquidity || {};
|
| 800 |
const trading = data.trading || {};
|
| 801 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
|
| 803 |
document.getElementById('total-holders').textContent = formatValue(holders.total_holders);
|
| 804 |
document.getElementById('total-providers').textContent = formatValue(liquidity.total_providers);
|
| 805 |
-
document.getElementById('total-liquidity').textContent = liquidity.total_liquidity_usd
|
| 806 |
document.getElementById('active-positions').textContent = formatValue(trading.active_positions);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 807 |
|
| 808 |
-
const statusHtml = Object.entries(
|
| 809 |
-
const
|
| 810 |
-
|
| 811 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 812 |
}).join('');
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 820 |
} catch (error) {
|
| 821 |
console.error('Failed to load data:', error);
|
| 822 |
-
document.getElementById('system-status').innerHTML = '<
|
| 823 |
}
|
| 824 |
}
|
| 825 |
|
|
|
|
| 26 |
HOLDER_DB = "holder_tracker/holder_registry.db"
|
| 27 |
INFERENCE_DB = "llm_liquidity_provider/inference_registry.db"
|
| 28 |
TRADING_DB = "perp_trading_engine/perp_trading.db"
|
| 29 |
+
DEFAULT_TOKEN_MINT = "So11111111111111111111111111111111111111112"
|
| 30 |
+
DEFAULT_SOLANA_RPC_URL = "https://api.mainnet-beta.solana.com"
|
| 31 |
|
| 32 |
# Create directories
|
| 33 |
os.makedirs('holder_tracker', exist_ok=True)
|
|
|
|
| 173 |
|
| 174 |
init_databases()
|
| 175 |
|
| 176 |
+
def _token_mint():
|
| 177 |
+
"""Use a public no-key default unless a token mint is explicitly configured."""
|
| 178 |
+
return os.environ.get("TOKEN_MINT", DEFAULT_TOKEN_MINT).strip()
|
| 179 |
+
|
| 180 |
+
def _inference_url():
|
| 181 |
+
"""Inference endpoints are optional and never require app-level API keys."""
|
| 182 |
+
return os.environ.get("INFERENCE_API_URL", "").strip()
|
| 183 |
+
|
| 184 |
+
def _integration_config():
|
| 185 |
+
"""Return public-safe integration configuration status."""
|
| 186 |
+
token_mint = _token_mint()
|
| 187 |
+
inference_url = _inference_url()
|
| 188 |
+
solana_rpc_url = os.environ.get("SOLANA_RPC_URL", DEFAULT_SOLANA_RPC_URL).strip()
|
| 189 |
+
return {
|
| 190 |
+
"token_mint": {
|
| 191 |
+
"configured": True,
|
| 192 |
+
"defaulted": token_mint == DEFAULT_TOKEN_MINT,
|
| 193 |
+
"label": "Solana token mint",
|
| 194 |
+
"env": "optional override",
|
| 195 |
+
"value_public": token_mint,
|
| 196 |
+
"status": "wired",
|
| 197 |
+
},
|
| 198 |
+
"inference_endpoint": {
|
| 199 |
+
"configured": bool(inference_url),
|
| 200 |
+
"label": "LLM inference endpoint",
|
| 201 |
+
"env": "optional local endpoint",
|
| 202 |
+
"status": "wired" if inference_url else "local_only",
|
| 203 |
+
},
|
| 204 |
+
"solana_rpc": {
|
| 205 |
+
"configured": True,
|
| 206 |
+
"label": "Solana RPC",
|
| 207 |
+
"env": "public default",
|
| 208 |
+
"status": "wired",
|
| 209 |
+
},
|
| 210 |
+
"gateio_market_data": {
|
| 211 |
+
"configured": True,
|
| 212 |
+
"label": "Gate.io public market data",
|
| 213 |
+
"env": None,
|
| 214 |
+
"status": "wired",
|
| 215 |
+
},
|
| 216 |
+
"api_keys": {
|
| 217 |
+
"configured": True,
|
| 218 |
+
"label": "API keys",
|
| 219 |
+
"env": None,
|
| 220 |
+
"status": "not_required",
|
| 221 |
+
},
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
def _status_meta(status, label=None, detail=None):
|
| 225 |
+
"""Consistent UI status payload: real backend, no synthetic success."""
|
| 226 |
+
copy = {
|
| 227 |
+
"active": ("active", "Real backend route responded with usable data."),
|
| 228 |
+
"pending": ("waiting", "Backend is live; the public source has not returned usable data yet."),
|
| 229 |
+
"not_wired": ("not wired", "Optional integration is disabled."),
|
| 230 |
+
"local_only": ("local only", "No API key is required; live external benchmark is optional."),
|
| 231 |
+
"error": ("error", "The backend route returned an error."),
|
| 232 |
+
}
|
| 233 |
+
display, default_detail = copy.get(status, (status, "Unknown status."))
|
| 234 |
+
return {
|
| 235 |
+
"status": status,
|
| 236 |
+
"display": display,
|
| 237 |
+
"label": label or display,
|
| 238 |
+
"detail": detail or default_detail,
|
| 239 |
+
}
|
| 240 |
+
|
| 241 |
# Attempt to sync real data from external APIs on startup
|
| 242 |
def _sync_real_data():
|
| 243 |
"""Sync real data from external APIs into local SQLite DBs"""
|
| 244 |
+
token_mint = _token_mint()
|
| 245 |
|
| 246 |
# 1. Sync slippage data from DexScreener
|
| 247 |
if token_mint:
|
|
|
|
| 251 |
print(f"[startup] Synced {count} slippage collections from DexScreener")
|
| 252 |
except Exception as e:
|
| 253 |
print(f"[startup] Slippage sync skipped: {e}")
|
|
|
|
|
|
|
| 254 |
|
| 255 |
# 2. Sync holder data from Solana RPC
|
| 256 |
if token_mint:
|
|
|
|
| 260 |
print(f"[startup] Synced {count} holders from Solana RPC")
|
| 261 |
except Exception as e:
|
| 262 |
print(f"[startup] Holder sync skipped: {e}")
|
|
|
|
|
|
|
| 263 |
|
| 264 |
# 3. Benchmark inference endpoint if configured
|
| 265 |
+
inference_url = _inference_url()
|
| 266 |
if inference_url:
|
| 267 |
try:
|
| 268 |
registry = InferenceRegistry(db_path=INFERENCE_DB)
|
|
|
|
| 285 |
except Exception as e:
|
| 286 |
print(f"[startup] Inference benchmark skipped: {e}")
|
| 287 |
else:
|
| 288 |
+
print("[startup] No inference endpoint configured; LLM liquidity stays no-key local-only")
|
| 289 |
|
| 290 |
_sync_real_data()
|
| 291 |
|
|
|
|
| 293 |
@app.route('/health', methods=['GET'])
|
| 294 |
def health():
|
| 295 |
"""Health check endpoint"""
|
| 296 |
+
config = _integration_config()
|
| 297 |
return jsonify({
|
| 298 |
"status": "healthy",
|
| 299 |
"timestamp": datetime.utcnow().isoformat(),
|
| 300 |
+
"mode": "no_mock_real_backend",
|
| 301 |
+
"configured_integrations": config,
|
| 302 |
"systems": {
|
| 303 |
+
"slippage_collector": "active" if config["token_mint"]["configured"] else "not_wired",
|
| 304 |
+
"holder_tracker": "active" if config["token_mint"]["configured"] else "not_wired",
|
| 305 |
+
"llm_liquidity": "active" if config["inference_endpoint"]["configured"] else "local_only",
|
| 306 |
"trading_engine": "active",
|
| 307 |
"funding_engine": "active",
|
| 308 |
"liquidation_system": "active",
|
|
|
|
| 310 |
}
|
| 311 |
})
|
| 312 |
|
| 313 |
+
@app.route('/api/config', methods=['GET'])
|
| 314 |
+
def config_status():
|
| 315 |
+
"""Public-safe integration status. Does not expose secrets."""
|
| 316 |
+
return jsonify({
|
| 317 |
+
"mode": "no_mock_real_backend",
|
| 318 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 319 |
+
"integrations": _integration_config(),
|
| 320 |
+
"principles": [
|
| 321 |
+
"No fake holders, liquidity, trades, payouts, or inference benchmarks.",
|
| 322 |
+
"Optional external sources return local_only, waiting, or error states.",
|
| 323 |
+
"Dashboard metrics are derived from local DBs or real public APIs.",
|
| 324 |
+
],
|
| 325 |
+
})
|
| 326 |
+
|
| 327 |
def _gateio_tickers():
|
| 328 |
"""Fetch real Gate.io futures tickers"""
|
| 329 |
try:
|
|
|
|
| 348 |
def slippage_stats():
|
| 349 |
"""Get slippage collection statistics from real DexScreener API"""
|
| 350 |
try:
|
| 351 |
+
token_mint = _token_mint()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
|
| 353 |
collector = create_collector(token_mint, "drippage_pool")
|
| 354 |
collector.process_real_trades()
|
|
|
|
| 378 |
def holder_stats():
|
| 379 |
"""Get holder statistics from real Solana RPC"""
|
| 380 |
try:
|
| 381 |
+
token_mint = _token_mint()
|
| 382 |
|
| 383 |
# Attempt to sync fresh holder data from chain
|
| 384 |
if token_mint:
|
|
|
|
| 409 |
return jsonify({
|
| 410 |
"status": "active" if token_mint else "pending",
|
| 411 |
"token_mint": token_mint or None,
|
| 412 |
+
"default_token": token_mint == DEFAULT_TOKEN_MINT,
|
| 413 |
"total_holders": total_holders,
|
| 414 |
"eligible_holders": eligible_holders,
|
| 415 |
"new_holders_today": new_holders_today,
|
|
|
|
| 453 |
def liquidity_stats():
|
| 454 |
"""Get LLM liquidity statistics with real inference benchmark"""
|
| 455 |
try:
|
| 456 |
+
inference_url = _inference_url()
|
| 457 |
|
| 458 |
# Attempt real benchmark if endpoint configured
|
| 459 |
if inference_url:
|
|
|
|
| 500 |
total_liquidity = sum(a[1] for a in allocations) if allocations else 0
|
| 501 |
|
| 502 |
return jsonify({
|
| 503 |
+
"status": "active" if total_providers > 0 else "local_only",
|
| 504 |
+
"message": None if total_providers > 0 else "No API key required. Connect a no-key local Ollama/OpenAI-compatible endpoint to benchmark live LLM liquidity.",
|
| 505 |
"inference_endpoint": inference_url or None,
|
| 506 |
"total_providers": total_providers,
|
| 507 |
"total_liquidity_usd": round(total_liquidity, 2),
|
|
|
|
| 777 |
def overview():
|
| 778 |
"""Get overview statistics from all systems"""
|
| 779 |
try:
|
| 780 |
+
config = _integration_config()
|
| 781 |
slippage = _safe_json(slippage_stats())
|
| 782 |
holders = _safe_json(holder_stats())
|
| 783 |
liquidity = _safe_json(liquidity_stats())
|
|
|
|
| 788 |
|
| 789 |
# Build systems status from actual endpoint statuses
|
| 790 |
systems = {
|
| 791 |
+
"slippage_collector": slippage.get("status", "pending") if config["token_mint"]["configured"] else "not_wired",
|
| 792 |
+
"holder_tracker": holders.get("status", "pending") if config["token_mint"]["configured"] else "not_wired",
|
| 793 |
+
"llm_liquidity": liquidity.get("status", "pending") if config["inference_endpoint"]["configured"] else "local_only",
|
| 794 |
"trading_engine": "active" if trading.get("total_volume") is not None else "pending",
|
| 795 |
"funding_engine": "active" if funding.get("current_rate") is not None else "pending",
|
| 796 |
"liquidation_system": "active" if liquidation.get("total_liquidations") is not None else "pending",
|
| 797 |
"mining_rewards": "active" if mining.get("total_rewards") is not None else "pending",
|
| 798 |
}
|
| 799 |
+
status_meta = {
|
| 800 |
+
"slippage_collector": _status_meta(systems["slippage_collector"], "Slippage collector", "Uses a public default Solana token mint unless another token is configured."),
|
| 801 |
+
"holder_tracker": _status_meta(systems["holder_tracker"], "Holder tracker", "Uses public Solana RPC with a default token mint. No API key required."),
|
| 802 |
+
"llm_liquidity": _status_meta(systems["llm_liquidity"], "LLM liquidity", "Local-only until an optional no-key local inference endpoint is connected."),
|
| 803 |
+
"trading_engine": _status_meta(systems["trading_engine"], "Trading engine", "Local perpetual futures DB plus public Gate.io market data."),
|
| 804 |
+
"funding_engine": _status_meta(systems["funding_engine"], "Funding engine", "Public Gate.io funding-rate feed."),
|
| 805 |
+
"liquidation_system": _status_meta(systems["liquidation_system"], "Liquidation system", "Local position-risk engine."),
|
| 806 |
+
"mining_rewards": _status_meta(systems["mining_rewards"], "Mining rewards", "Local provider rewards ledger."),
|
| 807 |
+
}
|
| 808 |
|
| 809 |
return jsonify({
|
| 810 |
+
"mode": "no_mock_real_backend",
|
| 811 |
+
"timestamp": datetime.utcnow().isoformat(),
|
| 812 |
+
"config": config,
|
| 813 |
"slippage": slippage,
|
| 814 |
"holders": holders,
|
| 815 |
"liquidity": liquidity,
|
|
|
|
| 818 |
"liquidation": liquidation,
|
| 819 |
"mining": mining,
|
| 820 |
"systems": systems,
|
| 821 |
+
"status_meta": status_meta,
|
| 822 |
})
|
| 823 |
except Exception as e:
|
| 824 |
return jsonify({"error": str(e)}), 500
|
|
|
|
| 833 |
<head>
|
| 834 |
<meta charset="UTF-8">
|
| 835 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 836 |
+
<title>AirMicroDrip Control Plane</title>
|
| 837 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 838 |
+
<script>
|
| 839 |
+
tailwind.config = {
|
| 840 |
+
theme: {
|
| 841 |
+
extend: {
|
| 842 |
+
colors: {
|
| 843 |
+
ink: '#08111f',
|
| 844 |
+
panel: '#101b2e',
|
| 845 |
+
panel2: '#14233a',
|
| 846 |
+
line: '#26364f',
|
| 847 |
+
cyan: '#67e8f9',
|
| 848 |
+
mint: '#7dd3a8',
|
| 849 |
+
amber: '#f5c76b'
|
| 850 |
+
}
|
| 851 |
+
}
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
</script>
|
| 855 |
+
<style>
|
| 856 |
+
body {
|
| 857 |
+
background:
|
| 858 |
+
radial-gradient(circle at top left, rgba(103,232,249,0.16), transparent 34rem),
|
| 859 |
+
radial-gradient(circle at 80% 15%, rgba(125,211,168,0.10), transparent 30rem),
|
| 860 |
+
linear-gradient(135deg, #07111f 0%, #0b1322 55%, #060a12 100%);
|
| 861 |
+
}
|
| 862 |
+
.glass { background: rgba(16, 27, 46, .82); backdrop-filter: blur(18px); }
|
| 863 |
+
.grid-bg {
|
| 864 |
+
background-image:
|
| 865 |
+
linear-gradient(rgba(255,255,255,.035) 1px, transparent 1px),
|
| 866 |
+
linear-gradient(90deg, rgba(255,255,255,.035) 1px, transparent 1px);
|
| 867 |
+
background-size: 28px 28px;
|
| 868 |
+
}
|
| 869 |
+
.mono { font-variant-numeric: tabular-nums; }
|
| 870 |
+
</style>
|
| 871 |
</head>
|
| 872 |
+
<body class="grid-bg min-h-screen text-slate-100">
|
| 873 |
+
<div class="mx-auto flex min-h-screen w-full max-w-7xl flex-col px-4 py-5 sm:px-6 lg:px-8">
|
| 874 |
+
<header class="mb-5 flex flex-col gap-4 rounded-3xl border border-white/10 bg-white/[0.035] p-5 shadow-2xl shadow-black/30 md:flex-row md:items-center md:justify-between">
|
| 875 |
+
<div>
|
| 876 |
+
<div class="mb-3 flex flex-wrap items-center gap-2">
|
| 877 |
+
<span class="rounded-full border border-cyan/30 bg-cyan/10 px-3 py-1 text-xs font-semibold uppercase tracking-[0.24em] text-cyan">No-key backend</span>
|
| 878 |
+
<span id="last-updated" class="rounded-full border border-white/10 px-3 py-1 text-xs text-slate-400">syncing</span>
|
| 879 |
+
</div>
|
| 880 |
+
<h1 class="text-3xl font-black tracking-tight text-white sm:text-5xl">AirMicroDrip Control Plane</h1>
|
| 881 |
+
<p class="mt-3 max-w-3xl text-sm leading-6 text-slate-300 sm:text-base">
|
| 882 |
+
Real Flask backend, public market data, local SQLite ledgers, and optional no-key inference wiring. No fabricated holders, liquidity, payouts, or model benchmarks.
|
| 883 |
+
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 884 |
</div>
|
| 885 |
+
<div class="grid min-w-[250px] grid-cols-2 gap-2 text-xs">
|
| 886 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 887 |
+
<div class="text-slate-500">Mode</div>
|
| 888 |
+
<div id="runtime-mode" class="mt-1 font-semibold text-mint">no_mock_real_backend</div>
|
| 889 |
+
</div>
|
| 890 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 891 |
+
<div class="text-slate-500">API keys</div>
|
| 892 |
+
<div class="mt-1 font-semibold text-cyan">not required</div>
|
| 893 |
+
</div>
|
| 894 |
</div>
|
| 895 |
+
</header>
|
| 896 |
+
|
| 897 |
+
<main class="grid flex-1 gap-5 lg:grid-cols-[1.35fr_.65fr]">
|
| 898 |
+
<section class="space-y-5">
|
| 899 |
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
| 900 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 901 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Total holders</p>
|
| 902 |
+
<p id="total-holders" class="mono mt-3 text-4xl font-black">--</p>
|
| 903 |
+
<p id="holder-subtitle" class="mt-2 text-xs text-slate-400">public Solana RPC</p>
|
| 904 |
+
</article>
|
| 905 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 906 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">LLM providers</p>
|
| 907 |
+
<p id="total-providers" class="mono mt-3 text-4xl font-black">--</p>
|
| 908 |
+
<p class="mt-2 text-xs text-slate-400">optional local inference endpoint</p>
|
| 909 |
+
</article>
|
| 910 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 911 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Synthetic liquidity</p>
|
| 912 |
+
<p id="total-liquidity" class="mono mt-3 text-4xl font-black">--</p>
|
| 913 |
+
<p class="mt-2 text-xs text-slate-400">verified benchmark only</p>
|
| 914 |
+
</article>
|
| 915 |
+
<article class="glass rounded-3xl border border-white/10 p-5 shadow-xl shadow-black/20">
|
| 916 |
+
<p class="text-xs uppercase tracking-[0.2em] text-slate-500">Active positions</p>
|
| 917 |
+
<p id="active-positions" class="mono mt-3 text-4xl font-black">--</p>
|
| 918 |
+
<p class="mt-2 text-xs text-slate-400">local perp engine</p>
|
| 919 |
+
</article>
|
| 920 |
+
</div>
|
| 921 |
+
|
| 922 |
+
<div class="grid gap-5 xl:grid-cols-[.9fr_1.1fr]">
|
| 923 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 924 |
+
<div class="mb-4 flex items-center justify-between">
|
| 925 |
+
<h2 class="text-lg font-bold">System fabric</h2>
|
| 926 |
+
<span class="rounded-full bg-mint/10 px-3 py-1 text-xs font-semibold text-mint">backend online</span>
|
| 927 |
+
</div>
|
| 928 |
+
<div id="system-status" class="space-y-3">
|
| 929 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 930 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 931 |
+
<div class="h-14 animate-pulse rounded-2xl bg-white/5"></div>
|
| 932 |
+
</div>
|
| 933 |
+
</section>
|
| 934 |
+
|
| 935 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 936 |
+
<div class="mb-4 flex items-center justify-between">
|
| 937 |
+
<h2 class="text-lg font-bold">No-key integration map</h2>
|
| 938 |
+
<span class="rounded-full border border-cyan/25 bg-cyan/10 px-3 py-1 text-xs text-cyan">public + local</span>
|
| 939 |
+
</div>
|
| 940 |
+
<div id="integration-map" class="grid gap-3 sm:grid-cols-2"></div>
|
| 941 |
+
</section>
|
| 942 |
+
</div>
|
| 943 |
+
|
| 944 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 945 |
+
<div class="mb-4 flex items-center justify-between">
|
| 946 |
+
<h2 class="text-lg font-bold">Market and protocol telemetry</h2>
|
| 947 |
+
<span class="text-xs text-slate-500">values are persisted or fetched live</span>
|
| 948 |
+
</div>
|
| 949 |
+
<div class="grid gap-3 md:grid-cols-3">
|
| 950 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 951 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">24h volume</p>
|
| 952 |
+
<p id="volume-24h" class="mono mt-2 text-2xl font-bold">$0</p>
|
| 953 |
+
</div>
|
| 954 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 955 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Funding avg</p>
|
| 956 |
+
<p id="funding-rate" class="mono mt-2 text-2xl font-bold">0%</p>
|
| 957 |
+
</div>
|
| 958 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 959 |
+
<p class="text-xs uppercase tracking-[0.18em] text-slate-500">Insurance fund</p>
|
| 960 |
+
<p id="insurance-fund" class="mono mt-2 text-2xl font-bold">$0</p>
|
| 961 |
+
</div>
|
| 962 |
+
</div>
|
| 963 |
+
</section>
|
| 964 |
</div>
|
| 965 |
+
|
| 966 |
+
<aside class="space-y-5">
|
| 967 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 968 |
+
<h2 class="text-lg font-bold">Backend contract</h2>
|
| 969 |
+
<div class="mt-4 space-y-3 text-sm text-slate-300">
|
| 970 |
+
<div class="rounded-2xl border border-mint/20 bg-mint/10 p-4">
|
| 971 |
+
<div class="font-semibold text-mint">No API keys required</div>
|
| 972 |
+
<p class="mt-1 text-xs text-slate-300">Public feeds and local ledgers are used by default. Optional endpoints are clearly marked local-only until connected.</p>
|
| 973 |
+
</div>
|
| 974 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-4">
|
| 975 |
+
<div class="font-semibold text-white">No mock success states</div>
|
| 976 |
+
<p class="mt-1 text-xs text-slate-400">If a source is unavailable, the app reports waiting, local-only, or error states instead of inventing values.</p>
|
| 977 |
+
</div>
|
| 978 |
+
</div>
|
| 979 |
+
</section>
|
| 980 |
+
|
| 981 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 982 |
+
<h2 class="text-lg font-bold">Data provenance</h2>
|
| 983 |
+
<div class="mt-4 space-y-3 text-sm" id="provenance-list">
|
| 984 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3 text-slate-400">Loading provenance...</div>
|
| 985 |
+
</div>
|
| 986 |
+
</section>
|
| 987 |
+
|
| 988 |
+
<section class="glass rounded-3xl border border-white/10 p-5">
|
| 989 |
+
<h2 class="text-lg font-bold">Operator notes</h2>
|
| 990 |
+
<ul class="mt-4 space-y-2 text-sm text-slate-300">
|
| 991 |
+
<li>• Holder and slippage routes use a public default token mint.</li>
|
| 992 |
+
<li>• LLM liquidity activates only after a real endpoint responds.</li>
|
| 993 |
+
<li>• Existing local ledgers remain empty until real events occur.</li>
|
| 994 |
+
</ul>
|
| 995 |
+
</section>
|
| 996 |
+
</aside>
|
| 997 |
+
</main>
|
| 998 |
</div>
|
| 999 |
|
| 1000 |
<script>
|
| 1001 |
+
function formatNumber(value, prefix = '') {
|
| 1002 |
+
if (value === undefined || value === null || Number.isNaN(Number(value))) return prefix + '0';
|
| 1003 |
+
return prefix + Number(value).toLocaleString(undefined, { maximumFractionDigits: 2 });
|
| 1004 |
+
}
|
| 1005 |
+
|
| 1006 |
function formatValue(value, prefix = '') {
|
| 1007 |
+
if (value === undefined || value === null) return prefix + '0';
|
| 1008 |
+
if (typeof value === 'number') return formatNumber(value, prefix);
|
| 1009 |
return String(value);
|
| 1010 |
}
|
| 1011 |
+
|
| 1012 |
+
function statusClasses(status) {
|
| 1013 |
+
if (status === 'active') return 'border-mint/30 bg-mint/10 text-mint';
|
| 1014 |
+
if (status === 'local_only') return 'border-cyan/30 bg-cyan/10 text-cyan';
|
| 1015 |
+
if (status === 'pending' || status === 'not_wired') return 'border-amber/30 bg-amber/10 text-amber';
|
| 1016 |
+
return 'border-red-400/30 bg-red-400/10 text-red-300';
|
| 1017 |
+
}
|
| 1018 |
+
|
| 1019 |
+
function statusDot(status) {
|
| 1020 |
+
if (status === 'active') return 'bg-mint';
|
| 1021 |
+
if (status === 'local_only') return 'bg-cyan';
|
| 1022 |
+
if (status === 'pending' || status === 'not_wired') return 'bg-amber';
|
| 1023 |
+
return 'bg-red-300';
|
| 1024 |
+
}
|
| 1025 |
|
| 1026 |
async function loadData() {
|
| 1027 |
try {
|
| 1028 |
const response = await fetch('/api/overview');
|
| 1029 |
+
if (!response.ok) throw new Error('overview failed: ' + response.status);
|
| 1030 |
const data = await response.json();
|
| 1031 |
|
| 1032 |
const holders = data.holders || {};
|
| 1033 |
const liquidity = data.liquidity || {};
|
| 1034 |
const trading = data.trading || {};
|
| 1035 |
+
const funding = data.funding || {};
|
| 1036 |
+
const liquidation = data.liquidation || {};
|
| 1037 |
+
const statusMeta = data.status_meta || {};
|
| 1038 |
+
const config = data.config || {};
|
| 1039 |
+
const integrations = config || {};
|
| 1040 |
|
| 1041 |
document.getElementById('total-holders').textContent = formatValue(holders.total_holders);
|
| 1042 |
document.getElementById('total-providers').textContent = formatValue(liquidity.total_providers);
|
| 1043 |
+
document.getElementById('total-liquidity').textContent = formatNumber(liquidity.total_liquidity_usd, '$');
|
| 1044 |
document.getElementById('active-positions').textContent = formatValue(trading.active_positions);
|
| 1045 |
+
document.getElementById('volume-24h').textContent = formatNumber(trading.total_volume, '$');
|
| 1046 |
+
document.getElementById('funding-rate').textContent = ((funding.current_rate_percent || 0).toFixed(4)) + '%';
|
| 1047 |
+
document.getElementById('insurance-fund').textContent = formatNumber(liquidation.insurance_fund, '$');
|
| 1048 |
+
document.getElementById('runtime-mode').textContent = data.mode || 'no_mock_real_backend';
|
| 1049 |
+
document.getElementById('last-updated').textContent = data.timestamp ? new Date(data.timestamp).toLocaleTimeString() : 'live';
|
| 1050 |
+
document.getElementById('holder-subtitle').textContent = holders.default_token ? 'default public token mint' : 'configured token mint';
|
| 1051 |
|
| 1052 |
+
const statusHtml = Object.entries(statusMeta).map(([key, meta]) => {
|
| 1053 |
+
const status = meta.status || 'pending';
|
| 1054 |
+
return `
|
| 1055 |
+
<div class="rounded-2xl border ${statusClasses(status)} p-4">
|
| 1056 |
+
<div class="flex items-start justify-between gap-3">
|
| 1057 |
+
<div>
|
| 1058 |
+
<div class="flex items-center gap-2 font-semibold">
|
| 1059 |
+
<span class="h-2 w-2 rounded-full ${statusDot(status)}"></span>
|
| 1060 |
+
${meta.label || key}
|
| 1061 |
+
</div>
|
| 1062 |
+
<p class="mt-1 text-xs leading-5 text-slate-300">${meta.detail || ''}</p>
|
| 1063 |
+
</div>
|
| 1064 |
+
<span class="rounded-full bg-black/30 px-2.5 py-1 text-[10px] uppercase tracking-[0.18em]">${meta.display || status}</span>
|
| 1065 |
+
</div>
|
| 1066 |
+
</div>`;
|
| 1067 |
}).join('');
|
| 1068 |
+
document.getElementById('system-status').innerHTML = statusHtml || '<p class="text-slate-400">No system status returned.</p>';
|
| 1069 |
+
|
| 1070 |
+
const integrationHtml = Object.entries(integrations).map(([key, item]) => {
|
| 1071 |
+
const status = item.status || 'pending';
|
| 1072 |
+
return `
|
| 1073 |
+
<div class="rounded-2xl border ${statusClasses(status)} p-4">
|
| 1074 |
+
<div class="text-xs uppercase tracking-[0.18em] opacity-80">${item.label || key}</div>
|
| 1075 |
+
<div class="mt-2 text-lg font-bold">${(item.status || '').replace('_', ' ')}</div>
|
| 1076 |
+
<div class="mt-2 text-xs text-slate-300">${item.env || 'no key needed'}</div>
|
| 1077 |
+
${item.value_public ? `<div class="mono mt-2 truncate text-[11px] text-slate-400">${item.value_public}</div>` : ''}
|
| 1078 |
+
</div>`;
|
| 1079 |
+
}).join('');
|
| 1080 |
+
document.getElementById('integration-map').innerHTML = integrationHtml;
|
| 1081 |
+
|
| 1082 |
+
document.getElementById('provenance-list').innerHTML = `
|
| 1083 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1084 |
+
<div class="font-semibold text-white">Market data</div>
|
| 1085 |
+
<div class="mt-1 text-xs text-slate-400">Gate.io public futures endpoints; no API key.</div>
|
| 1086 |
+
</div>
|
| 1087 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1088 |
+
<div class="font-semibold text-white">Token graph</div>
|
| 1089 |
+
<div class="mt-1 text-xs text-slate-400">Solana RPC and DexScreener public routes using the displayed token mint.</div>
|
| 1090 |
+
</div>
|
| 1091 |
+
<div class="rounded-2xl border border-white/10 bg-black/20 p-3">
|
| 1092 |
+
<div class="font-semibold text-white">Local ledgers</div>
|
| 1093 |
+
<div class="mt-1 text-xs text-slate-400">SQLite stores inside the running Space container.</div>
|
| 1094 |
+
</div>`;
|
| 1095 |
} catch (error) {
|
| 1096 |
console.error('Failed to load data:', error);
|
| 1097 |
+
document.getElementById('system-status').innerHTML = '<div class="rounded-2xl border border-red-400/30 bg-red-400/10 p-4 text-red-200">Backend API did not respond. This is a real error, not a simulated state.</div>';
|
| 1098 |
}
|
| 1099 |
}
|
| 1100 |
|
audit_integration.py
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
AirMicroDrip Audit Integration
|
| 4 |
+
Connects the audit framework to the AirMicroDrip perpetual futures system.
|
| 5 |
+
Provides continuous monitoring, health checks, and compliance verification.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import Dict, Any, Optional
|
| 12 |
+
|
| 13 |
+
# Add parent directory to path for audit_framework import
|
| 14 |
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
| 15 |
+
|
| 16 |
+
from audit_framework import (
|
| 17 |
+
AuditFramework,
|
| 18 |
+
AuditCategory,
|
| 19 |
+
AuditSeverity,
|
| 20 |
+
AuditStatus,
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class AirMicroDripAuditor:
|
| 25 |
+
"""Audit wrapper for AirMicroDrip systems."""
|
| 26 |
+
|
| 27 |
+
def __init__(self, db_path: str = "airmicrodrip_audit.db"):
|
| 28 |
+
self.audit = AuditFramework()
|
| 29 |
+
self.db_path = db_path
|
| 30 |
+
self._register_airmicrodrip_checks()
|
| 31 |
+
|
| 32 |
+
def _register_airmicrodrip_checks(self):
|
| 33 |
+
"""Register AirMicroDrip-specific audit checks."""
|
| 34 |
+
from audit_framework import AuditCheck
|
| 35 |
+
|
| 36 |
+
extra_checks = [
|
| 37 |
+
AuditCheck(
|
| 38 |
+
check_id="amd_001",
|
| 39 |
+
name="Liquidity Provider Health",
|
| 40 |
+
description="Verify at least one active LLM inference provider",
|
| 41 |
+
category=AuditCategory.AVAILABILITY,
|
| 42 |
+
severity=AuditSeverity.HIGH,
|
| 43 |
+
),
|
| 44 |
+
AuditCheck(
|
| 45 |
+
check_id="amd_002",
|
| 46 |
+
name="Synthetic Liquidity Depth",
|
| 47 |
+
description="Verify total synthetic liquidity exceeds minimum threshold",
|
| 48 |
+
category=AuditCategory.ACCURACY,
|
| 49 |
+
severity=AuditSeverity.HIGH,
|
| 50 |
+
),
|
| 51 |
+
AuditCheck(
|
| 52 |
+
check_id="amd_003",
|
| 53 |
+
name="Perpetual Engine Consistency",
|
| 54 |
+
description="Verify mark prices and index prices are within tolerance",
|
| 55 |
+
category=AuditCategory.ACCURACY,
|
| 56 |
+
severity=AuditSeverity.CRITICAL,
|
| 57 |
+
),
|
| 58 |
+
AuditCheck(
|
| 59 |
+
check_id="amd_004",
|
| 60 |
+
name="Funding Rate Bounds",
|
| 61 |
+
description="Verify funding rates are within configured min/max",
|
| 62 |
+
category=AuditCategory.ACCURACY,
|
| 63 |
+
severity=AuditSeverity.MEDIUM,
|
| 64 |
+
),
|
| 65 |
+
AuditCheck(
|
| 66 |
+
check_id="amd_005",
|
| 67 |
+
name="Liquidation Backlog",
|
| 68 |
+
description="Verify no positions are stuck in liquidation queue",
|
| 69 |
+
category=AuditCategory.INTEGRITY,
|
| 70 |
+
severity=AuditSeverity.CRITICAL,
|
| 71 |
+
),
|
| 72 |
+
AuditCheck(
|
| 73 |
+
check_id="amd_006",
|
| 74 |
+
name="Order Book Spread",
|
| 75 |
+
description="Verify bid-ask spread is within acceptable range",
|
| 76 |
+
category=AuditCategory.PERFORMANCE,
|
| 77 |
+
severity=AuditSeverity.MEDIUM,
|
| 78 |
+
),
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
for check in extra_checks:
|
| 82 |
+
self.audit.checks[check.check_id] = check
|
| 83 |
+
|
| 84 |
+
def check_liquidity_providers(self, registry) -> Dict[str, Any]:
|
| 85 |
+
"""Run liquidity provider health check."""
|
| 86 |
+
providers = registry.get_all_providers(status="active")
|
| 87 |
+
if not providers:
|
| 88 |
+
return {
|
| 89 |
+
"status": AuditStatus.FAILED,
|
| 90 |
+
"message": "No active liquidity providers",
|
| 91 |
+
"details": {"active_count": 0},
|
| 92 |
+
}
|
| 93 |
+
return {
|
| 94 |
+
"status": AuditStatus.PASSED,
|
| 95 |
+
"message": f"{len(providers)} active liquidity providers",
|
| 96 |
+
"details": {"active_count": len(providers)},
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
def check_liquidity_depth(self, converter) -> Dict[str, Any]:
|
| 100 |
+
"""Run synthetic liquidity depth check."""
|
| 101 |
+
total = converter.get_total_liquidity()
|
| 102 |
+
total_usd = total.get("total_usd", 0.0)
|
| 103 |
+
min_liquidity = float(os.environ.get("MIN_LIQUIDITY_USD", 10000.0))
|
| 104 |
+
|
| 105 |
+
if total_usd < min_liquidity:
|
| 106 |
+
return {
|
| 107 |
+
"status": AuditStatus.FAILED,
|
| 108 |
+
"message": f"Total liquidity ${total_usd:.2f} below minimum ${min_liquidity:.2f}",
|
| 109 |
+
"details": {"total_usd": total_usd, "minimum": min_liquidity},
|
| 110 |
+
}
|
| 111 |
+
return {
|
| 112 |
+
"status": AuditStatus.PASSED,
|
| 113 |
+
"message": f"Total liquidity ${total_usd:.2f} above minimum",
|
| 114 |
+
"details": {"total_usd": total_usd, "by_market": total.get("by_market", {})},
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
def check_mark_price_consistency(self, trading_engine, tolerance: float = 0.02) -> Dict[str, Any]:
|
| 118 |
+
"""Verify mark prices are close to index prices."""
|
| 119 |
+
inconsistent = []
|
| 120 |
+
for market, state in trading_engine.market_states.items():
|
| 121 |
+
if state.index_price == 0:
|
| 122 |
+
continue
|
| 123 |
+
deviation = abs(state.mark_price - state.index_price) / state.index_price
|
| 124 |
+
if deviation > tolerance:
|
| 125 |
+
inconsistent.append({
|
| 126 |
+
"market": market,
|
| 127 |
+
"mark": state.mark_price,
|
| 128 |
+
"index": state.index_price,
|
| 129 |
+
"deviation": deviation,
|
| 130 |
+
})
|
| 131 |
+
|
| 132 |
+
if inconsistent:
|
| 133 |
+
return {
|
| 134 |
+
"status": AuditStatus.FAILED,
|
| 135 |
+
"message": f"{len(inconsistent)} market(s) with price deviation > {tolerance:.1%}",
|
| 136 |
+
"details": {"inconsistent": inconsistent},
|
| 137 |
+
}
|
| 138 |
+
return {
|
| 139 |
+
"status": AuditStatus.PASSED,
|
| 140 |
+
"message": "Mark prices consistent with index prices",
|
| 141 |
+
"details": {"markets_checked": len(trading_engine.market_states)},
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
def check_funding_rate_bounds(self, funding_engine) -> Dict[str, Any]:
|
| 145 |
+
"""Verify funding rates within bounds."""
|
| 146 |
+
from funding_rate_engine import FUNDING_CONFIG
|
| 147 |
+
|
| 148 |
+
out_of_bounds = []
|
| 149 |
+
for market in funding_engine.trading_engine.market_states:
|
| 150 |
+
rate = funding_engine.calculate_funding_rate(market)
|
| 151 |
+
if rate < FUNDING_CONFIG["min_funding_rate"] or rate > FUNDING_CONFIG["max_funding_rate"]:
|
| 152 |
+
out_of_bounds.append({"market": market, "rate": rate})
|
| 153 |
+
|
| 154 |
+
if out_of_bounds:
|
| 155 |
+
return {
|
| 156 |
+
"status": AuditStatus.FAILED,
|
| 157 |
+
"message": f"{len(out_of_bounds)} funding rate(s) out of bounds",
|
| 158 |
+
"details": {"out_of_bounds": out_of_bounds},
|
| 159 |
+
}
|
| 160 |
+
return {
|
| 161 |
+
"status": AuditStatus.PASSED,
|
| 162 |
+
"message": "All funding rates within bounds",
|
| 163 |
+
"details": {"markets_checked": len(funding_engine.trading_engine.market_states)},
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
def check_liquidation_backlog(self, liq_system) -> Dict[str, Any]:
|
| 167 |
+
"""Check for stuck liquidations."""
|
| 168 |
+
at_risk = liq_system.get_at_risk_positions()
|
| 169 |
+
if len(at_risk) > 10:
|
| 170 |
+
return {
|
| 171 |
+
"status": AuditStatus.WARNING,
|
| 172 |
+
"message": f"{len(at_risk)} positions at risk — possible backlog",
|
| 173 |
+
"details": {"at_risk_count": len(at_risk)},
|
| 174 |
+
}
|
| 175 |
+
return {
|
| 176 |
+
"status": AuditStatus.PASSED,
|
| 177 |
+
"message": f"Liquidation queue healthy ({len(at_risk)} at risk)",
|
| 178 |
+
"details": {"at_risk_count": len(at_risk)},
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
def check_orderbook_spread(self, trading_engine, max_spread_bps: float = 50.0) -> Dict[str, Any]:
|
| 182 |
+
"""Verify bid-ask spreads are within tolerance."""
|
| 183 |
+
wide_spreads = []
|
| 184 |
+
for market, ob in trading_engine.order_books.items():
|
| 185 |
+
best_bid = ob.get_best_bid()
|
| 186 |
+
best_ask = ob.get_best_ask()
|
| 187 |
+
if best_bid and best_ask and best_bid > 0:
|
| 188 |
+
spread_bps = ((best_ask - best_bid) / best_bid) * 10000
|
| 189 |
+
if spread_bps > max_spread_bps:
|
| 190 |
+
wide_spreads.append({"market": market, "spread_bps": spread_bps})
|
| 191 |
+
|
| 192 |
+
if wide_spreads:
|
| 193 |
+
return {
|
| 194 |
+
"status": AuditStatus.WARNING,
|
| 195 |
+
"message": f"{len(wide_spreads)} market(s) with wide spread",
|
| 196 |
+
"details": {"wide_spreads": wide_spreads},
|
| 197 |
+
}
|
| 198 |
+
return {
|
| 199 |
+
"status": AuditStatus.PASSED,
|
| 200 |
+
"message": "Order book spreads within tolerance",
|
| 201 |
+
"details": {"markets_checked": len(trading_engine.order_books)},
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
def run_airmicrodrip_audit(
|
| 205 |
+
self,
|
| 206 |
+
registry=None,
|
| 207 |
+
converter=None,
|
| 208 |
+
trading_engine=None,
|
| 209 |
+
funding_engine=None,
|
| 210 |
+
liq_system=None,
|
| 211 |
+
) -> Dict[str, Any]:
|
| 212 |
+
"""Run the full AirMicroDrip audit suite."""
|
| 213 |
+
ctx: Dict[str, Any] = {}
|
| 214 |
+
|
| 215 |
+
if registry:
|
| 216 |
+
ctx["liquidity_providers"] = self.check_liquidity_providers(registry)
|
| 217 |
+
if converter:
|
| 218 |
+
ctx["liquidity_depth"] = self.check_liquidity_depth(converter)
|
| 219 |
+
if trading_engine:
|
| 220 |
+
ctx["price_consistency"] = self.check_mark_price_consistency(trading_engine)
|
| 221 |
+
if funding_engine:
|
| 222 |
+
ctx["funding_bounds"] = self.check_funding_rate_bounds(funding_engine)
|
| 223 |
+
if liq_system:
|
| 224 |
+
ctx["liquidation_backlog"] = self.check_liquidation_backlog(liq_system)
|
| 225 |
+
if trading_engine:
|
| 226 |
+
ctx["orderbook_spread"] = self.check_orderbook_spread(trading_engine)
|
| 227 |
+
|
| 228 |
+
# Log all results
|
| 229 |
+
for check_name, result in ctx.items():
|
| 230 |
+
status = result.get("status", AuditStatus.SKIPPED)
|
| 231 |
+
self.audit.log(
|
| 232 |
+
category=AuditCategory.INTEGRITY,
|
| 233 |
+
severity=AuditSeverity.HIGH if status == AuditStatus.FAILED else AuditSeverity.INFO,
|
| 234 |
+
status=status,
|
| 235 |
+
message=result.get("message", f"{check_name} check completed"),
|
| 236 |
+
details=result.get("details", {}),
|
| 237 |
+
actor="airmicrodrip_auditor",
|
| 238 |
+
component=check_name,
|
| 239 |
+
)
|
| 240 |
+
|
| 241 |
+
# Run base framework checks too
|
| 242 |
+
base_report = self.audit.run_audit(context=ctx)
|
| 243 |
+
|
| 244 |
+
return {
|
| 245 |
+
"base_report_id": base_report.report_id,
|
| 246 |
+
"overall_score": base_report.overall_score,
|
| 247 |
+
"airmicrodrip_checks": ctx,
|
| 248 |
+
"system_health": self.audit.get_system_health(),
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
|
| 252 |
+
if __name__ == "__main__":
|
| 253 |
+
# Standalone demo
|
| 254 |
+
auditor = AirMicroDripAuditor()
|
| 255 |
+
print("AirMicroDrip Auditor initialized with checks:")
|
| 256 |
+
for cid, check in auditor.audit.checks.items():
|
| 257 |
+
print(f" {cid}: {check.name} ({check.category.value}, {check.severity.value})")
|
| 258 |
+
print(f"\nTotal checks registered: {len(auditor.audit.checks)}")
|
funding_rate_engine.py
CHANGED
|
@@ -40,6 +40,8 @@ class FundingRateEngine:
|
|
| 40 |
market_state = self.trading_engine.market_states[market]
|
| 41 |
|
| 42 |
# Calculate premium
|
|
|
|
|
|
|
| 43 |
premium = (market_state.mark_price - market_state.index_price) / market_state.index_price
|
| 44 |
|
| 45 |
# Calculate interest rate component (hourly)
|
|
|
|
| 40 |
market_state = self.trading_engine.market_states[market]
|
| 41 |
|
| 42 |
# Calculate premium
|
| 43 |
+
if market_state.index_price == 0:
|
| 44 |
+
return 0.0
|
| 45 |
premium = (market_state.mark_price - market_state.index_price) / market_state.index_price
|
| 46 |
|
| 47 |
# Calculate interest rate component (hourly)
|
holder_tracker.py
CHANGED
|
@@ -12,7 +12,7 @@ import requests
|
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
|
| 15 |
-
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.
|
| 16 |
TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
| 17 |
|
| 18 |
# Configuration
|
|
|
|
| 12 |
from typing import Dict, List, Optional
|
| 13 |
from datetime import datetime, timedelta
|
| 14 |
|
| 15 |
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
|
| 16 |
TOKEN_PROGRAM_ID = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"
|
| 17 |
|
| 18 |
# Configuration
|
liquidation_system.py
CHANGED
|
@@ -112,6 +112,8 @@ class LiquidationSystem:
|
|
| 112 |
|
| 113 |
# Calculate margin ratio
|
| 114 |
position_value = position.size * current_price
|
|
|
|
|
|
|
| 115 |
margin_ratio = position.margin / position_value
|
| 116 |
|
| 117 |
# Check if below maintenance margin
|
|
@@ -235,6 +237,8 @@ class LiquidationSystem:
|
|
| 235 |
market_state = self.trading_engine.market_states[position.market]
|
| 236 |
current_price = market_state.mark_price
|
| 237 |
position_value = position.size * current_price
|
|
|
|
|
|
|
| 238 |
margin_ratio = position.margin / position_value
|
| 239 |
|
| 240 |
# Check if at risk (within 20% of liquidation)
|
|
|
|
| 112 |
|
| 113 |
# Calculate margin ratio
|
| 114 |
position_value = position.size * current_price
|
| 115 |
+
if position_value == 0:
|
| 116 |
+
return False
|
| 117 |
margin_ratio = position.margin / position_value
|
| 118 |
|
| 119 |
# Check if below maintenance margin
|
|
|
|
| 237 |
market_state = self.trading_engine.market_states[position.market]
|
| 238 |
current_price = market_state.mark_price
|
| 239 |
position_value = position.size * current_price
|
| 240 |
+
if position_value == 0 or current_price == 0:
|
| 241 |
+
continue
|
| 242 |
margin_ratio = position.margin / position_value
|
| 243 |
|
| 244 |
# Check if at risk (within 20% of liquidation)
|
llm_liquidity_provider.py
CHANGED
|
@@ -203,18 +203,25 @@ class InferenceRegistry:
|
|
| 203 |
"""Get latest capacity metrics for provider"""
|
| 204 |
conn = sqlite3.connect(self.db_path)
|
| 205 |
cursor = conn.cursor()
|
| 206 |
-
|
| 207 |
cursor.execute("""
|
| 208 |
-
SELECT
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 212 |
LIMIT 1
|
| 213 |
""", (provider_id,))
|
| 214 |
-
|
| 215 |
result = cursor.fetchone()
|
| 216 |
conn.close()
|
| 217 |
-
|
| 218 |
if result:
|
| 219 |
return InferenceMetrics(
|
| 220 |
tokens_per_second=result[0],
|
|
@@ -224,8 +231,135 @@ class InferenceRegistry:
|
|
| 224 |
quality_score=result[4],
|
| 225 |
last_verified=datetime.fromisoformat(result[5]),
|
| 226 |
)
|
| 227 |
-
|
| 228 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 229 |
|
| 230 |
def get_all_providers(self, status: Optional[str] = None) -> List[Dict]:
|
| 231 |
"""Get all providers, optionally filtered by status"""
|
|
@@ -468,27 +602,38 @@ class PerformanceMonitor:
|
|
| 468 |
"""Check individual provider performance from real registry data"""
|
| 469 |
current_capacity = self.registry.get_provider_capacity(provider_id)
|
| 470 |
|
| 471 |
-
if current_capacity:
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
def stop_monitoring(self):
|
| 494 |
"""Stop performance monitoring"""
|
|
|
|
| 203 |
"""Get latest capacity metrics for provider"""
|
| 204 |
conn = sqlite3.connect(self.db_path)
|
| 205 |
cursor = conn.cursor()
|
| 206 |
+
|
| 207 |
cursor.execute("""
|
| 208 |
+
SELECT
|
| 209 |
+
c.tokens_per_second,
|
| 210 |
+
p.model_type,
|
| 211 |
+
c.latency_ms,
|
| 212 |
+
c.uptime_percentage,
|
| 213 |
+
c.quality_score,
|
| 214 |
+
c.verified_at
|
| 215 |
+
FROM capacity c
|
| 216 |
+
JOIN providers p ON c.provider_id = p.provider_id
|
| 217 |
+
WHERE c.provider_id = ?
|
| 218 |
+
ORDER BY c.verified_at DESC
|
| 219 |
LIMIT 1
|
| 220 |
""", (provider_id,))
|
| 221 |
+
|
| 222 |
result = cursor.fetchone()
|
| 223 |
conn.close()
|
| 224 |
+
|
| 225 |
if result:
|
| 226 |
return InferenceMetrics(
|
| 227 |
tokens_per_second=result[0],
|
|
|
|
| 231 |
quality_score=result[4],
|
| 232 |
last_verified=datetime.fromisoformat(result[5]),
|
| 233 |
)
|
| 234 |
+
|
| 235 |
return None
|
| 236 |
+
|
| 237 |
+
def update_reputation(self, provider_id: str, delta: float) -> Dict[str, any]:
|
| 238 |
+
"""Adjust provider reputation score (clamped 0.0-1.0)."""
|
| 239 |
+
conn = sqlite3.connect(self.db_path)
|
| 240 |
+
cursor = conn.cursor()
|
| 241 |
+
|
| 242 |
+
cursor.execute("SELECT reputation_score FROM providers WHERE provider_id = ?", (provider_id,))
|
| 243 |
+
row = cursor.fetchone()
|
| 244 |
+
if not row:
|
| 245 |
+
conn.close()
|
| 246 |
+
return {"status": "error", "message": "Provider not found"}
|
| 247 |
+
|
| 248 |
+
new_score = max(0.0, min(1.0, row[0] + delta))
|
| 249 |
+
cursor.execute(
|
| 250 |
+
"UPDATE providers SET reputation_score = ? WHERE provider_id = ?",
|
| 251 |
+
(new_score, provider_id),
|
| 252 |
+
)
|
| 253 |
+
conn.commit()
|
| 254 |
+
conn.close()
|
| 255 |
+
return {"status": "ok", "provider_id": provider_id, "new_score": new_score}
|
| 256 |
+
|
| 257 |
+
def record_earnings(self, provider_id: str, amount: float, source: str = "fees") -> Dict[str, any]:
|
| 258 |
+
"""Record earnings and update provider total."""
|
| 259 |
+
conn = sqlite3.connect(self.db_path)
|
| 260 |
+
cursor = conn.cursor()
|
| 261 |
+
current_time = datetime.utcnow().isoformat()
|
| 262 |
+
|
| 263 |
+
cursor.execute("""
|
| 264 |
+
INSERT INTO earnings (provider_id, amount, source, timestamp)
|
| 265 |
+
VALUES (?, ?, ?, ?)
|
| 266 |
+
""", (provider_id, amount, source, current_time))
|
| 267 |
+
|
| 268 |
+
cursor.execute("""
|
| 269 |
+
UPDATE providers SET total_earnings = total_earnings + ?
|
| 270 |
+
WHERE provider_id = ?
|
| 271 |
+
""", (amount, provider_id))
|
| 272 |
+
|
| 273 |
+
conn.commit()
|
| 274 |
+
conn.close()
|
| 275 |
+
return {"status": "ok", "provider_id": provider_id, "amount": amount, "source": source}
|
| 276 |
+
|
| 277 |
+
def get_provider_earnings(self, provider_id: str, limit: int = 100) -> List[Dict]:
|
| 278 |
+
"""Get earnings history for a provider."""
|
| 279 |
+
conn = sqlite3.connect(self.db_path)
|
| 280 |
+
cursor = conn.cursor()
|
| 281 |
+
|
| 282 |
+
cursor.execute("""
|
| 283 |
+
SELECT amount, source, timestamp
|
| 284 |
+
FROM earnings
|
| 285 |
+
WHERE provider_id = ?
|
| 286 |
+
ORDER BY timestamp DESC
|
| 287 |
+
LIMIT ?
|
| 288 |
+
""", (provider_id, limit))
|
| 289 |
+
|
| 290 |
+
rows = cursor.fetchall()
|
| 291 |
+
conn.close()
|
| 292 |
+
return [
|
| 293 |
+
{"amount": r[0], "source": r[1], "timestamp": r[2]}
|
| 294 |
+
for r in rows
|
| 295 |
+
]
|
| 296 |
+
|
| 297 |
+
def deactivate_provider(self, provider_id: str, reason: str = "") -> Dict[str, any]:
|
| 298 |
+
"""Deactivate a provider (slashing / offboarding)."""
|
| 299 |
+
conn = sqlite3.connect(self.db_path)
|
| 300 |
+
cursor = conn.cursor()
|
| 301 |
+
|
| 302 |
+
cursor.execute("""
|
| 303 |
+
UPDATE providers SET status = 'inactive' WHERE provider_id = ?
|
| 304 |
+
""", (provider_id,))
|
| 305 |
+
|
| 306 |
+
changed = cursor.rowcount
|
| 307 |
+
conn.commit()
|
| 308 |
+
conn.close()
|
| 309 |
+
|
| 310 |
+
if changed == 0:
|
| 311 |
+
return {"status": "error", "message": "Provider not found"}
|
| 312 |
+
return {
|
| 313 |
+
"status": "ok",
|
| 314 |
+
"provider_id": provider_id,
|
| 315 |
+
"new_status": "inactive",
|
| 316 |
+
"reason": reason,
|
| 317 |
+
}
|
| 318 |
+
|
| 319 |
+
def get_provider_stats(self, provider_id: str) -> Optional[Dict[str, any]]:
|
| 320 |
+
"""Get combined provider stats (profile + latest capacity + earnings)."""
|
| 321 |
+
conn = sqlite3.connect(self.db_path)
|
| 322 |
+
cursor = conn.cursor()
|
| 323 |
+
|
| 324 |
+
cursor.execute("""
|
| 325 |
+
SELECT provider_id, wallet_address, model_type, registered_at, status,
|
| 326 |
+
reputation_score, total_earnings
|
| 327 |
+
FROM providers WHERE provider_id = ?
|
| 328 |
+
""", (provider_id,))
|
| 329 |
+
p = cursor.fetchone()
|
| 330 |
+
if not p:
|
| 331 |
+
conn.close()
|
| 332 |
+
return None
|
| 333 |
+
|
| 334 |
+
cursor.execute("""
|
| 335 |
+
SELECT tokens_per_second, latency_ms, uptime_percentage, quality_score, verified_at
|
| 336 |
+
FROM capacity WHERE provider_id = ? ORDER BY verified_at DESC LIMIT 1
|
| 337 |
+
""", (provider_id,))
|
| 338 |
+
c = cursor.fetchone()
|
| 339 |
+
|
| 340 |
+
cursor.execute("""
|
| 341 |
+
SELECT COALESCE(SUM(amount), 0) FROM earnings WHERE provider_id = ?
|
| 342 |
+
""", (provider_id,))
|
| 343 |
+
total_earned = cursor.fetchone()[0]
|
| 344 |
+
|
| 345 |
+
conn.close()
|
| 346 |
+
|
| 347 |
+
return {
|
| 348 |
+
"provider_id": p[0],
|
| 349 |
+
"wallet_address": p[1],
|
| 350 |
+
"model_type": p[2],
|
| 351 |
+
"registered_at": p[3],
|
| 352 |
+
"status": p[4],
|
| 353 |
+
"reputation_score": p[5],
|
| 354 |
+
"total_earnings": total_earned,
|
| 355 |
+
"latest_capacity": {
|
| 356 |
+
"tokens_per_second": c[0],
|
| 357 |
+
"latency_ms": c[1],
|
| 358 |
+
"uptime_percentage": c[2],
|
| 359 |
+
"quality_score": c[3],
|
| 360 |
+
"verified_at": c[4],
|
| 361 |
+
} if c else None,
|
| 362 |
+
}
|
| 363 |
|
| 364 |
def get_all_providers(self, status: Optional[str] = None) -> List[Dict]:
|
| 365 |
"""Get all providers, optionally filtered by status"""
|
|
|
|
| 602 |
"""Check individual provider performance from real registry data"""
|
| 603 |
current_capacity = self.registry.get_provider_capacity(provider_id)
|
| 604 |
|
| 605 |
+
if not current_capacity:
|
| 606 |
+
return
|
| 607 |
+
|
| 608 |
+
age = datetime.utcnow() - current_capacity.last_verified
|
| 609 |
+
|
| 610 |
+
if age > timedelta(hours=1):
|
| 611 |
+
endpoint = os.environ.get("INFERENCE_API_URL", "http://localhost:11434")
|
| 612 |
+
model = current_capacity.model_type or "llama2"
|
| 613 |
+
print(f"Re-verifying capacity for {provider_id} via {endpoint}")
|
| 614 |
+
|
| 615 |
+
bench = self._benchmark_inference_endpoint(endpoint, model)
|
| 616 |
+
if bench["status"] == "verified":
|
| 617 |
+
self.registry.verify_capacity(
|
| 618 |
+
provider_id,
|
| 619 |
+
bench["tokens_per_second"],
|
| 620 |
+
bench["latency_ms"],
|
| 621 |
+
99.0,
|
| 622 |
+
bench["quality_score"],
|
| 623 |
+
)
|
| 624 |
+
# Small reputation boost for passing re-verification
|
| 625 |
+
self.registry.update_reputation(provider_id, 0.02)
|
| 626 |
+
print(f"Verified: {bench['tokens_per_second']:.1f} tokens/sec, {bench['latency_ms']:.1f}ms")
|
| 627 |
+
else:
|
| 628 |
+
# Penalize and possibly slash
|
| 629 |
+
stats = self.registry.get_provider_stats(provider_id)
|
| 630 |
+
if stats:
|
| 631 |
+
rep = stats.get("reputation_score", 0.5)
|
| 632 |
+
self.registry.update_reputation(provider_id, -0.10)
|
| 633 |
+
print(f"Provider {provider_id} unreachable — reputation slashed to {max(0.0, rep - 0.10):.2f}")
|
| 634 |
+
if rep <= 0.20:
|
| 635 |
+
self.registry.deactivate_provider(provider_id, reason="Repeated benchmark failures")
|
| 636 |
+
print(f"Provider {provider_id} DEACTIVATED due to low reputation")
|
| 637 |
|
| 638 |
def stop_monitoring(self):
|
| 639 |
"""Stop performance monitoring"""
|
llm_orderbook_integration.py
CHANGED
|
@@ -51,7 +51,9 @@ class LLMOrderBookIntegrator:
|
|
| 51 |
|
| 52 |
def _add_synthetic_orders(self, order_book: OrderBook, liquidity_usd: float):
|
| 53 |
"""Add synthetic orders to order book based on liquidity"""
|
| 54 |
-
market_state = self.trading_engine.market_states
|
|
|
|
|
|
|
| 55 |
mark_price = market_state.mark_price
|
| 56 |
|
| 57 |
# Calculate order sizes
|
|
@@ -93,7 +95,7 @@ class LLMOrderBookIntegrator:
|
|
| 93 |
best_bid = order_book.get_best_bid()
|
| 94 |
best_ask = order_book.get_best_ask()
|
| 95 |
|
| 96 |
-
if best_bid and best_ask:
|
| 97 |
imbalance = (best_ask - best_bid) / market_state.mark_price
|
| 98 |
|
| 99 |
# If imbalance is high, add more liquidity
|
|
|
|
| 51 |
|
| 52 |
def _add_synthetic_orders(self, order_book: OrderBook, liquidity_usd: float):
|
| 53 |
"""Add synthetic orders to order book based on liquidity"""
|
| 54 |
+
market_state = self.trading_engine.market_states.get(order_book.market)
|
| 55 |
+
if not market_state or market_state.mark_price == 0:
|
| 56 |
+
return
|
| 57 |
mark_price = market_state.mark_price
|
| 58 |
|
| 59 |
# Calculate order sizes
|
|
|
|
| 95 |
best_bid = order_book.get_best_bid()
|
| 96 |
best_ask = order_book.get_best_ask()
|
| 97 |
|
| 98 |
+
if best_bid and best_ask and market_state.mark_price != 0:
|
| 99 |
imbalance = (best_ask - best_bid) / market_state.mark_price
|
| 100 |
|
| 101 |
# If imbalance is high, add more liquidity
|
perp_trading_engine.py
CHANGED
|
@@ -243,8 +243,9 @@ class PerpTradingEngine:
|
|
| 243 |
prices['ETH/USDC'] = last
|
| 244 |
elif contract == 'SOL_USDT':
|
| 245 |
prices['SOL/USDC'] = last
|
| 246 |
-
except Exception:
|
| 247 |
-
|
|
|
|
| 248 |
# Fallback only if API unreachable
|
| 249 |
if 'BTC/USDC' not in prices:
|
| 250 |
prices['BTC/USDC'] = 50000.0
|
|
|
|
| 243 |
prices['ETH/USDC'] = last
|
| 244 |
elif contract == 'SOL_USDT':
|
| 245 |
prices['SOL/USDC'] = last
|
| 246 |
+
except Exception as e:
|
| 247 |
+
import logging
|
| 248 |
+
logging.warning(f"Price fetch failed: {e}")
|
| 249 |
# Fallback only if API unreachable
|
| 250 |
if 'BTC/USDC' not in prices:
|
| 251 |
prices['BTC/USDC'] = 50000.0
|
slippage_collector.py
CHANGED
|
@@ -21,7 +21,7 @@ SLIPPAGE_CONFIG = {
|
|
| 21 |
|
| 22 |
# Real API endpoints
|
| 23 |
DEXSCREENER_API = "https://api.dexscreener.com/latest/dex/tokens"
|
| 24 |
-
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.
|
| 25 |
|
| 26 |
|
| 27 |
def _fetch_dexscreener_pairs(token_mint: str) -> List[Dict]:
|
|
|
|
| 21 |
|
| 22 |
# Real API endpoints
|
| 23 |
DEXSCREENER_API = "https://api.dexscreener.com/latest/dex/tokens"
|
| 24 |
+
SOLANA_RPC_URL = os.environ.get("SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com")
|
| 25 |
|
| 26 |
|
| 27 |
def _fetch_dexscreener_pairs(token_mint: str) -> List[Dict]:
|
ui/next.config.mjs
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
|
|
|
|
|
| 1 |
/** @type {import('next').NextConfig} */
|
| 2 |
const nextConfig = {
|
| 3 |
output: 'standalone',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
};
|
| 5 |
|
| 6 |
export default nextConfig;
|
|
|
|
| 1 |
+
const API_BASE = process.env.API_BASE_URL || '';
|
| 2 |
+
|
| 3 |
/** @type {import('next').NextConfig} */
|
| 4 |
const nextConfig = {
|
| 5 |
output: 'standalone',
|
| 6 |
+
async rewrites() {
|
| 7 |
+
if (!API_BASE) return [];
|
| 8 |
+
return [
|
| 9 |
+
{
|
| 10 |
+
source: '/api/:path*',
|
| 11 |
+
destination: `${API_BASE}/api/:path*`,
|
| 12 |
+
},
|
| 13 |
+
];
|
| 14 |
+
},
|
| 15 |
};
|
| 16 |
|
| 17 |
export default nextConfig;
|
ui/package.json
CHANGED
|
@@ -15,7 +15,8 @@
|
|
| 15 |
"lucide-react": "^0.344.0",
|
| 16 |
"clsx": "^2.1.0",
|
| 17 |
"tailwind-merge": "^2.2.1",
|
| 18 |
-
"recharts": "^2.12.0"
|
|
|
|
| 19 |
},
|
| 20 |
"devDependencies": {
|
| 21 |
"@types/node": "^20.11.5",
|
|
@@ -28,4 +29,4 @@
|
|
| 28 |
"eslint": "^8.56.0",
|
| 29 |
"eslint-config-next": "14.2.15"
|
| 30 |
}
|
| 31 |
-
}
|
|
|
|
| 15 |
"lucide-react": "^0.344.0",
|
| 16 |
"clsx": "^2.1.0",
|
| 17 |
"tailwind-merge": "^2.2.1",
|
| 18 |
+
"recharts": "^2.12.0",
|
| 19 |
+
"framer-motion": "^11.0.0"
|
| 20 |
},
|
| 21 |
"devDependencies": {
|
| 22 |
"@types/node": "^20.11.5",
|
|
|
|
| 29 |
"eslint": "^8.56.0",
|
| 30 |
"eslint-config-next": "14.2.15"
|
| 31 |
}
|
| 32 |
+
}
|
ui/src/app/components/StatCard.tsx
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { motion } from "framer-motion";
|
| 4 |
+
import { LucideIcon } from "lucide-react";
|
| 5 |
+
|
| 6 |
+
interface StatCardProps {
|
| 7 |
+
title: string;
|
| 8 |
+
value: string | number;
|
| 9 |
+
icon: LucideIcon;
|
| 10 |
+
change?: number;
|
| 11 |
+
suffix?: string;
|
| 12 |
+
color?: "cyan" | "violet" | "emerald" | "amber" | "rose" | "neutral";
|
| 13 |
+
delay?: number;
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
const colorMap = {
|
| 17 |
+
cyan: { border: "border-[#38bdf8]/30", text: "text-[#38bdf8]", glow: "glow-cyan", iconBg: "bg-[#38bdf8]/10" },
|
| 18 |
+
violet: { border: "border-[#818cf8]/30", text: "text-[#818cf8]", glow: "glow-violet", iconBg: "bg-[#818cf8]/10" },
|
| 19 |
+
emerald: { border: "border-[#00d68f]/30", text: "text-[#00d68f]", glow: "glow-emerald", iconBg: "bg-[#00d68f]/10" },
|
| 20 |
+
amber: { border: "border-[#f59e0b]/30", text: "text-[#f59e0b]", glow: "glow-amber", iconBg: "bg-[#f59e0b]/10" },
|
| 21 |
+
rose: { border: "border-[#f43f5e]/30", text: "text-[#f43f5e]", glow: "glow-rose", iconBg: "bg-[#f43f5e]/10" },
|
| 22 |
+
neutral: { border: "border-white/[0.06]", text: "text-[#9a9aae]", glow: "", iconBg: "bg-white/[0.04]" },
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
export default function StatCard({ title, value, icon: Icon, change, suffix = "", color = "cyan", delay = 0 }: StatCardProps) {
|
| 26 |
+
const cfg = colorMap[color];
|
| 27 |
+
return (
|
| 28 |
+
<motion.div
|
| 29 |
+
initial={{ opacity: 0, y: 12 }}
|
| 30 |
+
animate={{ opacity: 1, y: 0 }}
|
| 31 |
+
transition={{ duration: 0.4, delay: delay * 0.05, ease: [0.23, 1, 0.32, 1] }}
|
| 32 |
+
className={`glass rounded-xl p-4 border-l-2 ${cfg.border} ${cfg.glow}`}
|
| 33 |
+
>
|
| 34 |
+
<div className="flex items-center justify-between mb-3">
|
| 35 |
+
<div className={`p-2 rounded-lg ${cfg.iconBg}`}>
|
| 36 |
+
<Icon className={`w-4 h-4 ${cfg.text}`} />
|
| 37 |
+
</div>
|
| 38 |
+
{change !== undefined && (
|
| 39 |
+
<span className={`text-[11px] font-medium ${change >= 0 ? "text-[#00d68f]" : "text-[#f43f5e]"}`}>
|
| 40 |
+
{change >= 0 ? "+" : ""}{change.toFixed(2)}%
|
| 41 |
+
</span>
|
| 42 |
+
)}
|
| 43 |
+
</div>
|
| 44 |
+
<div className="text-[10px] font-bold text-[#5a5a6e] uppercase tracking-widest mb-1">{title}</div>
|
| 45 |
+
<div className={`text-xl font-bold tabular-nums ${cfg.text}`}>
|
| 46 |
+
{typeof value === "number" ? value.toLocaleString() : value}{suffix}
|
| 47 |
+
</div>
|
| 48 |
+
</motion.div>
|
| 49 |
+
);
|
| 50 |
+
}
|
ui/src/app/components/StatusBadge.tsx
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
interface StatusBadgeProps {
|
| 4 |
+
status: string;
|
| 5 |
+
label?: string;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
export default function StatusBadge({ status, label }: StatusBadgeProps) {
|
| 9 |
+
const normalized = status?.toLowerCase() || "unknown";
|
| 10 |
+
|
| 11 |
+
const styles: Record<string, string> = {
|
| 12 |
+
active: "bg-[#00d68f]/15 text-[#00d68f] border-[#00d68f]/30",
|
| 13 |
+
verified: "bg-[#00d68f]/15 text-[#00d68f] border-[#00d68f]/30",
|
| 14 |
+
wired: "bg-[#00d68f]/15 text-[#00d68f] border-[#00d68f]/30",
|
| 15 |
+
local_only: "bg-[#38bdf8]/15 text-[#38bdf8] border-[#38bdf8]/30",
|
| 16 |
+
pending: "bg-[#f59e0b]/15 text-[#f59e0b] border-[#f59e0b]/30",
|
| 17 |
+
waiting: "bg-[#f59e0b]/15 text-[#f59e0b] border-[#f59e0b]/30",
|
| 18 |
+
not_wired: "bg-[#f59e0b]/15 text-[#f59e0b] border-[#f59e0b]/30",
|
| 19 |
+
error: "bg-[#f43f5e]/15 text-[#f43f5e] border-[#f43f5e]/30",
|
| 20 |
+
risk: "bg-[#f43f5e]/15 text-[#f43f5e] border-[#f43f5e]/30",
|
| 21 |
+
inactive: "bg-white/[0.04] text-[#5a5a6e] border-white/[0.06]",
|
| 22 |
+
unknown: "bg-white/[0.04] text-[#5a5a6e] border-white/[0.06]",
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
const cls = styles[normalized] || styles.unknown;
|
| 26 |
+
const display = label || status;
|
| 27 |
+
|
| 28 |
+
return (
|
| 29 |
+
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-[10px] font-bold uppercase tracking-wider border ${cls}`}>
|
| 30 |
+
<span className={`w-1.5 h-1.5 rounded-full ${normalized.includes("active") || normalized === "wired" || normalized === "verified" ? "bg-[#00d68f]" : normalized.includes("error") || normalized === "risk" ? "bg-[#f43f5e]" : normalized.includes("pending") || normalized === "waiting" || normalized === "not_wired" ? "bg-[#f59e0b]" : "bg-[#5a5a6e]"}`} />
|
| 31 |
+
{display}
|
| 32 |
+
</span>
|
| 33 |
+
);
|
| 34 |
+
}
|
ui/src/app/globals.css
CHANGED
|
@@ -1,3 +1,109 @@
|
|
| 1 |
@tailwind base;
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
@tailwind base;
|
| 2 |
@tailwind components;
|
| 3 |
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
--bg-deep: #050508;
|
| 9 |
+
--bg-panel: #08080c;
|
| 10 |
+
--bg-elevated: #0c0c12;
|
| 11 |
+
--border-subtle: rgba(255, 255, 255, 0.04);
|
| 12 |
+
--border-default: rgba(255, 255, 255, 0.06);
|
| 13 |
+
--text-primary: #e2e2ec;
|
| 14 |
+
--text-secondary: #9a9aae;
|
| 15 |
+
--text-muted: #5a5a6e;
|
| 16 |
+
--cyan: #38bdf8;
|
| 17 |
+
--violet: #818cf8;
|
| 18 |
+
--emerald: #00d68f;
|
| 19 |
+
--amber: #f59e0b;
|
| 20 |
+
--rose: #f43f5e;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
html {
|
| 24 |
+
scroll-behavior: smooth;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
body {
|
| 28 |
+
background-color: #050508;
|
| 29 |
+
color: #e2e2ec;
|
| 30 |
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
| 31 |
+
-webkit-font-smoothing: antialiased;
|
| 32 |
+
-moz-osx-font-smoothing: grayscale;
|
| 33 |
+
font-feature-settings: 'tnum' on, 'lnum' on;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
@layer components {
|
| 37 |
+
.glass {
|
| 38 |
+
background: rgba(12, 12, 18, 0.55);
|
| 39 |
+
backdrop-filter: blur(20px) saturate(1.4);
|
| 40 |
+
-webkit-backdrop-filter: blur(20px) saturate(1.4);
|
| 41 |
+
border: 1px solid rgba(255, 255, 255, 0.06);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
.glass-strong {
|
| 45 |
+
background: rgba(12, 12, 18, 0.75);
|
| 46 |
+
backdrop-filter: blur(24px) saturate(1.6);
|
| 47 |
+
-webkit-backdrop-filter: blur(24px) saturate(1.6);
|
| 48 |
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
@layer utilities {
|
| 53 |
+
.bg-grid {
|
| 54 |
+
background-image:
|
| 55 |
+
linear-gradient(rgba(56, 189, 248, 0.03) 1px, transparent 1px),
|
| 56 |
+
linear-gradient(90deg, rgba(56, 189, 248, 0.03) 1px, transparent 1px);
|
| 57 |
+
background-size: 48px 48px;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
.glow-cyan {
|
| 61 |
+
box-shadow: 0 0 20px rgba(56, 189, 248, 0.25), 0 0 40px rgba(56, 189, 248, 0.08);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
.glow-violet {
|
| 65 |
+
box-shadow: 0 0 20px rgba(129, 140, 248, 0.25), 0 0 40px rgba(129, 140, 248, 0.08);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
.glow-emerald {
|
| 69 |
+
box-shadow: 0 0 20px rgba(0, 214, 143, 0.25), 0 0 40px rgba(0, 214, 143, 0.08);
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
.glow-amber {
|
| 73 |
+
box-shadow: 0 0 20px rgba(245, 158, 11, 0.25), 0 0 40px rgba(245, 158, 11, 0.08);
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
.glow-rose {
|
| 77 |
+
box-shadow: 0 0 20px rgba(244, 63, 94, 0.25), 0 0 40px rgba(244, 63, 94, 0.08);
|
| 78 |
+
}
|
| 79 |
+
|
| 80 |
+
.text-gradient-membra {
|
| 81 |
+
background: linear-gradient(135deg, #38bdf8 0%, #818cf8 50%, #c084fc 100%);
|
| 82 |
+
-webkit-background-clip: text;
|
| 83 |
+
-webkit-text-fill-color: transparent;
|
| 84 |
+
background-clip: text;
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
::-webkit-scrollbar {
|
| 89 |
+
width: 6px;
|
| 90 |
+
height: 6px;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
::-webkit-scrollbar-track {
|
| 94 |
+
background: #050508;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
::-webkit-scrollbar-thumb {
|
| 98 |
+
background: rgba(255, 255, 255, 0.08);
|
| 99 |
+
border-radius: 3px;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
::-webkit-scrollbar-thumb:hover {
|
| 103 |
+
background: rgba(255, 255, 255, 0.12);
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
::selection {
|
| 107 |
+
background: rgba(56, 189, 248, 0.3);
|
| 108 |
+
color: #e2e2ec;
|
| 109 |
+
}
|
ui/src/app/layout.tsx
CHANGED
|
@@ -2,8 +2,8 @@ import type { Metadata } from "next";
|
|
| 2 |
import "./globals.css";
|
| 3 |
|
| 4 |
export const metadata: Metadata = {
|
| 5 |
-
title: "AirMicroDrip
|
| 6 |
-
description: "
|
| 7 |
};
|
| 8 |
|
| 9 |
export default function RootLayout({
|
|
@@ -12,8 +12,8 @@ export default function RootLayout({
|
|
| 12 |
children: React.ReactNode;
|
| 13 |
}>) {
|
| 14 |
return (
|
| 15 |
-
<html lang="en">
|
| 16 |
-
<body>{children}</body>
|
| 17 |
</html>
|
| 18 |
);
|
| 19 |
}
|
|
|
|
| 2 |
import "./globals.css";
|
| 3 |
|
| 4 |
export const metadata: Metadata = {
|
| 5 |
+
title: "AirMicroDrip — LLM Liquidity Perpetual Futures",
|
| 6 |
+
description: "Real-time perpetual futures DEX powered by LLM inference liquidity. No mocks. Real data.",
|
| 7 |
};
|
| 8 |
|
| 9 |
export default function RootLayout({
|
|
|
|
| 12 |
children: React.ReactNode;
|
| 13 |
}>) {
|
| 14 |
return (
|
| 15 |
+
<html lang="en" className="dark">
|
| 16 |
+
<body className="bg-[#050508] text-[#e2e2ec] antialiased">{children}</body>
|
| 17 |
</html>
|
| 18 |
);
|
| 19 |
}
|
ui/src/lib/api.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL || '';
|
| 2 |
+
|
| 3 |
+
export async function fetchApi(path: string) {
|
| 4 |
+
const res = await fetch(`${API_BASE}${path}`);
|
| 5 |
+
if (!res.ok) {
|
| 6 |
+
const text = await res.text().catch(() => 'Unknown error');
|
| 7 |
+
throw new Error(`${path}: ${res.status} ${text}`);
|
| 8 |
+
}
|
| 9 |
+
return res.json();
|
| 10 |
+
}
|