atp-protocol 1.2.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
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.
- atp/__init__.py +9 -1
- atp/client.py +618 -0
- atp/config.py +5 -0
- atp/encryption.py +155 -0
- atp/middleware.py +442 -103
- atp/schemas.py +186 -0
- atp/settlement_client.py +608 -52
- atp_protocol-1.3.0.dist-info/METADATA +590 -0
- atp_protocol-1.3.0.dist-info/RECORD +11 -0
- atp_protocol-1.2.0.dist-info/METADATA +0 -401
- atp_protocol-1.2.0.dist-info/RECORD +0 -9
- {atp_protocol-1.2.0.dist-info → atp_protocol-1.3.0.dist-info}/LICENSE +0 -0
- {atp_protocol-1.2.0.dist-info → atp_protocol-1.3.0.dist-info}/WHEEL +0 -0
atp/settlement_client.py
CHANGED
|
@@ -8,31 +8,389 @@ logic to the immutable service.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import json
|
|
11
12
|
from typing import Any, Dict, Optional
|
|
12
13
|
|
|
13
14
|
import httpx
|
|
14
15
|
from loguru import logger
|
|
15
16
|
|
|
16
|
-
from atp.config import ATP_SETTLEMENT_URL
|
|
17
|
+
from atp.config import ATP_SETTLEMENT_URL, ATP_SETTLEMENT_TIMEOUT
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SettlementServiceError(Exception):
|
|
21
|
+
"""
|
|
22
|
+
Exception raised when settlement service returns an error.
|
|
23
|
+
|
|
24
|
+
This exception carries detailed error information from the settlement service
|
|
25
|
+
response, including HTTP status codes, error types, and detailed error messages.
|
|
26
|
+
It provides structured error information that can be easily converted to API
|
|
27
|
+
response formats.
|
|
28
|
+
|
|
29
|
+
**Attributes:**
|
|
30
|
+
status_code (Optional[int]): HTTP status code from the settlement service response.
|
|
31
|
+
error_detail (Optional[str]): Detailed error message from the service.
|
|
32
|
+
error_type (Optional[str]): Type/category of the error (e.g., "Client error",
|
|
33
|
+
"Server error", "Timeout", "Connection error").
|
|
34
|
+
response_body (Optional[Dict[str, Any]]): Full response body if available.
|
|
35
|
+
|
|
36
|
+
**Error Types:**
|
|
37
|
+
- "Invalid request" (400): Bad request format or missing required parameters
|
|
38
|
+
- "Authentication error" (401): Authentication failed
|
|
39
|
+
- "Authorization error" (403): Insufficient permissions
|
|
40
|
+
- "Not found" (404): Resource not found
|
|
41
|
+
- "Client error" (4xx): Other client-side errors
|
|
42
|
+
- "Server error" (5xx): Server-side errors
|
|
43
|
+
- "Timeout": Request timed out (payment may have succeeded)
|
|
44
|
+
- "Connection timeout": Connection to service timed out
|
|
45
|
+
- "Connection error": Failed to connect to service
|
|
46
|
+
|
|
47
|
+
**Example:**
|
|
48
|
+
```python
|
|
49
|
+
try:
|
|
50
|
+
result = await client.settle(...)
|
|
51
|
+
except SettlementServiceError as e:
|
|
52
|
+
print(f"Error type: {e.error_type}")
|
|
53
|
+
print(f"Status code: {e.status_code}")
|
|
54
|
+
print(f"Detail: {e.error_detail}")
|
|
55
|
+
# Convert to dict for API response
|
|
56
|
+
error_dict = e.to_dict()
|
|
57
|
+
```
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
message: str,
|
|
63
|
+
status_code: Optional[int] = None,
|
|
64
|
+
error_detail: Optional[str] = None,
|
|
65
|
+
error_type: Optional[str] = None,
|
|
66
|
+
response_body: Optional[Dict[str, Any]] = None,
|
|
67
|
+
):
|
|
68
|
+
"""
|
|
69
|
+
Initialize settlement service error.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
message: Error message.
|
|
73
|
+
status_code: HTTP status code from the response.
|
|
74
|
+
error_detail: Detailed error message from the service.
|
|
75
|
+
error_type: Type/category of the error.
|
|
76
|
+
response_body: Full response body if available.
|
|
77
|
+
"""
|
|
78
|
+
super().__init__(message)
|
|
79
|
+
self.status_code = status_code
|
|
80
|
+
self.error_detail = error_detail
|
|
81
|
+
self.error_type = error_type
|
|
82
|
+
self.response_body = response_body
|
|
83
|
+
|
|
84
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
85
|
+
"""
|
|
86
|
+
Convert error to dictionary for API responses.
|
|
87
|
+
|
|
88
|
+
Returns a dictionary representation of the error suitable for including
|
|
89
|
+
in API responses. The dictionary includes error type, message, detail,
|
|
90
|
+
and status code.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Dict with keys: "error" (error type), "message" (error message),
|
|
94
|
+
"detail" (detailed error message, if available),
|
|
95
|
+
"status_code" (HTTP status code, if available).
|
|
96
|
+
"""
|
|
97
|
+
result: Dict[str, Any] = {
|
|
98
|
+
"error": self.error_type or "Settlement service error",
|
|
99
|
+
"message": str(self),
|
|
100
|
+
}
|
|
101
|
+
if self.error_detail:
|
|
102
|
+
result["detail"] = self.error_detail
|
|
103
|
+
if self.status_code:
|
|
104
|
+
result["status_code"] = self.status_code
|
|
105
|
+
return result
|
|
17
106
|
|
|
18
107
|
|
|
19
108
|
class SettlementServiceClient:
|
|
20
|
-
"""
|
|
109
|
+
"""
|
|
110
|
+
Client for ATP Settlement Service API.
|
|
111
|
+
|
|
112
|
+
This client provides an interface for communicating with the ATP Settlement Service,
|
|
113
|
+
which handles all settlement logic in an immutable, centralized manner. The client
|
|
114
|
+
abstracts HTTP communication and provides structured error handling.
|
|
115
|
+
|
|
116
|
+
**Architecture:**
|
|
117
|
+
|
|
118
|
+
The settlement service is a centralized API that handles:
|
|
119
|
+
- Usage token parsing from various API formats (OpenAI, Anthropic, Google, etc.)
|
|
120
|
+
- Payment amount calculation based on token usage and pricing rates
|
|
121
|
+
- Solana blockchain transaction execution
|
|
122
|
+
- Payment verification and confirmation
|
|
123
|
+
|
|
124
|
+
All settlement logic is immutable and centralized, ensuring consistency across
|
|
125
|
+
all services using the ATP Protocol.
|
|
126
|
+
|
|
127
|
+
**Error Handling:**
|
|
128
|
+
|
|
129
|
+
The client provides comprehensive error handling:
|
|
130
|
+
- Automatic extraction of error details from various response formats
|
|
131
|
+
- Structured error types based on HTTP status codes
|
|
132
|
+
- Special handling for timeout errors (payment may have succeeded)
|
|
133
|
+
- Detailed logging with appropriate log levels
|
|
134
|
+
|
|
135
|
+
**Timeout Considerations:**
|
|
136
|
+
|
|
137
|
+
Settlement operations may take time due to blockchain confirmation. The default
|
|
138
|
+
timeout is 300 seconds (5 minutes), but this can be configured. If a timeout occurs,
|
|
139
|
+
the payment may have been sent successfully - check the blockchain for transaction
|
|
140
|
+
confirmation.
|
|
141
|
+
|
|
142
|
+
**Attributes:**
|
|
143
|
+
base_url (str): Base URL of the settlement service (trailing slashes removed).
|
|
144
|
+
timeout (float): Request timeout in seconds for all API calls.
|
|
145
|
+
|
|
146
|
+
**Example Usage:**
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
from atp.settlement_client import SettlementServiceClient
|
|
150
|
+
|
|
151
|
+
# Initialize client
|
|
152
|
+
client = SettlementServiceClient(
|
|
153
|
+
base_url="https://facilitator.swarms.world",
|
|
154
|
+
timeout=300.0 # 5 minutes
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Parse usage from any format
|
|
158
|
+
usage_data = {
|
|
159
|
+
"prompt_tokens": 100,
|
|
160
|
+
"completion_tokens": 50,
|
|
161
|
+
"total_tokens": 150
|
|
162
|
+
}
|
|
163
|
+
parsed = await client.parse_usage(usage_data)
|
|
164
|
+
# Returns: {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}
|
|
165
|
+
|
|
166
|
+
# Calculate payment
|
|
167
|
+
payment_calc = await client.calculate_payment(
|
|
168
|
+
usage=usage_data,
|
|
169
|
+
input_cost_per_million_usd=10.0,
|
|
170
|
+
output_cost_per_million_usd=30.0,
|
|
171
|
+
payment_token="SOL"
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Execute settlement
|
|
175
|
+
result = await client.settle(
|
|
176
|
+
private_key="[1,2,3,...]", # Wallet private key
|
|
177
|
+
usage=usage_data,
|
|
178
|
+
input_cost_per_million_usd=10.0,
|
|
179
|
+
output_cost_per_million_usd=30.0,
|
|
180
|
+
recipient_pubkey="RecipientPublicKeyHere",
|
|
181
|
+
payment_token="SOL"
|
|
182
|
+
)
|
|
183
|
+
# Returns: {"status": "paid", "transaction_signature": "...", ...}
|
|
184
|
+
|
|
185
|
+
# Health check
|
|
186
|
+
health = await client.health_check()
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
**API Endpoints:**
|
|
190
|
+
|
|
191
|
+
The client communicates with the following settlement service endpoints:
|
|
192
|
+
- `POST /v1/settlement/parse-usage`: Parse usage tokens from various formats
|
|
193
|
+
- `POST /v1/settlement/calculate-payment`: Calculate payment amounts
|
|
194
|
+
- `POST /v1/settlement/settle`: Execute payment transaction
|
|
195
|
+
- `GET /health`: Health check endpoint
|
|
196
|
+
"""
|
|
21
197
|
|
|
22
198
|
def __init__(
|
|
23
199
|
self,
|
|
24
200
|
base_url: str = ATP_SETTLEMENT_URL,
|
|
25
|
-
timeout: float =
|
|
201
|
+
timeout: Optional[float] = None,
|
|
26
202
|
):
|
|
27
203
|
"""
|
|
28
204
|
Initialize the settlement service client.
|
|
29
205
|
|
|
30
206
|
Args:
|
|
31
207
|
base_url: Base URL of the settlement service (default: ATP_SETTLEMENT_URL).
|
|
32
|
-
timeout: Request timeout in seconds (default:
|
|
208
|
+
timeout: Request timeout in seconds (default: ATP_SETTLEMENT_TIMEOUT or 300.0).
|
|
209
|
+
Settlement operations may take longer due to blockchain confirmation times.
|
|
210
|
+
User-configurable - can be set via environment variable or passed directly.
|
|
33
211
|
"""
|
|
34
212
|
self.base_url = base_url.rstrip("/")
|
|
35
|
-
self.timeout = timeout
|
|
213
|
+
self.timeout = timeout if timeout is not None else ATP_SETTLEMENT_TIMEOUT
|
|
214
|
+
|
|
215
|
+
def _extract_error_details(
|
|
216
|
+
self, response: httpx.Response
|
|
217
|
+
) -> Dict[str, Any]:
|
|
218
|
+
"""
|
|
219
|
+
Extract error details from HTTP response.
|
|
220
|
+
|
|
221
|
+
This internal method parses error information from various response formats
|
|
222
|
+
commonly used by FastAPI and other web frameworks. It handles JSON responses,
|
|
223
|
+
text responses, and multiple error field formats.
|
|
224
|
+
|
|
225
|
+
**Supported Error Formats:**
|
|
226
|
+
- FastAPI: `{"detail": "error message"}`
|
|
227
|
+
- Generic: `{"error": "error message", "type": "error_type"}`
|
|
228
|
+
- Message: `{"message": "error message"}`
|
|
229
|
+
- Plain text: Non-JSON text responses
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
response: HTTP response object from httpx.
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Dict with error details:
|
|
236
|
+
- `status_code` (int): HTTP status code
|
|
237
|
+
- `error_detail` (Optional[str]): Detailed error message
|
|
238
|
+
- `error_type` (Optional[str]): Type/category of error
|
|
239
|
+
- `response_body` (Optional[Dict[str, Any]]): Full parsed response body
|
|
240
|
+
"""
|
|
241
|
+
error_info: Dict[str, Any] = {
|
|
242
|
+
"status_code": response.status_code,
|
|
243
|
+
"error_detail": None,
|
|
244
|
+
"error_type": None,
|
|
245
|
+
"response_body": None,
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
# Try to parse JSON response
|
|
250
|
+
response_body = response.json()
|
|
251
|
+
error_info["response_body"] = response_body
|
|
252
|
+
|
|
253
|
+
# Extract error details from common response formats
|
|
254
|
+
if isinstance(response_body, dict):
|
|
255
|
+
# FastAPI error format: {"detail": "error message"}
|
|
256
|
+
if "detail" in response_body:
|
|
257
|
+
error_info["error_detail"] = response_body["detail"]
|
|
258
|
+
# Alternative format: {"error": "error message"}
|
|
259
|
+
elif "error" in response_body:
|
|
260
|
+
error_info["error_detail"] = response_body["error"]
|
|
261
|
+
error_info["error_type"] = response_body.get("type")
|
|
262
|
+
# Message field
|
|
263
|
+
elif "message" in response_body:
|
|
264
|
+
error_info["error_detail"] = response_body["message"]
|
|
265
|
+
error_info["error_type"] = response_body.get("error")
|
|
266
|
+
|
|
267
|
+
except (json.JSONDecodeError, ValueError):
|
|
268
|
+
# If JSON parsing fails, use text response
|
|
269
|
+
try:
|
|
270
|
+
text_response = response.text
|
|
271
|
+
if text_response:
|
|
272
|
+
error_info["error_detail"] = text_response
|
|
273
|
+
except Exception:
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
return error_info
|
|
277
|
+
|
|
278
|
+
def _handle_http_error(
|
|
279
|
+
self, error: httpx.HTTPError, operation: str
|
|
280
|
+
) -> SettlementServiceError:
|
|
281
|
+
"""
|
|
282
|
+
Handle HTTP error and extract error details.
|
|
283
|
+
|
|
284
|
+
This internal method processes HTTP errors from httpx and converts them into
|
|
285
|
+
structured SettlementServiceError exceptions. It handles both HTTP status
|
|
286
|
+
errors (with response) and network/timeout errors (without response).
|
|
287
|
+
|
|
288
|
+
**Error Type Detection:**
|
|
289
|
+
- 400: "Invalid request"
|
|
290
|
+
- 401: "Authentication error"
|
|
291
|
+
- 403: "Authorization error"
|
|
292
|
+
- 404: "Not found"
|
|
293
|
+
- 4xx: "Client error"
|
|
294
|
+
- 5xx: "Server error"
|
|
295
|
+
- ReadTimeout: "Timeout" (with special message about payment possibly succeeding)
|
|
296
|
+
- ConnectTimeout: "Connection timeout"
|
|
297
|
+
- ConnectError: "Connection error"
|
|
298
|
+
|
|
299
|
+
**Logging:**
|
|
300
|
+
- Server errors (5xx): Logged at ERROR level
|
|
301
|
+
- Client errors (4xx): Logged at WARNING level
|
|
302
|
+
- Network errors: Logged at ERROR level
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
error: HTTP error exception from httpx (HTTPStatusError, ReadTimeout, etc.).
|
|
306
|
+
operation: Name of the operation that failed (e.g., "parse_usage", "settle")
|
|
307
|
+
for logging and error messages.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
SettlementServiceError with extracted details including status code,
|
|
311
|
+
error type, error detail, and response body (if available).
|
|
312
|
+
|
|
313
|
+
**Note:**
|
|
314
|
+
For timeout errors, the error message includes a note that the payment
|
|
315
|
+
may have been sent successfully, and users should check the blockchain
|
|
316
|
+
for transaction confirmation.
|
|
317
|
+
"""
|
|
318
|
+
# Check if error has a response (HTTPStatusError)
|
|
319
|
+
if hasattr(error, "response") and error.response is not None:
|
|
320
|
+
response = error.response
|
|
321
|
+
error_info = self._extract_error_details(response)
|
|
322
|
+
|
|
323
|
+
# Determine error type based on status code
|
|
324
|
+
status_code = error_info["status_code"]
|
|
325
|
+
if 400 <= status_code < 500:
|
|
326
|
+
error_type = "Client error"
|
|
327
|
+
if status_code == 400:
|
|
328
|
+
error_type = "Invalid request"
|
|
329
|
+
elif status_code == 401:
|
|
330
|
+
error_type = "Authentication error"
|
|
331
|
+
elif status_code == 403:
|
|
332
|
+
error_type = "Authorization error"
|
|
333
|
+
elif status_code == 404:
|
|
334
|
+
error_type = "Not found"
|
|
335
|
+
elif status_code >= 500:
|
|
336
|
+
error_type = "Server error"
|
|
337
|
+
else:
|
|
338
|
+
error_type = "HTTP error"
|
|
339
|
+
|
|
340
|
+
# Build error message
|
|
341
|
+
error_detail = error_info["error_detail"] or str(error)
|
|
342
|
+
message = (
|
|
343
|
+
f"Settlement service {operation} failed: {error_detail}"
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
# Log with appropriate level
|
|
347
|
+
if status_code >= 500:
|
|
348
|
+
logger.error(
|
|
349
|
+
f"Settlement service {operation} failed (HTTP {status_code}): {error_detail}"
|
|
350
|
+
)
|
|
351
|
+
else:
|
|
352
|
+
logger.warning(
|
|
353
|
+
f"Settlement service {operation} failed (HTTP {status_code}): {error_detail}"
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
return SettlementServiceError(
|
|
357
|
+
message=message,
|
|
358
|
+
status_code=status_code,
|
|
359
|
+
error_detail=error_detail,
|
|
360
|
+
error_type=error_type,
|
|
361
|
+
response_body=error_info["response_body"],
|
|
362
|
+
)
|
|
363
|
+
else:
|
|
364
|
+
# Network/timeout errors without response
|
|
365
|
+
error_type = type(error).__name__
|
|
366
|
+
message = f"Settlement service {operation} failed: {str(error)}"
|
|
367
|
+
|
|
368
|
+
# Provide more informative error messages for timeouts
|
|
369
|
+
if isinstance(error, httpx.ReadTimeout):
|
|
370
|
+
message = (
|
|
371
|
+
f"Settlement service {operation} timed out after {self.timeout}s. "
|
|
372
|
+
"The payment may have been sent successfully, but the settlement service "
|
|
373
|
+
"did not respond in time. Check the blockchain for transaction confirmation."
|
|
374
|
+
)
|
|
375
|
+
error_type = "Timeout"
|
|
376
|
+
elif isinstance(error, httpx.ConnectTimeout):
|
|
377
|
+
message = (
|
|
378
|
+
f"Connection to settlement service timed out during {operation}. "
|
|
379
|
+
"The service may be unreachable or overloaded."
|
|
380
|
+
)
|
|
381
|
+
error_type = "Connection timeout"
|
|
382
|
+
elif isinstance(error, httpx.ConnectError):
|
|
383
|
+
message = (
|
|
384
|
+
f"Failed to connect to settlement service during {operation}. "
|
|
385
|
+
"The service may be down or unreachable."
|
|
386
|
+
)
|
|
387
|
+
error_type = "Connection error"
|
|
388
|
+
|
|
389
|
+
logger.error(f"Settlement service {operation} failed: {message}")
|
|
390
|
+
return SettlementServiceError(
|
|
391
|
+
message=message,
|
|
392
|
+
error_type=error_type,
|
|
393
|
+
)
|
|
36
394
|
|
|
37
395
|
async def parse_usage(
|
|
38
396
|
self, usage_data: Dict[str, Any]
|
|
@@ -40,11 +398,53 @@ class SettlementServiceClient:
|
|
|
40
398
|
"""
|
|
41
399
|
Parse usage tokens from various API formats.
|
|
42
400
|
|
|
43
|
-
|
|
44
|
-
|
|
401
|
+
This method sends usage data to the settlement service's parse-usage endpoint,
|
|
402
|
+
which automatically detects the format and extracts token counts. Supports
|
|
403
|
+
multiple API provider formats including OpenAI, Anthropic, Google/Gemini,
|
|
404
|
+
Cohere, and nested structures.
|
|
45
405
|
|
|
46
|
-
|
|
47
|
-
|
|
406
|
+
**Supported Formats:**
|
|
407
|
+
- OpenAI: `prompt_tokens`, `completion_tokens`, `total_tokens`
|
|
408
|
+
- Anthropic: `input_tokens`, `output_tokens`, `total_tokens`
|
|
409
|
+
- Google/Gemini: `promptTokenCount`, `candidatesTokenCount`, `totalTokenCount`
|
|
410
|
+
- Cohere: `tokens`, `input_tokens`, `output_tokens`
|
|
411
|
+
- Nested: `usage.usage`, `meta.usage`, `statistics`
|
|
412
|
+
|
|
413
|
+
**Args:**
|
|
414
|
+
usage_data: Usage data in any supported format. Can be the entire response
|
|
415
|
+
body or just the usage portion. The service handles nested structures
|
|
416
|
+
automatically.
|
|
417
|
+
|
|
418
|
+
**Returns:**
|
|
419
|
+
Dict with normalized keys:
|
|
420
|
+
- `input_tokens` (Optional[int]): Number of input/prompt tokens
|
|
421
|
+
- `output_tokens` (Optional[int]): Number of output/completion tokens
|
|
422
|
+
- `total_tokens` (Optional[int]): Total number of tokens
|
|
423
|
+
|
|
424
|
+
**Raises:**
|
|
425
|
+
SettlementServiceError: If the settlement service returns an error or
|
|
426
|
+
cannot parse the usage data.
|
|
427
|
+
|
|
428
|
+
**Example:**
|
|
429
|
+
```python
|
|
430
|
+
# OpenAI format
|
|
431
|
+
usage = await client.parse_usage({
|
|
432
|
+
"prompt_tokens": 100,
|
|
433
|
+
"completion_tokens": 50,
|
|
434
|
+
"total_tokens": 150
|
|
435
|
+
})
|
|
436
|
+
# Returns: {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}
|
|
437
|
+
|
|
438
|
+
# Nested format
|
|
439
|
+
usage = await client.parse_usage({
|
|
440
|
+
"response": "...",
|
|
441
|
+
"usage": {
|
|
442
|
+
"input_tokens": 100,
|
|
443
|
+
"output_tokens": 50
|
|
444
|
+
}
|
|
445
|
+
})
|
|
446
|
+
# Returns: {"input_tokens": 100, "output_tokens": 50, "total_tokens": 150}
|
|
447
|
+
```
|
|
48
448
|
"""
|
|
49
449
|
try:
|
|
50
450
|
async with httpx.AsyncClient(
|
|
@@ -57,15 +457,16 @@ class SettlementServiceClient:
|
|
|
57
457
|
response.raise_for_status()
|
|
58
458
|
return response.json()
|
|
59
459
|
except httpx.HTTPError as e:
|
|
60
|
-
|
|
61
|
-
f"Failed to parse usage via settlement service: {e}"
|
|
62
|
-
)
|
|
63
|
-
raise
|
|
460
|
+
raise self._handle_http_error(e, "parse_usage")
|
|
64
461
|
except Exception as e:
|
|
65
462
|
logger.error(
|
|
66
|
-
f"Unexpected error calling settlement service: {e}"
|
|
463
|
+
f"Unexpected error calling settlement service parse_usage: {e}",
|
|
464
|
+
exc_info=True,
|
|
465
|
+
)
|
|
466
|
+
raise SettlementServiceError(
|
|
467
|
+
message=f"Unexpected error during parse_usage: {str(e)}",
|
|
468
|
+
error_type="Unexpected error",
|
|
67
469
|
)
|
|
68
|
-
raise
|
|
69
470
|
|
|
70
471
|
async def calculate_payment(
|
|
71
472
|
self,
|
|
@@ -77,14 +478,61 @@ class SettlementServiceClient:
|
|
|
77
478
|
"""
|
|
78
479
|
Calculate payment amounts from usage data.
|
|
79
480
|
|
|
80
|
-
|
|
81
|
-
|
|
481
|
+
This method calculates payment amounts based on token usage and pricing rates.
|
|
482
|
+
It parses usage tokens, calculates USD costs, fetches current token prices,
|
|
483
|
+
and computes payment amounts in the specified token (SOL or USDC).
|
|
484
|
+
|
|
485
|
+
**Calculation Process:**
|
|
486
|
+
1. Parses usage tokens from the provided usage data
|
|
487
|
+
2. Calculates USD cost: (input_tokens / 1M) * input_rate + (output_tokens / 1M) * output_rate
|
|
488
|
+
3. Fetches current token price from price oracle
|
|
489
|
+
4. Converts USD cost to token amount
|
|
490
|
+
5. Calculates split: treasury fee (5% default) and agent amount (95% default)
|
|
491
|
+
|
|
492
|
+
**Args:**
|
|
493
|
+
usage: Usage data containing token counts. Supports same formats as
|
|
494
|
+
`parse_usage` method. Can be raw usage data or already parsed.
|
|
82
495
|
input_cost_per_million_usd: Cost per million input tokens in USD.
|
|
83
496
|
output_cost_per_million_usd: Cost per million output tokens in USD.
|
|
84
|
-
payment_token: Token to use for payment
|
|
497
|
+
payment_token: Token to use for payment. Must be "SOL" or "USDC".
|
|
498
|
+
Default: "SOL".
|
|
85
499
|
|
|
86
|
-
Returns
|
|
87
|
-
Dict with payment calculation details
|
|
500
|
+
**Returns:**
|
|
501
|
+
Dict with payment calculation details:
|
|
502
|
+
- `status` (str): "calculated" or "skipped" (if zero cost)
|
|
503
|
+
- `pricing` (dict): Pricing information with token counts and costs
|
|
504
|
+
- `payment_amounts` (dict, optional): Payment amounts in token units
|
|
505
|
+
- `token_price_usd` (float, optional): Current token price in USD
|
|
506
|
+
|
|
507
|
+
**Raises:**
|
|
508
|
+
SettlementServiceError: If the settlement service returns an error.
|
|
509
|
+
|
|
510
|
+
**Example:**
|
|
511
|
+
```python
|
|
512
|
+
result = await client.calculate_payment(
|
|
513
|
+
usage={"input_tokens": 1000, "output_tokens": 500},
|
|
514
|
+
input_cost_per_million_usd=10.0,
|
|
515
|
+
output_cost_per_million_usd=30.0,
|
|
516
|
+
payment_token="SOL"
|
|
517
|
+
)
|
|
518
|
+
# Returns:
|
|
519
|
+
# {
|
|
520
|
+
# "status": "calculated",
|
|
521
|
+
# "pricing": {
|
|
522
|
+
# "usd_cost": 0.025, # $0.01 input + $0.015 output
|
|
523
|
+
# "input_tokens": 1000,
|
|
524
|
+
# "output_tokens": 500,
|
|
525
|
+
# ...
|
|
526
|
+
# },
|
|
527
|
+
# "payment_amounts": {
|
|
528
|
+
# "total_amount_token": 0.00125, # SOL amount
|
|
529
|
+
# "fee_amount_token": 0.0000625, # Treasury fee
|
|
530
|
+
# "agent_amount_token": 0.0011875, # Agent payment
|
|
531
|
+
# ...
|
|
532
|
+
# },
|
|
533
|
+
# "token_price_usd": 20.0 # SOL price
|
|
534
|
+
# }
|
|
535
|
+
```
|
|
88
536
|
"""
|
|
89
537
|
try:
|
|
90
538
|
async with httpx.AsyncClient(
|
|
@@ -102,15 +550,16 @@ class SettlementServiceClient:
|
|
|
102
550
|
response.raise_for_status()
|
|
103
551
|
return response.json()
|
|
104
552
|
except httpx.HTTPError as e:
|
|
105
|
-
|
|
106
|
-
f"Failed to calculate payment via settlement service: {e}"
|
|
107
|
-
)
|
|
108
|
-
raise
|
|
553
|
+
raise self._handle_http_error(e, "calculate_payment")
|
|
109
554
|
except Exception as e:
|
|
110
555
|
logger.error(
|
|
111
|
-
f"Unexpected error calling settlement service: {e}"
|
|
556
|
+
f"Unexpected error calling settlement service calculate_payment: {e}",
|
|
557
|
+
exc_info=True,
|
|
558
|
+
)
|
|
559
|
+
raise SettlementServiceError(
|
|
560
|
+
message=f"Unexpected error during calculate_payment: {str(e)}",
|
|
561
|
+
error_type="Unexpected error",
|
|
112
562
|
)
|
|
113
|
-
raise
|
|
114
563
|
|
|
115
564
|
async def settle(
|
|
116
565
|
self,
|
|
@@ -120,26 +569,107 @@ class SettlementServiceClient:
|
|
|
120
569
|
output_cost_per_million_usd: float,
|
|
121
570
|
recipient_pubkey: str,
|
|
122
571
|
payment_token: str = "SOL",
|
|
123
|
-
treasury_pubkey: Optional[str] = None,
|
|
124
572
|
skip_preflight: bool = False,
|
|
125
573
|
commitment: str = "confirmed",
|
|
126
574
|
) -> Dict[str, Any]:
|
|
127
575
|
"""
|
|
128
|
-
Execute a settlement payment.
|
|
576
|
+
Execute a settlement payment on Solana blockchain.
|
|
129
577
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
578
|
+
This method performs a complete settlement flow: parses usage tokens, calculates
|
|
579
|
+
payment amounts, fetches token prices, creates and signs a split payment transaction
|
|
580
|
+
on Solana, sends the transaction, and waits for confirmation.
|
|
581
|
+
|
|
582
|
+
**Settlement Flow:**
|
|
583
|
+
1. Parses usage tokens from the provided usage data
|
|
584
|
+
2. Calculates payment amounts based on pricing rates
|
|
585
|
+
3. Fetches current token price (currently supports SOL only)
|
|
586
|
+
4. Creates a split payment transaction (treasury fee + recipient payment)
|
|
587
|
+
5. Signs the transaction with the provided private key
|
|
588
|
+
6. Sends the transaction to Solana network
|
|
589
|
+
7. Waits for confirmation at the specified commitment level
|
|
590
|
+
8. Returns transaction signature and payment details
|
|
591
|
+
|
|
592
|
+
**Payment Splitting:**
|
|
593
|
+
The payment is automatically split between:
|
|
594
|
+
- **Treasury**: Receives the processing fee (5% by default, configurable on service)
|
|
595
|
+
- **Recipient**: Receives the net payment amount (95% by default)
|
|
596
|
+
|
|
597
|
+
**Security Notes:**
|
|
598
|
+
- Private key is used only in-memory for transaction signing
|
|
599
|
+
- No key material is logged or persisted
|
|
600
|
+
- Transaction is executed on-chain with full transparency
|
|
601
|
+
- The treasury pubkey is configured on the settlement service and cannot be overridden
|
|
602
|
+
|
|
603
|
+
**Args:**
|
|
604
|
+
private_key: Solana wallet private key. Can be in JSON array format
|
|
605
|
+
(e.g., "[1,2,3,...64 bytes...]") or base58 encoded string.
|
|
606
|
+
Must be 32 or 64 bytes. WARNING: This is custodial-like behavior.
|
|
607
|
+
usage: Usage data containing token counts. Supports same formats as
|
|
608
|
+
`parse_usage` method. Can be raw usage data or already parsed.
|
|
133
609
|
input_cost_per_million_usd: Cost per million input tokens in USD.
|
|
134
610
|
output_cost_per_million_usd: Cost per million output tokens in USD.
|
|
135
|
-
recipient_pubkey: Solana public key of the recipient wallet.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
611
|
+
recipient_pubkey: Solana public key of the recipient wallet (base58 encoded).
|
|
612
|
+
This wallet receives the net payment after fees.
|
|
613
|
+
payment_token: Token to use for payment. Currently only "SOL" is supported
|
|
614
|
+
for automatic settlement. Default: "SOL".
|
|
615
|
+
skip_preflight: Whether to skip preflight simulation. Setting to True
|
|
616
|
+
can speed up transactions but may result in failed transactions.
|
|
617
|
+
Default: False.
|
|
618
|
+
commitment: Solana commitment level for transaction confirmation:
|
|
619
|
+
- "processed": Fastest, but may be rolled back
|
|
620
|
+
- "confirmed": Recommended default, confirmed by cluster
|
|
621
|
+
- "finalized": Slowest, but cannot be rolled back
|
|
622
|
+
Default: "confirmed".
|
|
140
623
|
|
|
141
|
-
Returns
|
|
142
|
-
Dict with payment details
|
|
624
|
+
**Returns:**
|
|
625
|
+
Dict with payment details:
|
|
626
|
+
- `status` (str): "paid" if successful, "skipped" if zero cost
|
|
627
|
+
- `transaction_signature` (str, optional): Solana transaction signature
|
|
628
|
+
- `pricing` (dict): Complete cost breakdown
|
|
629
|
+
- `payment` (dict, optional): Payment details including:
|
|
630
|
+
- `total_amount_lamports` (int): Total payment in lamports
|
|
631
|
+
- `total_amount_sol` (float): Total payment in SOL
|
|
632
|
+
- `total_amount_usd` (float): Total payment in USD
|
|
633
|
+
- `treasury` (dict): Treasury payment details
|
|
634
|
+
- `recipient` (dict): Recipient payment details
|
|
635
|
+
|
|
636
|
+
**Raises:**
|
|
637
|
+
SettlementServiceError: If the settlement service returns an error.
|
|
638
|
+
Common errors include:
|
|
639
|
+
- Invalid private key format
|
|
640
|
+
- Insufficient funds
|
|
641
|
+
- Network errors
|
|
642
|
+
- Transaction failures
|
|
643
|
+
|
|
644
|
+
**Example:**
|
|
645
|
+
```python
|
|
646
|
+
result = await client.settle(
|
|
647
|
+
private_key="[1,2,3,...]", # Wallet private key
|
|
648
|
+
usage={"input_tokens": 1000, "output_tokens": 500},
|
|
649
|
+
input_cost_per_million_usd=10.0,
|
|
650
|
+
output_cost_per_million_usd=30.0,
|
|
651
|
+
recipient_pubkey="RecipientPublicKeyHere",
|
|
652
|
+
payment_token="SOL",
|
|
653
|
+
commitment="confirmed"
|
|
654
|
+
)
|
|
655
|
+
# Returns:
|
|
656
|
+
# {
|
|
657
|
+
# "status": "paid",
|
|
658
|
+
# "transaction_signature": "5j7s8K9...",
|
|
659
|
+
# "pricing": {...},
|
|
660
|
+
# "payment": {
|
|
661
|
+
# "total_amount_sol": 0.00125,
|
|
662
|
+
# "treasury": {"amount_sol": 0.0000625, ...},
|
|
663
|
+
# "recipient": {"amount_sol": 0.0011875, ...}
|
|
664
|
+
# }
|
|
665
|
+
# }
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
**Note:**
|
|
669
|
+
The treasury_pubkey is automatically set from the SWARMS_TREASURY_PUBKEY
|
|
670
|
+
environment variable on the settlement service and cannot be overridden.
|
|
671
|
+
Settlement operations may take time due to blockchain confirmation. Increase
|
|
672
|
+
the client timeout if you experience timeout errors even when payments succeed.
|
|
143
673
|
"""
|
|
144
674
|
try:
|
|
145
675
|
async with httpx.AsyncClient(
|
|
@@ -155,8 +685,6 @@ class SettlementServiceClient:
|
|
|
155
685
|
"skip_preflight": skip_preflight,
|
|
156
686
|
"commitment": commitment,
|
|
157
687
|
}
|
|
158
|
-
if treasury_pubkey:
|
|
159
|
-
payload["treasury_pubkey"] = treasury_pubkey
|
|
160
688
|
|
|
161
689
|
response = await client.post(
|
|
162
690
|
f"{self.base_url}/v1/settlement/settle",
|
|
@@ -165,22 +693,45 @@ class SettlementServiceClient:
|
|
|
165
693
|
response.raise_for_status()
|
|
166
694
|
return response.json()
|
|
167
695
|
except httpx.HTTPError as e:
|
|
168
|
-
|
|
169
|
-
f"Failed to settle payment via settlement service: {e}"
|
|
170
|
-
)
|
|
171
|
-
raise
|
|
696
|
+
raise self._handle_http_error(e, "settle")
|
|
172
697
|
except Exception as e:
|
|
173
698
|
logger.error(
|
|
174
|
-
f"Unexpected error calling settlement service: {e}"
|
|
699
|
+
f"Unexpected error calling settlement service settle: {e}",
|
|
700
|
+
exc_info=True,
|
|
701
|
+
)
|
|
702
|
+
raise SettlementServiceError(
|
|
703
|
+
message=f"Unexpected error during settle: {str(e)}",
|
|
704
|
+
error_type="Unexpected error",
|
|
175
705
|
)
|
|
176
|
-
raise
|
|
177
706
|
|
|
178
707
|
async def health_check(self) -> Dict[str, Any]:
|
|
179
708
|
"""
|
|
180
709
|
Check if the settlement service is healthy.
|
|
181
710
|
|
|
182
|
-
|
|
183
|
-
|
|
711
|
+
This method calls the settlement service's health check endpoint to verify
|
|
712
|
+
that the service is running and responsive. Useful for monitoring and
|
|
713
|
+
connection testing.
|
|
714
|
+
|
|
715
|
+
**Returns:**
|
|
716
|
+
Dict with health status information, typically including:
|
|
717
|
+
- `status` (str): Service status (e.g., "healthy")
|
|
718
|
+
- `service` (str): Service name
|
|
719
|
+
- `version` (str): Service version
|
|
720
|
+
|
|
721
|
+
**Raises:**
|
|
722
|
+
SettlementServiceError: If the settlement service is unreachable or
|
|
723
|
+
returns an error. This indicates the service may be down or
|
|
724
|
+
experiencing issues.
|
|
725
|
+
|
|
726
|
+
**Example:**
|
|
727
|
+
```python
|
|
728
|
+
try:
|
|
729
|
+
health = await client.health_check()
|
|
730
|
+
print(f"Service status: {health['status']}")
|
|
731
|
+
# Output: Service status: healthy
|
|
732
|
+
except SettlementServiceError as e:
|
|
733
|
+
print(f"Service is down: {e}")
|
|
734
|
+
```
|
|
184
735
|
"""
|
|
185
736
|
try:
|
|
186
737
|
async with httpx.AsyncClient(
|
|
@@ -190,8 +741,13 @@ class SettlementServiceClient:
|
|
|
190
741
|
response.raise_for_status()
|
|
191
742
|
return response.json()
|
|
192
743
|
except httpx.HTTPError as e:
|
|
193
|
-
|
|
194
|
-
raise
|
|
744
|
+
raise self._handle_http_error(e, "health_check")
|
|
195
745
|
except Exception as e:
|
|
196
|
-
logger.error(
|
|
197
|
-
|
|
746
|
+
logger.error(
|
|
747
|
+
f"Unexpected error during health check: {e}",
|
|
748
|
+
exc_info=True,
|
|
749
|
+
)
|
|
750
|
+
raise SettlementServiceError(
|
|
751
|
+
message=f"Unexpected error during health_check: {str(e)}",
|
|
752
|
+
error_type="Unexpected error",
|
|
753
|
+
)
|