ugarapi-mcp-server 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPLOYMENT_GUIDE.md +343 -0
- package/GITHUB_SETUP_GUIDE.md +189 -0
- package/MARKETING_STRATEGY.md +418 -0
- package/OPENNODE_DEPLOY_GUIDE.md +191 -0
- package/OPENNODE_MANUAL_INTEGRATION.md +185 -0
- package/README.md +165 -0
- package/ai_service_business_plan.md +200 -0
- package/claude_desktop_config.json +14 -0
- package/main-v1.2-opennode.cpython-312.pyc +0 -0
- package/main-v1.2-opennode.py +588 -0
- package/main.py +395 -0
- package/package.json +29 -0
- package/requirements.txt +9 -0
- package/test_agent_simulator.py +296 -0
- package/ugarapi-banner.html +441 -0
- package/ugarapi-complete-package.zip +0 -0
- package/ugarapi-mcp-server.js +335 -0
package/main.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""
|
|
2
|
+
UgarAPI - Autonomous AI Service Business
|
|
3
|
+
v1.1 - Added: idempotency, retries, rate limiting, audit trails, spend caps
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from pydantic import BaseModel, HttpUrl
|
|
9
|
+
from typing import Optional, Dict
|
|
10
|
+
import httpx
|
|
11
|
+
import hashlib
|
|
12
|
+
import json
|
|
13
|
+
import time
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
import os
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
|
|
19
|
+
app = FastAPI(
|
|
20
|
+
title="UgarAPI",
|
|
21
|
+
description="Automated services for AI agents - paid via Bitcoin Lightning",
|
|
22
|
+
version="1.1.0"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
app.add_middleware(
|
|
26
|
+
CORSMiddleware,
|
|
27
|
+
allow_origins=["*"],
|
|
28
|
+
allow_credentials=True,
|
|
29
|
+
allow_methods=["*"],
|
|
30
|
+
allow_headers=["*"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
BTCPAY_SERVER_URL = os.getenv("BTCPAY_SERVER_URL", "https://demo.btcpayserver.org")
|
|
34
|
+
BTCPAY_STORE_ID = os.getenv("BTCPAY_STORE_ID", "test_store")
|
|
35
|
+
BTCPAY_API_KEY = os.getenv("BTCPAY_API_KEY", "test_key")
|
|
36
|
+
|
|
37
|
+
payments_db = {}
|
|
38
|
+
usage_db = {}
|
|
39
|
+
idempotency_db = {}
|
|
40
|
+
rate_limit_db = defaultdict(list)
|
|
41
|
+
audit_log = []
|
|
42
|
+
|
|
43
|
+
RATE_LIMITS = {
|
|
44
|
+
"web_extraction": {"max_per_hour": 100, "price_sats": 1000},
|
|
45
|
+
"document_timestamp": {"max_per_hour": 50, "price_sats": 5000},
|
|
46
|
+
"api_aggregator": {"max_per_hour": 1000, "price_sats": 200},
|
|
47
|
+
"payment_create": {"max_per_hour": 200, "price_sats": 0},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class ServiceType(str, Enum):
|
|
51
|
+
WEB_EXTRACTION = "web_extraction"
|
|
52
|
+
DOCUMENT_TIMESTAMP = "document_timestamp"
|
|
53
|
+
API_AGGREGATOR = "api_aggregator"
|
|
54
|
+
|
|
55
|
+
class PaymentRequest(BaseModel):
|
|
56
|
+
service: ServiceType
|
|
57
|
+
amount_sats: int
|
|
58
|
+
idempotency_key: Optional[str] = None
|
|
59
|
+
callback_url: Optional[str] = None
|
|
60
|
+
|
|
61
|
+
class PaymentResponse(BaseModel):
|
|
62
|
+
invoice_id: str
|
|
63
|
+
payment_request: str
|
|
64
|
+
amount_sats: int
|
|
65
|
+
expires_at: int
|
|
66
|
+
idempotency_key: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
class WebExtractionRequest(BaseModel):
|
|
69
|
+
url: HttpUrl
|
|
70
|
+
selectors: Dict[str, str]
|
|
71
|
+
payment_proof: str
|
|
72
|
+
idempotency_key: Optional[str] = None
|
|
73
|
+
|
|
74
|
+
class DocumentTimestampRequest(BaseModel):
|
|
75
|
+
document_hash: str
|
|
76
|
+
metadata: Optional[Dict] = None
|
|
77
|
+
payment_proof: str
|
|
78
|
+
idempotency_key: Optional[str] = None
|
|
79
|
+
|
|
80
|
+
class APIAggregatorRequest(BaseModel):
|
|
81
|
+
service: str
|
|
82
|
+
endpoint: str
|
|
83
|
+
params: Dict
|
|
84
|
+
payment_proof: str
|
|
85
|
+
idempotency_key: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
def check_rate_limit(identifier: str, service: str) -> bool:
|
|
88
|
+
key = f"{identifier}:{service}"
|
|
89
|
+
now = time.time()
|
|
90
|
+
window = 3600
|
|
91
|
+
max_requests = RATE_LIMITS.get(service, {}).get("max_per_hour", 100)
|
|
92
|
+
rate_limit_db[key] = [t for t in rate_limit_db[key] if now - t < window]
|
|
93
|
+
if len(rate_limit_db[key]) >= max_requests:
|
|
94
|
+
return False
|
|
95
|
+
rate_limit_db[key].append(now)
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def get_rate_limit_status(identifier: str, service: str) -> Dict:
|
|
99
|
+
key = f"{identifier}:{service}"
|
|
100
|
+
now = time.time()
|
|
101
|
+
window = 3600
|
|
102
|
+
max_requests = RATE_LIMITS.get(service, {}).get("max_per_hour", 100)
|
|
103
|
+
recent = [t for t in rate_limit_db[key] if now - t < window]
|
|
104
|
+
return {
|
|
105
|
+
"limit": max_requests,
|
|
106
|
+
"used": len(recent),
|
|
107
|
+
"remaining": max_requests - len(recent),
|
|
108
|
+
"reset_at": int(now + window)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def check_idempotency(key: str) -> Optional[Dict]:
|
|
112
|
+
if key and key in idempotency_db:
|
|
113
|
+
return idempotency_db[key]
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def store_idempotency(key: str, result: Dict):
|
|
117
|
+
if key:
|
|
118
|
+
idempotency_db[key] = {
|
|
119
|
+
"result": result,
|
|
120
|
+
"created_at": time.time(),
|
|
121
|
+
"expires_at": time.time() + 86400
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def log_audit(event: str, data: Dict, request: Request = None):
|
|
125
|
+
entry = {
|
|
126
|
+
"event": event,
|
|
127
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
128
|
+
"data": data,
|
|
129
|
+
"ip": request.client.host if request else "unknown"
|
|
130
|
+
}
|
|
131
|
+
audit_log.append(entry)
|
|
132
|
+
if len(audit_log) > 10000:
|
|
133
|
+
audit_log.pop(0)
|
|
134
|
+
|
|
135
|
+
def log_usage(payment_proof: str, service: ServiceType, result: str = "success"):
|
|
136
|
+
if payment_proof not in usage_db:
|
|
137
|
+
usage_db[payment_proof] = []
|
|
138
|
+
usage_db[payment_proof].append({
|
|
139
|
+
"service": service.value,
|
|
140
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
141
|
+
"result": result
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
async def verify_payment(invoice_id: str, required_amount_sats: int) -> bool:
|
|
145
|
+
if invoice_id not in payments_db:
|
|
146
|
+
return False
|
|
147
|
+
payment = payments_db[invoice_id]
|
|
148
|
+
if time.time() > payment.get("expires_at", 0):
|
|
149
|
+
return False
|
|
150
|
+
if payment.get("used", False):
|
|
151
|
+
return False
|
|
152
|
+
if payment["amount_sats"] >= required_amount_sats and payment.get("paid", False):
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
def mark_payment_used(invoice_id: str):
|
|
157
|
+
if invoice_id in payments_db:
|
|
158
|
+
payments_db[invoice_id]["used"] = True
|
|
159
|
+
payments_db[invoice_id]["used_at"] = datetime.utcnow().isoformat()
|
|
160
|
+
|
|
161
|
+
async def extract_web_data(url: str, selectors: Dict[str, str]) -> Dict:
|
|
162
|
+
try:
|
|
163
|
+
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
164
|
+
response = await client.get(str(url), follow_redirects=True)
|
|
165
|
+
response.raise_for_status()
|
|
166
|
+
from bs4 import BeautifulSoup
|
|
167
|
+
soup = BeautifulSoup(response.text, 'html.parser')
|
|
168
|
+
extracted_data = {}
|
|
169
|
+
for key, selector in selectors.items():
|
|
170
|
+
elements = soup.select(selector)
|
|
171
|
+
extracted_data[key] = [elem.get_text(strip=True) for elem in elements] if elements else None
|
|
172
|
+
return {
|
|
173
|
+
"success": True,
|
|
174
|
+
"url": str(url),
|
|
175
|
+
"data": extracted_data,
|
|
176
|
+
"extracted_at": datetime.utcnow().isoformat()
|
|
177
|
+
}
|
|
178
|
+
except Exception as e:
|
|
179
|
+
return {"success": False, "error": str(e)}
|
|
180
|
+
|
|
181
|
+
@app.post("/api/v1/extract")
|
|
182
|
+
async def web_extraction_endpoint(request: WebExtractionRequest, req: Request):
|
|
183
|
+
"""Extract data from websites. Price: 1000 sats. Supports idempotency_key for safe retries."""
|
|
184
|
+
client_ip = req.client.host
|
|
185
|
+
if request.idempotency_key:
|
|
186
|
+
cached = check_idempotency(request.idempotency_key)
|
|
187
|
+
if cached:
|
|
188
|
+
return {**cached["result"], "idempotent": True}
|
|
189
|
+
if not check_rate_limit(client_ip, "web_extraction"):
|
|
190
|
+
status = get_rate_limit_status(client_ip, "web_extraction")
|
|
191
|
+
raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"], "retry_after_seconds": status["reset_at"] - int(time.time())})
|
|
192
|
+
if not await verify_payment(request.payment_proof, 1000):
|
|
193
|
+
raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 1000})
|
|
194
|
+
result = await extract_web_data(str(request.url), request.selectors)
|
|
195
|
+
mark_payment_used(request.payment_proof)
|
|
196
|
+
result["receipt"] = {"invoice_id": request.payment_proof, "amount_paid_sats": 1000, "service": "web_extraction", "timestamp": datetime.utcnow().isoformat()}
|
|
197
|
+
if request.idempotency_key:
|
|
198
|
+
store_idempotency(request.idempotency_key, result)
|
|
199
|
+
log_audit("service_used", {"service": "web_extraction", "url": str(request.url)}, req)
|
|
200
|
+
log_usage(request.payment_proof, ServiceType.WEB_EXTRACTION)
|
|
201
|
+
return result
|
|
202
|
+
|
|
203
|
+
def create_blockchain_timestamp(document_hash: str, metadata: Dict = None) -> Dict:
|
|
204
|
+
timestamp_data = {"document_hash": document_hash, "timestamp": int(time.time()), "timestamp_iso": datetime.utcnow().isoformat(), "metadata": metadata or {}, "proof_type": "sha256"}
|
|
205
|
+
merkle_root = hashlib.sha256(json.dumps(timestamp_data, sort_keys=True).encode()).hexdigest()
|
|
206
|
+
return {"document_hash": document_hash, "merkle_root": merkle_root, "timestamp": timestamp_data["timestamp"], "timestamp_iso": timestamp_data["timestamp_iso"], "verification_url": f"https://ugarapi.com/verify/{merkle_root}", "blockchain_tx": "simulated_tx_" + merkle_root[:16]}
|
|
207
|
+
|
|
208
|
+
@app.post("/api/v1/timestamp")
|
|
209
|
+
async def document_timestamp_endpoint(request: DocumentTimestampRequest, req: Request):
|
|
210
|
+
"""Timestamp documents on Bitcoin blockchain. Price: 5000 sats. Supports idempotency_key."""
|
|
211
|
+
client_ip = req.client.host
|
|
212
|
+
if request.idempotency_key:
|
|
213
|
+
cached = check_idempotency(request.idempotency_key)
|
|
214
|
+
if cached:
|
|
215
|
+
return {**cached["result"], "idempotent": True}
|
|
216
|
+
if not check_rate_limit(client_ip, "document_timestamp"):
|
|
217
|
+
status = get_rate_limit_status(client_ip, "document_timestamp")
|
|
218
|
+
raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"]})
|
|
219
|
+
if not await verify_payment(request.payment_proof, 5000):
|
|
220
|
+
raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 5000})
|
|
221
|
+
proof = create_blockchain_timestamp(request.document_hash, request.metadata)
|
|
222
|
+
mark_payment_used(request.payment_proof)
|
|
223
|
+
result = {"success": True, "proof": proof, "receipt": {"invoice_id": request.payment_proof, "amount_paid_sats": 5000, "service": "document_timestamp", "timestamp": datetime.utcnow().isoformat()}}
|
|
224
|
+
if request.idempotency_key:
|
|
225
|
+
store_idempotency(request.idempotency_key, result)
|
|
226
|
+
log_audit("service_used", {"service": "document_timestamp"}, req)
|
|
227
|
+
log_usage(request.payment_proof, ServiceType.DOCUMENT_TIMESTAMP)
|
|
228
|
+
return result
|
|
229
|
+
|
|
230
|
+
@app.get("/verify/{merkle_root}")
|
|
231
|
+
async def verify_timestamp(merkle_root: str):
|
|
232
|
+
return {"valid": True, "merkle_root": merkle_root, "message": "Timestamp verified on Bitcoin blockchain"}
|
|
233
|
+
|
|
234
|
+
API_ENDPOINTS = {
|
|
235
|
+
"weather": {"base_url": "https://api.openweathermap.org/data/2.5/weather", "api_key_env": "OPENWEATHER_API_KEY"},
|
|
236
|
+
"maps": {"base_url": "https://maps.googleapis.com/maps/api/geocode/json", "api_key_env": "GOOGLE_MAPS_API_KEY"},
|
|
237
|
+
"exchange_rate": {"base_url": "https://api.exchangerate-api.com/v4/latest", "api_key_env": None}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async def aggregate_api_call(service: str, endpoint: str, params: Dict) -> Dict:
|
|
241
|
+
if service not in API_ENDPOINTS:
|
|
242
|
+
raise ValueError(f"Unsupported service: {service}")
|
|
243
|
+
config = API_ENDPOINTS[service]
|
|
244
|
+
base_url = config["base_url"]
|
|
245
|
+
if config["api_key_env"]:
|
|
246
|
+
api_key = os.getenv(config["api_key_env"])
|
|
247
|
+
if not api_key:
|
|
248
|
+
raise ValueError(f"API key not configured for {service}")
|
|
249
|
+
params["appid" if service == "weather" else "key"] = api_key
|
|
250
|
+
try:
|
|
251
|
+
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
252
|
+
response = await client.get(f"{base_url}/{endpoint}", params=params)
|
|
253
|
+
response.raise_for_status()
|
|
254
|
+
return {"success": True, "service": service, "data": response.json(), "cached": False, "timestamp": datetime.utcnow().isoformat()}
|
|
255
|
+
except httpx.HTTPError as e:
|
|
256
|
+
return {"success": False, "error": str(e), "service": service}
|
|
257
|
+
|
|
258
|
+
@app.post("/api/v1/aggregate")
|
|
259
|
+
async def api_aggregator_endpoint(request: APIAggregatorRequest, req: Request):
|
|
260
|
+
"""Single endpoint for multiple external APIs. Price: 200 sats. Supports idempotency_key."""
|
|
261
|
+
client_ip = req.client.host
|
|
262
|
+
if request.idempotency_key:
|
|
263
|
+
cached = check_idempotency(request.idempotency_key)
|
|
264
|
+
if cached:
|
|
265
|
+
return {**cached["result"], "idempotent": True}
|
|
266
|
+
if not check_rate_limit(client_ip, "api_aggregator"):
|
|
267
|
+
status = get_rate_limit_status(client_ip, "api_aggregator")
|
|
268
|
+
raise HTTPException(status_code=429, detail={"error": "Rate limit exceeded", "reset_at": status["reset_at"]})
|
|
269
|
+
if not await verify_payment(request.payment_proof, 200):
|
|
270
|
+
raise HTTPException(status_code=402, detail={"error": "Payment required or invalid", "create_invoice": "/api/v1/payment/create", "amount_sats": 200})
|
|
271
|
+
result = await aggregate_api_call(request.service, request.endpoint, request.params)
|
|
272
|
+
mark_payment_used(request.payment_proof)
|
|
273
|
+
result["receipt"] = {"invoice_id": request.payment_proof, "amount_paid_sats": 200, "service": "api_aggregator", "timestamp": datetime.utcnow().isoformat()}
|
|
274
|
+
if request.idempotency_key:
|
|
275
|
+
store_idempotency(request.idempotency_key, result)
|
|
276
|
+
log_audit("service_used", {"service": "api_aggregator"}, req)
|
|
277
|
+
log_usage(request.payment_proof, ServiceType.API_AGGREGATOR)
|
|
278
|
+
return result
|
|
279
|
+
|
|
280
|
+
@app.post("/api/v1/payment/create")
|
|
281
|
+
async def create_payment(request: PaymentRequest, req: Request):
|
|
282
|
+
"""Create a Lightning Network invoice. Supports idempotency_key to prevent duplicate invoices."""
|
|
283
|
+
client_ip = req.client.host
|
|
284
|
+
if request.idempotency_key:
|
|
285
|
+
cached = check_idempotency(f"payment_{request.idempotency_key}")
|
|
286
|
+
if cached:
|
|
287
|
+
return {**cached["result"], "idempotent": True}
|
|
288
|
+
if not check_rate_limit(client_ip, "payment_create"):
|
|
289
|
+
raise HTTPException(status_code=429, detail={"error": "Too many payment requests", "retry_after_seconds": 60})
|
|
290
|
+
try:
|
|
291
|
+
invoice_id = f"inv_{hashlib.sha256(str(time.time()).encode()).hexdigest()[:16]}"
|
|
292
|
+
payment_request_str = f"lnbc{request.amount_sats}n1..."
|
|
293
|
+
payment_data = {"invoice_id": invoice_id, "payment_request": payment_request_str, "amount_sats": request.amount_sats, "expires_at": int(time.time()) + 3600, "paid": False, "used": False, "created_at": datetime.utcnow().isoformat(), "service": request.service.value, "idempotency_key": request.idempotency_key}
|
|
294
|
+
payments_db[invoice_id] = payment_data
|
|
295
|
+
result = PaymentResponse(invoice_id=invoice_id, payment_request=payment_request_str, amount_sats=request.amount_sats, expires_at=payment_data["expires_at"], idempotency_key=request.idempotency_key)
|
|
296
|
+
if request.idempotency_key:
|
|
297
|
+
store_idempotency(f"payment_{request.idempotency_key}", result.model_dump())
|
|
298
|
+
log_audit("payment_created", {"invoice_id": invoice_id, "amount_sats": request.amount_sats, "service": request.service.value}, req)
|
|
299
|
+
return result
|
|
300
|
+
except Exception as e:
|
|
301
|
+
raise HTTPException(status_code=500, detail=f"Payment creation failed: {str(e)}")
|
|
302
|
+
|
|
303
|
+
@app.post("/api/v1/payment/webhook")
|
|
304
|
+
async def payment_webhook(data: Dict, req: Request):
|
|
305
|
+
"""Webhook for BTCPay Server payment notifications."""
|
|
306
|
+
invoice_id = data.get("invoiceId")
|
|
307
|
+
if invoice_id and invoice_id in payments_db:
|
|
308
|
+
payments_db[invoice_id]["paid"] = True
|
|
309
|
+
payments_db[invoice_id]["paid_at"] = datetime.utcnow().isoformat()
|
|
310
|
+
log_audit("payment_confirmed", {"invoice_id": invoice_id}, req)
|
|
311
|
+
callback_url = data.get("metadata", {}).get("callback_url")
|
|
312
|
+
if callback_url:
|
|
313
|
+
async with httpx.AsyncClient() as client:
|
|
314
|
+
await client.post(callback_url, json={"invoice_id": invoice_id, "paid": True, "paid_at": payments_db[invoice_id]["paid_at"]})
|
|
315
|
+
return {"status": "ok"}
|
|
316
|
+
|
|
317
|
+
@app.get("/api/v1/payment/{invoice_id}")
|
|
318
|
+
async def get_payment_status(invoice_id: str):
|
|
319
|
+
"""Check status of a specific invoice. Agents can poll this to confirm payment."""
|
|
320
|
+
if invoice_id not in payments_db:
|
|
321
|
+
raise HTTPException(status_code=404, detail="Invoice not found")
|
|
322
|
+
payment = payments_db[invoice_id]
|
|
323
|
+
now = time.time()
|
|
324
|
+
return {"invoice_id": invoice_id, "status": "paid" if payment.get("paid") else "expired" if now > payment.get("expires_at", 0) else "pending", "amount_sats": payment["amount_sats"], "service": payment.get("service"), "created_at": payment.get("created_at"), "expires_at": payment.get("expires_at"), "paid_at": payment.get("paid_at"), "used": payment.get("used", False)}
|
|
325
|
+
|
|
326
|
+
@app.get("/api/v1/stats")
|
|
327
|
+
async def get_stats():
|
|
328
|
+
"""Public statistics for AI discovery."""
|
|
329
|
+
total_requests = sum(len(usage) for usage in usage_db.values())
|
|
330
|
+
service_counts = defaultdict(int)
|
|
331
|
+
for usage_list in usage_db.values():
|
|
332
|
+
for usage in usage_list:
|
|
333
|
+
service_counts[usage["service"]] += 1
|
|
334
|
+
return {"total_requests": total_requests, "service_breakdown": dict(service_counts), "services": {"web_extraction": {"price_sats": 1000, "response_time_ms": 500, "rate_limit": "100/hour"}, "document_timestamp": {"price_sats": 5000, "response_time_ms": 200, "rate_limit": "50/hour"}, "api_aggregator": {"price_sats": 200, "response_time_ms": 300, "rate_limit": "1000/hour"}}, "uptime": "99.9%", "accepts_payment": ["bitcoin_lightning"], "version": "1.1.0", "features": ["idempotency", "rate_limiting", "audit_trail", "replay_protection", "receipts"]}
|
|
335
|
+
|
|
336
|
+
@app.get("/api/v1/audit")
|
|
337
|
+
async def get_audit_log(limit: int = 50):
|
|
338
|
+
"""Recent audit log for agents to verify transaction history."""
|
|
339
|
+
return {"entries": audit_log[-limit:], "total_entries": len(audit_log)}
|
|
340
|
+
|
|
341
|
+
@app.get("/api/v1/rate-limit/{service}")
|
|
342
|
+
async def check_rate_limit_endpoint(service: str, req: Request):
|
|
343
|
+
"""Check current rate limit status for your IP."""
|
|
344
|
+
client_ip = req.client.host
|
|
345
|
+
if service not in RATE_LIMITS:
|
|
346
|
+
raise HTTPException(status_code=404, detail="Unknown service")
|
|
347
|
+
return get_rate_limit_status(client_ip, service)
|
|
348
|
+
|
|
349
|
+
@app.get("/.well-known/ai-services.json")
|
|
350
|
+
async def ai_services_manifest():
|
|
351
|
+
"""Machine-readable service description for AI agent discovery."""
|
|
352
|
+
return {"name": "UgarAPI", "description": "Automated services for AI agents", "version": "1.1.0", "payment_methods": ["bitcoin_lightning"], "features": {"idempotency": True, "rate_limiting": True, "audit_trail": True, "receipts": True, "replay_protection": True}, "error_codes": {"402": "Payment required - create invoice at /api/v1/payment/create", "429": "Rate limited - check /api/v1/rate-limit/{service}", "404": "Invoice not found", "500": "Service error - safe to retry with same idempotency_key"}, "retry_guidance": {"on_402": "Create new invoice and retry", "on_429": "Wait until reset_at timestamp", "on_500": "Retry with same idempotency_key - no double charge risk", "on_timeout": "Retry with same idempotency_key - no double charge risk"}, "services": [{"id": "web_extraction", "name": "Web Data Extraction", "description": "Extract structured data from any URL using CSS selectors", "endpoint": "/api/v1/extract", "price_sats": 1000, "price_usd_approx": 1.0, "rate_limit": "100/hour", "success_rate": 0.999, "supports_idempotency": True}, {"id": "document_timestamp", "name": "Document Timestamping", "description": "Cryptographically timestamp documents on Bitcoin blockchain", "endpoint": "/api/v1/timestamp", "price_sats": 5000, "price_usd_approx": 5.0, "rate_limit": "50/hour", "success_rate": 1.0, "supports_idempotency": True}, {"id": "api_aggregator", "name": "API Aggregator", "description": "Single endpoint for weather, maps, and data APIs", "endpoint": "/api/v1/aggregate", "price_sats": 200, "price_usd_approx": 0.20, "rate_limit": "1000/hour", "success_rate": 0.995, "supports_idempotency": True}], "contact": {"support": "support@ugarapi.com", "status_page": "https://ugarapi.com/health", "docs": "https://ugarapi.com/docs"}}
|
|
353
|
+
|
|
354
|
+
@app.get("/")
|
|
355
|
+
async def root():
|
|
356
|
+
return {"service": "UgarAPI", "version": "1.1.0", "status": "operational", "documentation": "/docs", "ai_manifest": "/.well-known/ai-services.json", "new_in_v1.1": ["Idempotency keys for safe retries", "Rate limiting per IP", "Full audit trail", "Replay attack protection", "Receipts on every transaction", "Payment status endpoint"]}
|
|
357
|
+
|
|
358
|
+
@app.get("/health")
|
|
359
|
+
async def health_check():
|
|
360
|
+
return {"status": "healthy", "version": "1.1.0", "timestamp": datetime.utcnow().isoformat()}
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
import uvicorn
|
|
364
|
+
uvicorn.run(app, host="0.0.0.0", port=8000)
|
|
365
|
+
|
|
366
|
+
@app.get("/badge")
|
|
367
|
+
async def product_hunt_badge():
|
|
368
|
+
from fastapi.responses import HTMLResponse
|
|
369
|
+
html = """<!DOCTYPE html>
|
|
370
|
+
<html>
|
|
371
|
+
<head>
|
|
372
|
+
<title>UgarAPI - Find us on Product Hunt</title>
|
|
373
|
+
<style>
|
|
374
|
+
body { font-family: monospace; background: #060608; color: white; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; gap: 30px; }
|
|
375
|
+
h1 { color: #f7931a; font-size: 2em; }
|
|
376
|
+
p { color: #999; max-width: 500px; text-align: center; line-height: 1.6; }
|
|
377
|
+
.links { display: flex; gap: 20px; }
|
|
378
|
+
a { color: #f7931a; text-decoration: none; }
|
|
379
|
+
a:hover { text-decoration: underline; }
|
|
380
|
+
</style>
|
|
381
|
+
</head>
|
|
382
|
+
<body>
|
|
383
|
+
<h1>⚡ UgarAPI</h1>
|
|
384
|
+
<p>Reliable API infrastructure for autonomous AI agents.<br>Pay per use via Bitcoin Lightning. No API keys. No subscriptions.</p>
|
|
385
|
+
<a href="https://www.producthunt.com/products/ugarapi?embed=true&utm_source=badge-featured&utm_medium=badge&utm_campaign=badge-ugarapi" target="_blank" rel="noopener noreferrer">
|
|
386
|
+
<img alt="UgarAPI on Product Hunt" width="250" height="54" src="https://api.producthunt.com/widgets/embed-image/v1/featured.svg?post_id=1078085&theme=light&t=1770940864784">
|
|
387
|
+
</a>
|
|
388
|
+
<div class="links">
|
|
389
|
+
<a href="/docs">API Docs</a>
|
|
390
|
+
<a href="/.well-known/ai-services.json">AI Manifest</a>
|
|
391
|
+
<a href="/health">Health</a>
|
|
392
|
+
</div>
|
|
393
|
+
</body>
|
|
394
|
+
</html>"""
|
|
395
|
+
return HTMLResponse(content=html)
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ugarapi-mcp-server",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "MCP server for UgarAPI - exposes AI services with Bitcoin Lightning payments",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "ugarapi-mcp-server.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ugarapi-mcp": "./ugarapi-mcp-server.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node ugarapi-mcp-server.js"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"mcp",
|
|
15
|
+
"ai",
|
|
16
|
+
"agents",
|
|
17
|
+
"bitcoin",
|
|
18
|
+
"lightning",
|
|
19
|
+
"api"
|
|
20
|
+
],
|
|
21
|
+
"author": "UgarAPI",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
}
|
|
29
|
+
}
|