atp-protocol 1.2.0__py3-none-any.whl → 1.4.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/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
- """Client for ATP Settlement Service API."""
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 = 30.0,
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: 30.0).
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
- Args:
44
- usage_data: Usage data in any supported format.
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
- Returns:
47
- Dict with normalized keys: input_tokens, output_tokens, total_tokens
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
- logger.error(
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
- Args:
81
- usage: Usage data containing token counts.
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 (SOL or USDC).
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
- logger.error(
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
- Args:
131
- private_key: Solana wallet private key.
132
- usage: Usage data containing token counts.
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
- payment_token: Token to use for payment (SOL or USDC).
137
- treasury_pubkey: Treasury pubkey for processing fee (optional).
138
- skip_preflight: Whether to skip preflight simulation.
139
- commitment: Solana commitment level.
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 including transaction signature.
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
- logger.error(
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
- Returns:
183
- Dict with health status.
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
- logger.error(f"Health check failed: {e}")
194
- raise
744
+ raise self._handle_http_error(e, "health_check")
195
745
  except Exception as e:
196
- logger.error(f"Unexpected error during health check: {e}")
197
- raise
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
+ )