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/middleware.py CHANGED
@@ -23,8 +23,12 @@ from starlette.middleware.base import BaseHTTPMiddleware
23
23
  from starlette.types import ASGIApp
24
24
 
25
25
  from atp import config
26
+ from atp.encryption import ResponseEncryptor
26
27
  from atp.schemas import PaymentToken
27
- from atp.settlement_client import SettlementServiceClient
28
+ from atp.settlement_client import (
29
+ SettlementServiceClient,
30
+ SettlementServiceError,
31
+ )
28
32
 
29
33
 
30
34
  class ATPSettlementMiddleware(BaseHTTPMiddleware):
@@ -32,17 +36,96 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
32
36
  FastAPI middleware that automatically deducts payment from Solana wallets
33
37
  based on token usage for configured endpoints.
34
38
 
35
- The middleware delegates all settlement logic to the ATP Settlement Service,
36
- ensuring immutable and centralized settlement operations.
39
+ This middleware intercepts responses from specified endpoints, extracts usage
40
+ information (input/output tokens), calculates payment amounts, and executes
41
+ Solana blockchain transactions to deduct payment before returning the response
42
+ to the client.
37
43
 
38
- The middleware accepts wallet private keys directly via headers, making it
39
- simple to use. Users can add their own API key handling layer if needed.
44
+ **Architecture & Design:**
40
45
 
41
- Payments are split automatically:
42
- - Treasury (SWARMS_TREASURY_PUBKEY) receives the processing fee
43
- - Recipient (endpoint host) receives the remainder
46
+ The middleware delegates all settlement logic to the ATP Settlement Service,
47
+ ensuring immutable and centralized settlement operations. This design provides:
48
+ - Centralized parsing logic for multiple API formats (OpenAI, Anthropic, Google, etc.)
49
+ - Consistent payment calculation across all services
50
+ - Immutable settlement logic that cannot be modified by individual services
51
+ - Automatic handling of nested usage structures
52
+
53
+ **Request Flow:**
54
+
55
+ 1. Request arrives at a configured endpoint
56
+ 2. Middleware extracts wallet private key from request headers
57
+ 3. Request is forwarded to the endpoint handler
58
+ 4. Response is intercepted and parsed for usage data
59
+ 5. Response is encrypted to prevent unauthorized access
60
+ 6. Usage data is sent to settlement service for parsing and payment calculation
61
+ 7. Payment transaction is executed on Solana blockchain
62
+ 8. Response is decrypted only after payment confirmation
63
+ 9. Response is returned to client with settlement details
64
+
65
+ **Security Features:**
66
+
67
+ - **Response Encryption**: Agent responses are encrypted before payment verification,
68
+ ensuring users cannot see output until payment is confirmed on-chain.
69
+ - **Payment Verification**: Responses are only decrypted after successful blockchain
70
+ transaction confirmation (status="paid" with valid transaction signature).
71
+ - **Error Handling**: Failed payments result in encrypted responses with error details,
72
+ preventing unauthorized access to agent output.
73
+
74
+ **Payment Splitting:**
75
+
76
+ Payments are automatically split between:
77
+ - **Treasury**: Receives the processing fee (configured via SWARMS_TREASURY_PUBKEY
78
+ on settlement service). Default fee percentage is 5%.
79
+ - **Recipient**: Receives the remainder (95% by default). This is the endpoint host's
80
+ wallet specified via `recipient_pubkey`.
81
+
82
+ **Usage Parsing:**
83
+
84
+ The middleware sends the entire response body to the settlement service's
85
+ `/v1/settlement/parse-usage` endpoint, which automatically handles:
86
+ - Multiple API formats (OpenAI, Anthropic, Google/Gemini, Cohere, etc.)
87
+ - Nested structures (usage.usage, meta.usage, statistics, etc.)
88
+ - Recursive parsing for deeply nested usage objects
89
+ - Normalization to standard format (input_tokens, output_tokens, total_tokens)
90
+
91
+ **Error Handling:**
92
+
93
+ The middleware provides two error handling modes:
94
+ - **fail_on_settlement_error=False** (default): Returns encrypted response with
95
+ settlement error details. Useful for debugging and graceful degradation.
96
+ - **fail_on_settlement_error=True**: Raises HTTPException when settlement fails.
97
+ Useful for strict payment requirements.
98
+
99
+ **Response Modifications:**
100
+
101
+ The middleware adds the following fields to responses:
102
+ - `atp_usage`: Normalized usage data (input_tokens, output_tokens, total_tokens)
103
+ - `atp_settlement`: Settlement details including transaction signature and payment breakdown
104
+ - `atp_settlement_status`: Status of settlement ("paid", "failed", etc.)
105
+ - `atp_message`: Informational message about response encryption status
106
+
107
+ **Attributes:**
108
+
109
+ allowed_endpoints (Set[str]): Set of endpoint paths to apply settlement to.
110
+ input_cost_per_million_usd (float): Cost per million input tokens in USD.
111
+ output_cost_per_million_usd (float): Cost per million output tokens in USD.
112
+ wallet_private_key_header (str): HTTP header name for wallet private key.
113
+ payment_token (PaymentToken): Token to use for payment (SOL or USDC).
114
+ skip_preflight (bool): Whether to skip preflight simulation for Solana transactions.
115
+ commitment (str): Solana commitment level (processed|confirmed|finalized).
116
+ fail_on_settlement_error (bool): Whether to raise exception on settlement failure.
117
+ settlement_service_client (SettlementServiceClient): Client for settlement service API.
118
+ encryptor (ResponseEncryptor): Encryptor for protecting agent responses.
119
+
120
+ **Example Usage:**
121
+
122
+ ```python
123
+ from fastapi import FastAPI
124
+ from atp.middleware import ATPSettlementMiddleware
125
+ from atp.schemas import PaymentToken
126
+
127
+ app = FastAPI()
44
128
 
45
- Usage:
46
129
  app.add_middleware(
47
130
  ATPSettlementMiddleware,
48
131
  allowed_endpoints=["/v1/chat", "/v1/completions"],
@@ -50,9 +133,37 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
50
133
  output_cost_per_million_usd=30.0,
51
134
  wallet_private_key_header="x-wallet-private-key",
52
135
  payment_token=PaymentToken.SOL,
53
- recipient_pubkey="YourPublicKeyHere", # Required: endpoint host receives main payment
54
- # settlement_service_url is optional - uses ATP_SETTLEMENT_URL env var by default
136
+ recipient_pubkey="YourPublicKeyHere", # Required
137
+ settlement_service_url="https://facilitator.swarms.world", # Optional
138
+ settlement_timeout=300.0, # Optional, default 5 minutes
139
+ fail_on_settlement_error=False, # Optional, default False
55
140
  )
141
+
142
+ @app.post("/v1/chat")
143
+ async def chat(request: dict):
144
+ # Your endpoint logic here
145
+ # Response should include usage data in any supported format
146
+ return {
147
+ "response": "Hello!",
148
+ "usage": {
149
+ "prompt_tokens": 10,
150
+ "completion_tokens": 20,
151
+ "total_tokens": 30
152
+ }
153
+ }
154
+ ```
155
+
156
+ **Notes:**
157
+
158
+ - The middleware only processes successful responses (status_code < 400).
159
+ - If usage data cannot be parsed (no input_tokens, output_tokens, or total_tokens
160
+ in the response), the middleware raises HTTP 422 with an error message.
161
+ - Settlement operations may take time due to blockchain confirmation. Increase
162
+ `settlement_timeout` if you experience timeout errors even when payments succeed.
163
+ - The treasury pubkey is configured on the settlement service and cannot be
164
+ overridden by the middleware.
165
+ - Wallet private keys are passed directly via headers. For production, consider
166
+ adding an API key layer or using secure key management.
56
167
  """
57
168
 
58
169
  def __init__(
@@ -67,8 +178,9 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
67
178
  recipient_pubkey: Optional[str] = None,
68
179
  skip_preflight: bool = False,
69
180
  commitment: str = "confirmed",
70
- require_wallet: bool = True,
71
181
  settlement_service_url: Optional[str] = None,
182
+ fail_on_settlement_error: bool = False,
183
+ settlement_timeout: Optional[float] = None,
72
184
  ):
73
185
  """
74
186
  Initialize the ATP settlement middleware.
@@ -90,10 +202,15 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
90
202
  This wallet receives the main payment (after processing fee). Required.
91
203
  skip_preflight: Whether to skip preflight simulation for Solana transactions.
92
204
  commitment: Solana commitment level (processed|confirmed|finalized).
93
- require_wallet: Whether to require wallet private key (if False, skips settlement when missing).
94
205
  settlement_service_url: Base URL of the settlement service. If not provided, uses
95
206
  ATP_SETTLEMENT_URL environment variable (default: http://localhost:8001).
96
207
  The middleware always uses the settlement service for all settlement operations.
208
+ fail_on_settlement_error: If True, raises HTTPException when settlement fails (default: False).
209
+ If False, returns the response with settlement error info instead of failing the request.
210
+ settlement_timeout: Timeout in seconds for settlement service requests. User-configurable parameter.
211
+ Default: from ATP_SETTLEMENT_TIMEOUT env var or 300.0 (5 minutes). Settlement operations may
212
+ take longer due to blockchain confirmation times. Increase this value if you experience timeout
213
+ errors even when payments are successfully sent.
97
214
  """
98
215
  super().__init__(app)
99
216
  self.allowed_endpoints: Set[str] = set(allowed_endpoints)
@@ -107,45 +224,69 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
107
224
  self._recipient_pubkey = recipient_pubkey
108
225
  if not self._recipient_pubkey:
109
226
  raise ValueError("recipient_pubkey must be provided")
110
- # Treasury pubkey - always uses SWARMS_TREASURY_PUBKEY for processing fees
111
- self._treasury_pubkey = config.SWARMS_TREASURY_PUBKEY
112
- if not self._treasury_pubkey:
113
- raise ValueError(
114
- "SWARMS_TREASURY_PUBKEY must be set in configuration"
115
- )
227
+ # Note: Treasury pubkey is automatically set from SWARMS_TREASURY_PUBKEY
228
+ # environment variable on the settlement service and cannot be overridden
116
229
  self.skip_preflight = skip_preflight
117
230
  self.commitment = commitment
118
- self.require_wallet = require_wallet
231
+ self.fail_on_settlement_error = fail_on_settlement_error
119
232
  # Always use settlement service - initialize client with config value or provided URL
120
233
  service_url = (
121
234
  settlement_service_url or config.ATP_SETTLEMENT_URL
122
235
  )
123
236
  self.settlement_service_client = SettlementServiceClient(
124
- base_url=service_url
237
+ base_url=service_url,
238
+ timeout=settlement_timeout,
125
239
  )
240
+ # Initialize encryptor for protecting agent responses
241
+ self.encryptor = ResponseEncryptor()
126
242
 
127
243
  def _should_process(self, path: str) -> bool:
128
- """Check if the request path should be processed by this middleware."""
244
+ """
245
+ Check if the request path should be processed by this middleware.
246
+
247
+ Args:
248
+ path: The request URL path.
249
+
250
+ Returns:
251
+ True if the path is in the allowed endpoints set, False otherwise.
252
+ """
129
253
  return path in self.allowed_endpoints
130
254
 
131
255
  def _extract_wallet_private_key(
132
256
  self, request: Request
133
257
  ) -> Optional[str]:
134
- """Extract wallet private key from request headers."""
258
+ """
259
+ Extract wallet private key from request headers.
260
+
261
+ The private key should be provided in the header specified by
262
+ `wallet_private_key_header` (default: "x-wallet-private-key").
263
+ The key can be in JSON array format (e.g., "[1,2,3,...]") or
264
+ base58 string format.
265
+
266
+ Args:
267
+ request: The incoming HTTP request.
268
+
269
+ Returns:
270
+ The wallet private key string if found, None otherwise.
271
+ """
135
272
  return request.headers.get(self.wallet_private_key_header)
136
273
 
137
- async def _extract_usage_from_response(
274
+ async def _parse_usage_from_response(
138
275
  self, response_body: bytes
139
276
  ) -> Optional[Dict[str, Any]]:
140
277
  """
141
- Extract usage information from response body.
278
+ Parse usage information from response body using the settlement service.
279
+
280
+ Delegates all usage parsing logic to the settlement service's parse-usage
281
+ endpoint, which handles multiple formats and nested structures automatically.
282
+ This centralizes all parsing logic in the immutable settlement service.
142
283
 
143
- Automatically detects usage data using multiple strategies:
144
- 1. Check if the entire response contains usage-like keys
145
- 2. Try nested structures with common usage key names (usage, token_usage, etc.)
284
+ Args:
285
+ response_body: Raw response body bytes.
146
286
 
147
- The usage data is then sent to the settlement service for parsing,
148
- so we just need to extract the raw usage object.
287
+ Returns:
288
+ Parsed usage dict with normalized keys (input_tokens, output_tokens, total_tokens),
289
+ or None if parsing fails or no usage data is found.
149
290
  """
150
291
  try:
151
292
  body_str = response_body.decode("utf-8")
@@ -153,48 +294,19 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
153
294
  return None
154
295
  data = json.loads(body_str)
155
296
 
156
- # Strategy 1: Check if the entire response is usage-like
157
- if isinstance(data, dict):
158
- # Check for common usage keys at top level
159
- usage_keys = [
160
- "input_tokens",
161
- "output_tokens",
162
- "prompt_tokens",
163
- "completion_tokens",
164
- "total_tokens",
165
- "tokens",
166
- "promptTokenCount",
167
- "candidatesTokenCount",
168
- "totalTokenCount",
169
- ]
170
- if any(key in data for key in usage_keys):
171
- return data
172
-
173
- # Strategy 2: Try nested structures
174
- # Check for usage nested in common locations
175
- for nested_key in [
176
- "usage",
177
- "token_usage",
178
- "tokens",
179
- "statistics",
180
- "meta",
181
- ]:
182
- if nested_key in data and isinstance(
183
- data[nested_key], dict
184
- ):
185
- nested_usage = data[nested_key]
186
- # Check if it looks like usage data
187
- if any(
188
- key in nested_usage
189
- for key in [
190
- "input_tokens",
191
- "output_tokens",
192
- "prompt_tokens",
193
- "completion_tokens",
194
- "tokens",
195
- ]
196
- ):
197
- return nested_usage
297
+ # Send entire response body to settlement service for parsing
298
+ # The service handles all format detection and nested structure traversal
299
+ parsed_usage = await self.settlement_service_client.parse_usage(
300
+ usage_data=data
301
+ )
302
+
303
+ # Check if we got valid token counts
304
+ if (
305
+ parsed_usage.get("input_tokens") is not None
306
+ or parsed_usage.get("output_tokens") is not None
307
+ or parsed_usage.get("total_tokens") is not None
308
+ ):
309
+ return parsed_usage
198
310
 
199
311
  return None
200
312
  except (json.JSONDecodeError, UnicodeDecodeError) as e:
@@ -202,11 +314,79 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
202
314
  f"Failed to parse response body for usage: {e}"
203
315
  )
204
316
  return None
317
+ except SettlementServiceError as e:
318
+ # If settlement service can't parse usage, log and return None
319
+ logger.debug(
320
+ f"Settlement service could not parse usage from response: {e}"
321
+ )
322
+ return None
323
+ except Exception as e:
324
+ logger.debug(
325
+ f"Unexpected error parsing usage: {e}"
326
+ )
327
+ return None
328
+
329
+ def log_to_marketplace(self):
330
+ """
331
+ Log the request to the marketplace and make it discoverable.
332
+
333
+ This is a placeholder method for future marketplace integration.
334
+ Currently does nothing but can be extended to log requests to a
335
+ marketplace service for discovery and analytics.
336
+
337
+ Note: This method is not currently called by the middleware.
338
+ """
339
+ pass
205
340
 
206
341
  async def dispatch(
207
342
  self, request: Request, call_next: Callable
208
343
  ) -> Response:
209
- """Process the request and apply settlement if applicable."""
344
+ """
345
+ Process the request and apply settlement if applicable.
346
+
347
+ This is the main middleware entry point that intercepts requests and responses.
348
+ It handles the complete settlement flow including usage parsing, payment execution,
349
+ and response encryption/decryption.
350
+
351
+ **Flow:**
352
+
353
+ 1. Check if request path is in allowed endpoints
354
+ 2. Extract wallet private key from headers (if required)
355
+ 3. Forward request to endpoint handler
356
+ 4. Intercept response and parse usage data via settlement service
357
+ 5. Encrypt response to prevent unauthorized access
358
+ 6. Execute payment via settlement service
359
+ 7. Decrypt response only after payment confirmation
360
+ 8. Return response with settlement metadata
361
+
362
+ **Args:**
363
+ request: The incoming HTTP request.
364
+ call_next: Callable to invoke the next middleware/endpoint handler.
365
+
366
+ **Returns:**
367
+ Response with settlement metadata added. Response body is encrypted until
368
+ payment is confirmed. If payment fails, response remains encrypted with
369
+ error details.
370
+
371
+ **Raises:**
372
+ HTTPException: If wallet is required but missing (402 Payment Required), or if
373
+ `fail_on_settlement_error=True` and settlement fails.
374
+
375
+ **Response Modifications:**
376
+ - Adds `atp_usage` field with normalized token counts
377
+ - Adds `atp_settlement` field with payment details
378
+ - Adds `atp_settlement_status` field with payment status
379
+ - Adds `atp_message` field with encryption status message
380
+ - Removes `Content-Length` and `Content-Encoding` headers (recalculated)
381
+
382
+ **Error Scenarios:**
383
+ - Missing wallet (if required): Returns 402 Payment Required
384
+ - No usage data: Raises 422 with message that endpoint must output
385
+ input_tokens, output_tokens, or total_tokens (or equivalent usage fields)
386
+ - Encryption failure: Returns 500 with error (response not exposed)
387
+ - Settlement failure: Returns encrypted response with error details
388
+ (or raises exception if `fail_on_settlement_error=True`)
389
+ """
210
390
  path = request.url.path
211
391
 
212
392
  # Skip if not in allowed endpoints
@@ -215,14 +395,12 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
215
395
 
216
396
  # Extract wallet private key
217
397
  private_key = self._extract_wallet_private_key(request)
398
+
218
399
  if not private_key:
219
- if self.require_wallet:
220
- raise HTTPException(
221
- status_code=401,
222
- detail=f"Missing wallet private key in header: {self.wallet_private_key_header}",
223
- )
224
- # If wallet not required, skip settlement
225
- return await call_next(request)
400
+ raise HTTPException(
401
+ status_code=402,
402
+ detail="Payment required. Missing wallet private key in header. Please provide a valid wallet private key and ensure payment succeeds. The header should be x-wallet-private-key.",
403
+ )
226
404
 
227
405
  # Execute the endpoint
228
406
  response = await call_next(request)
@@ -231,26 +409,67 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
231
409
  if response.status_code >= 400:
232
410
  return response
233
411
 
234
- # Extract usage from response
412
+ # Parse usage from response using settlement service
235
413
  response_body = b""
236
414
  async for chunk in response.body_iterator:
237
415
  response_body += chunk
238
416
 
239
- usage = await self._extract_usage_from_response(response_body)
417
+ usage = await self._parse_usage_from_response(response_body)
240
418
 
241
419
  if not usage:
242
420
  logger.warning(
243
- f"No usage data found in response for {path}. Response keys: {list(json.loads(response_body.decode('utf-8')).keys()) if response_body else 'empty'}"
421
+ f"No usage data found in response for {path}. "
422
+ "Settlement service could not parse usage from response body."
423
+ )
424
+ raise HTTPException(
425
+ status_code=422,
426
+ detail=(
427
+ "Endpoint must include token usage in the response. "
428
+ "Response must contain at least one of: input_tokens, output_tokens, or total_tokens "
429
+ "(or equivalent fields such as prompt_tokens/completion_tokens in a usage object). "
430
+ f"No parseable usage data found for {path}."
431
+ ),
432
+ )
433
+
434
+ # Encrypt the agent response before payment verification
435
+ # This ensures users cannot see the output until payment is confirmed
436
+ try:
437
+ response_data = json.loads(response_body.decode("utf-8"))
438
+ # Encrypt sensitive output fields (output, response, result, message)
439
+ encrypted_response_data = self.encryptor.encrypt_response_data(
440
+ response_data
441
+ )
442
+ # Store original encrypted data for later decryption
443
+ original_encrypted_data = encrypted_response_data.copy()
444
+ except Exception as e:
445
+ logger.error(
446
+ f"Failed to encrypt response: {e}. "
447
+ "This is a security issue - cannot proceed without encryption.",
448
+ exc_info=True,
244
449
  )
245
- # Return original response if no usage found
450
+ # If encryption fails, we cannot securely proceed
451
+ # Return error without exposing agent output
452
+ error_response = {
453
+ "error": "Internal server error",
454
+ "message": "Failed to encrypt response. Please contact support.",
455
+ "atp_usage": usage,
456
+ }
457
+ new_headers = dict(response.headers)
458
+ new_headers.pop("content-length", None)
459
+ new_headers.pop("Content-Length", None)
460
+ new_headers.pop("content-encoding", None)
461
+ new_headers.pop("Content-Encoding", None)
246
462
  return Response(
247
- content=response_body,
248
- status_code=response.status_code,
249
- headers=dict(response.headers),
250
- media_type=response.media_type,
463
+ content=json.dumps(error_response).encode("utf-8"),
464
+ status_code=500,
465
+ headers=new_headers,
466
+ media_type="application/json",
251
467
  )
252
468
 
253
469
  # Calculate and deduct payment via settlement service
470
+ payment_result = None
471
+ settlement_error = None
472
+
254
473
  try:
255
474
  payment_result = await self.settlement_service_client.settle(
256
475
  private_key=private_key,
@@ -259,38 +478,156 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
259
478
  output_cost_per_million_usd=self.output_cost_per_million_usd,
260
479
  recipient_pubkey=self._recipient_pubkey,
261
480
  payment_token=self.payment_token.value,
262
- treasury_pubkey=self._treasury_pubkey,
263
481
  skip_preflight=self.skip_preflight,
264
482
  commitment=self.commitment,
265
483
  )
484
+ except SettlementServiceError as e:
485
+ # Handle settlement service errors with detailed information
486
+ # The error already contains extracted details from the service response
487
+ error_dict = e.to_dict()
488
+
489
+ # Determine if this is a client error (4xx) or server error (5xx)
490
+ is_client_error = e.status_code and 400 <= e.status_code < 500
491
+
492
+ if self.fail_on_settlement_error:
493
+ # Raise HTTPException with appropriate status code
494
+ status_code = e.status_code or 500
495
+ detail = e.error_detail or str(e)
496
+ raise HTTPException(
497
+ status_code=status_code,
498
+ detail=detail,
499
+ )
500
+
501
+ # Store error info to include in response
502
+ settlement_error = error_dict.copy()
503
+ settlement_error["type"] = e.error_type or error_dict.get("type", "Settlement error")
504
+
505
+ # Log with appropriate level based on error type
506
+ if is_client_error:
507
+ logger.warning(
508
+ f"Settlement failed (client error {e.status_code}): {e.error_detail or str(e)}"
509
+ )
510
+ else:
511
+ logger.error(
512
+ f"Settlement failed (server error {e.status_code or 'unknown'}): {e.error_detail or str(e)}"
513
+ )
266
514
  except HTTPException:
267
- raise
515
+ # Re-raise HTTPExceptions (these are intentional errors like 401, 403, etc.)
516
+ if self.fail_on_settlement_error:
517
+ raise
518
+ settlement_error = {
519
+ "error": "Settlement failed",
520
+ "status_code": 500,
521
+ "detail": "Settlement service returned an error",
522
+ }
523
+ logger.warning("Settlement failed with HTTPException, but continuing with response")
268
524
  except Exception as e:
269
- logger.error(f"Settlement error: {e}", exc_info=True)
270
- raise HTTPException(
271
- status_code=500,
272
- detail=f"Settlement failed: {str(e)}",
525
+ # Handle unexpected errors
526
+ logger.error(f"Unexpected settlement error: {e}", exc_info=True)
527
+ if self.fail_on_settlement_error:
528
+ raise HTTPException(
529
+ status_code=500,
530
+ detail=f"Settlement failed: {str(e)}",
531
+ )
532
+ # Store error info to include in response
533
+ settlement_error = {
534
+ "error": "Settlement failed",
535
+ "message": str(e),
536
+ "type": type(e).__name__,
537
+ }
538
+ logger.warning(
539
+ f"Settlement failed but continuing with response: {e}"
273
540
  )
274
541
 
275
- # Always include settlement information in the response
542
+ # Process payment result and decrypt response only if payment succeeded
276
543
  try:
277
- response_data = json.loads(
278
- response_body.decode("utf-8")
279
- )
280
- response_data["atp_settlement"] = payment_result
281
- response_data["atp_usage"] = usage
282
- response_body = json.dumps(response_data).encode(
283
- "utf-8"
284
- )
544
+ # Start with the encrypted response data
545
+ final_response_data = original_encrypted_data.copy()
546
+ final_response_data["atp_usage"] = usage
547
+
548
+ # Check if payment was successful
549
+ payment_succeeded = False
550
+ if payment_result:
551
+ # Check if payment status is "paid"
552
+ payment_status = payment_result.get("status", "").lower()
553
+ # Also check for transaction signature as additional confirmation
554
+ has_transaction = bool(
555
+ payment_result.get("transaction_signature")
556
+ )
557
+
558
+ if payment_status == "paid" and has_transaction:
559
+ payment_succeeded = True
560
+ # Decrypt the response now that payment is confirmed
561
+ final_response_data = self.encryptor.decrypt_response_data(
562
+ final_response_data
563
+ )
564
+ logger.info(
565
+ f"Payment confirmed (tx: {payment_result.get('transaction_signature', 'N/A')[:16]}...), "
566
+ "response decrypted"
567
+ )
568
+ else:
569
+ logger.warning(
570
+ f"Payment not confirmed. Status: '{payment_status}', "
571
+ f"Has transaction: {has_transaction}. "
572
+ "Response will remain encrypted."
573
+ )
574
+ final_response_data["atp_settlement"] = payment_result
575
+ elif settlement_error:
576
+ # Payment failed - keep response encrypted
577
+ final_response_data["atp_settlement"] = settlement_error
578
+ final_response_data["atp_settlement_status"] = "failed"
579
+ logger.warning(
580
+ "Payment failed, response remains encrypted. "
581
+ "User cannot see agent output."
582
+ )
583
+
584
+ # If payment didn't succeed, add a message indicating the response is encrypted
585
+ if not payment_succeeded:
586
+ final_response_data["atp_message"] = (
587
+ "Agent response is encrypted. Payment required to decrypt. "
588
+ "Please provide a valid wallet private key and ensure payment succeeds."
589
+ )
590
+
591
+ response_body = json.dumps(final_response_data).encode("utf-8")
285
592
  except Exception as e:
286
- logger.warning(
287
- f"Failed to add settlement info to response: {e}"
593
+ logger.error(
594
+ f"Failed to process payment and decrypt response: {e}",
595
+ exc_info=True,
288
596
  )
597
+ # On error, return encrypted response with error info
598
+ try:
599
+ error_response = original_encrypted_data.copy()
600
+ error_response["atp_usage"] = usage
601
+ error_response["atp_settlement_error"] = {
602
+ "error": "Failed to process payment",
603
+ "message": str(e),
604
+ }
605
+ error_response["atp_message"] = (
606
+ "Agent response is encrypted. Payment processing failed."
607
+ )
608
+ response_body = json.dumps(error_response).encode("utf-8")
609
+ except Exception as e2:
610
+ logger.error(
611
+ f"Failed to create error response: {e2}", exc_info=True
612
+ )
613
+ # Last resort: return original encrypted response
614
+ response_body = json.dumps(original_encrypted_data).encode(
615
+ "utf-8"
616
+ )
617
+
618
+ # Create new headers without Content-Length since we modified the body
619
+ # Starlette/FastAPI will recalculate it automatically
620
+ new_headers = dict(response.headers)
621
+ # Remove Content-Length and Content-Encoding headers as they're no longer valid
622
+ new_headers.pop("content-length", None)
623
+ new_headers.pop("Content-Length", None)
624
+ new_headers.pop("content-encoding", None)
625
+ new_headers.pop("Content-Encoding", None)
289
626
 
290
627
  return Response(
291
628
  content=response_body,
292
629
  status_code=response.status_code,
293
- headers=dict(response.headers),
630
+ headers=new_headers,
294
631
  media_type=response.media_type,
295
632
  )
296
633
 
@@ -310,7 +647,7 @@ def create_settlement_middleware(
310
647
  input_cost_per_million_usd=10.0,
311
648
  output_cost_per_million_usd=30.0,
312
649
  wallet_private_key_header="x-wallet-private-key",
313
- recipient_pubkey="YourPublicKeyHere", # Optional: defaults to SWARMS_TREASURY_PUBKEY
650
+ recipient_pubkey="YourPublicKeyHere", # Required: recipient wallet public key
314
651
  )
315
652
  app.add_middleware(middleware)
316
653
  """