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/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,36 @@ 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, the original response is returned without settlement.
160
+ - Settlement operations may take time due to blockchain confirmation. Increase
161
+ `settlement_timeout` if you experience timeout errors even when payments succeed.
162
+ - The treasury pubkey is configured on the settlement service and cannot be
163
+ overridden by the middleware.
164
+ - Wallet private keys are passed directly via headers. For production, consider
165
+ adding an API key layer or using secure key management.
56
166
  """
57
167
 
58
168
  def __init__(
@@ -67,8 +177,9 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
67
177
  recipient_pubkey: Optional[str] = None,
68
178
  skip_preflight: bool = False,
69
179
  commitment: str = "confirmed",
70
- require_wallet: bool = True,
71
180
  settlement_service_url: Optional[str] = None,
181
+ fail_on_settlement_error: bool = False,
182
+ settlement_timeout: Optional[float] = None,
72
183
  ):
73
184
  """
74
185
  Initialize the ATP settlement middleware.
@@ -90,10 +201,15 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
90
201
  This wallet receives the main payment (after processing fee). Required.
91
202
  skip_preflight: Whether to skip preflight simulation for Solana transactions.
92
203
  commitment: Solana commitment level (processed|confirmed|finalized).
93
- require_wallet: Whether to require wallet private key (if False, skips settlement when missing).
94
204
  settlement_service_url: Base URL of the settlement service. If not provided, uses
95
205
  ATP_SETTLEMENT_URL environment variable (default: http://localhost:8001).
96
206
  The middleware always uses the settlement service for all settlement operations.
207
+ fail_on_settlement_error: If True, raises HTTPException when settlement fails (default: False).
208
+ If False, returns the response with settlement error info instead of failing the request.
209
+ settlement_timeout: Timeout in seconds for settlement service requests. User-configurable parameter.
210
+ Default: from ATP_SETTLEMENT_TIMEOUT env var or 300.0 (5 minutes). Settlement operations may
211
+ take longer due to blockchain confirmation times. Increase this value if you experience timeout
212
+ errors even when payments are successfully sent.
97
213
  """
98
214
  super().__init__(app)
99
215
  self.allowed_endpoints: Set[str] = set(allowed_endpoints)
@@ -107,45 +223,69 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
107
223
  self._recipient_pubkey = recipient_pubkey
108
224
  if not self._recipient_pubkey:
109
225
  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
- )
226
+ # Note: Treasury pubkey is automatically set from SWARMS_TREASURY_PUBKEY
227
+ # environment variable on the settlement service and cannot be overridden
116
228
  self.skip_preflight = skip_preflight
117
229
  self.commitment = commitment
118
- self.require_wallet = require_wallet
230
+ self.fail_on_settlement_error = fail_on_settlement_error
119
231
  # Always use settlement service - initialize client with config value or provided URL
120
232
  service_url = (
121
233
  settlement_service_url or config.ATP_SETTLEMENT_URL
122
234
  )
123
235
  self.settlement_service_client = SettlementServiceClient(
124
- base_url=service_url
236
+ base_url=service_url,
237
+ timeout=settlement_timeout,
125
238
  )
239
+ # Initialize encryptor for protecting agent responses
240
+ self.encryptor = ResponseEncryptor()
126
241
 
127
242
  def _should_process(self, path: str) -> bool:
128
- """Check if the request path should be processed by this middleware."""
243
+ """
244
+ Check if the request path should be processed by this middleware.
245
+
246
+ Args:
247
+ path: The request URL path.
248
+
249
+ Returns:
250
+ True if the path is in the allowed endpoints set, False otherwise.
251
+ """
129
252
  return path in self.allowed_endpoints
130
253
 
131
254
  def _extract_wallet_private_key(
132
255
  self, request: Request
133
256
  ) -> Optional[str]:
134
- """Extract wallet private key from request headers."""
257
+ """
258
+ Extract wallet private key from request headers.
259
+
260
+ The private key should be provided in the header specified by
261
+ `wallet_private_key_header` (default: "x-wallet-private-key").
262
+ The key can be in JSON array format (e.g., "[1,2,3,...]") or
263
+ base58 string format.
264
+
265
+ Args:
266
+ request: The incoming HTTP request.
267
+
268
+ Returns:
269
+ The wallet private key string if found, None otherwise.
270
+ """
135
271
  return request.headers.get(self.wallet_private_key_header)
136
272
 
137
- async def _extract_usage_from_response(
273
+ async def _parse_usage_from_response(
138
274
  self, response_body: bytes
139
275
  ) -> Optional[Dict[str, Any]]:
140
276
  """
141
- Extract usage information from response body.
277
+ Parse usage information from response body using the settlement service.
278
+
279
+ Delegates all usage parsing logic to the settlement service's parse-usage
280
+ endpoint, which handles multiple formats and nested structures automatically.
281
+ This centralizes all parsing logic in the immutable settlement service.
142
282
 
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.)
283
+ Args:
284
+ response_body: Raw response body bytes.
146
285
 
147
- The usage data is then sent to the settlement service for parsing,
148
- so we just need to extract the raw usage object.
286
+ Returns:
287
+ Parsed usage dict with normalized keys (input_tokens, output_tokens, total_tokens),
288
+ or None if parsing fails or no usage data is found.
149
289
  """
150
290
  try:
151
291
  body_str = response_body.decode("utf-8")
@@ -153,48 +293,19 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
153
293
  return None
154
294
  data = json.loads(body_str)
155
295
 
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
296
+ # Send entire response body to settlement service for parsing
297
+ # The service handles all format detection and nested structure traversal
298
+ parsed_usage = await self.settlement_service_client.parse_usage(
299
+ usage_data=data
300
+ )
301
+
302
+ # Check if we got valid token counts
303
+ if (
304
+ parsed_usage.get("input_tokens") is not None
305
+ or parsed_usage.get("output_tokens") is not None
306
+ or parsed_usage.get("total_tokens") is not None
307
+ ):
308
+ return parsed_usage
198
309
 
199
310
  return None
200
311
  except (json.JSONDecodeError, UnicodeDecodeError) as e:
@@ -202,11 +313,78 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
202
313
  f"Failed to parse response body for usage: {e}"
203
314
  )
204
315
  return None
316
+ except SettlementServiceError as e:
317
+ # If settlement service can't parse usage, log and return None
318
+ logger.debug(
319
+ f"Settlement service could not parse usage from response: {e}"
320
+ )
321
+ return None
322
+ except Exception as e:
323
+ logger.debug(
324
+ f"Unexpected error parsing usage: {e}"
325
+ )
326
+ return None
327
+
328
+ def log_to_marketplace(self):
329
+ """
330
+ Log the request to the marketplace and make it discoverable.
331
+
332
+ This is a placeholder method for future marketplace integration.
333
+ Currently does nothing but can be extended to log requests to a
334
+ marketplace service for discovery and analytics.
335
+
336
+ Note: This method is not currently called by the middleware.
337
+ """
338
+ pass
205
339
 
206
340
  async def dispatch(
207
341
  self, request: Request, call_next: Callable
208
342
  ) -> Response:
209
- """Process the request and apply settlement if applicable."""
343
+ """
344
+ Process the request and apply settlement if applicable.
345
+
346
+ This is the main middleware entry point that intercepts requests and responses.
347
+ It handles the complete settlement flow including usage parsing, payment execution,
348
+ and response encryption/decryption.
349
+
350
+ **Flow:**
351
+
352
+ 1. Check if request path is in allowed endpoints
353
+ 2. Extract wallet private key from headers (if required)
354
+ 3. Forward request to endpoint handler
355
+ 4. Intercept response and parse usage data via settlement service
356
+ 5. Encrypt response to prevent unauthorized access
357
+ 6. Execute payment via settlement service
358
+ 7. Decrypt response only after payment confirmation
359
+ 8. Return response with settlement metadata
360
+
361
+ **Args:**
362
+ request: The incoming HTTP request.
363
+ call_next: Callable to invoke the next middleware/endpoint handler.
364
+
365
+ **Returns:**
366
+ Response with settlement metadata added. Response body is encrypted until
367
+ payment is confirmed. If payment fails, response remains encrypted with
368
+ error details.
369
+
370
+ **Raises:**
371
+ HTTPException: If wallet is required but missing (402 Payment Required), or if
372
+ `fail_on_settlement_error=True` and settlement fails.
373
+
374
+ **Response Modifications:**
375
+ - Adds `atp_usage` field with normalized token counts
376
+ - Adds `atp_settlement` field with payment details
377
+ - Adds `atp_settlement_status` field with payment status
378
+ - Adds `atp_message` field with encryption status message
379
+ - Removes `Content-Length` and `Content-Encoding` headers (recalculated)
380
+
381
+ **Error Scenarios:**
382
+ - Missing wallet (if required): Returns 402 Payment Required
383
+ - No usage data: Returns original response without settlement
384
+ - Encryption failure: Returns 500 with error (response not exposed)
385
+ - Settlement failure: Returns encrypted response with error details
386
+ (or raises exception if `fail_on_settlement_error=True`)
387
+ """
210
388
  path = request.url.path
211
389
 
212
390
  # Skip if not in allowed endpoints
@@ -215,14 +393,12 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
215
393
 
216
394
  # Extract wallet private key
217
395
  private_key = self._extract_wallet_private_key(request)
396
+
218
397
  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)
398
+ raise HTTPException(
399
+ status_code=402,
400
+ 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.",
401
+ )
226
402
 
227
403
  # Execute the endpoint
228
404
  response = await call_next(request)
@@ -231,26 +407,71 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
231
407
  if response.status_code >= 400:
232
408
  return response
233
409
 
234
- # Extract usage from response
410
+ # Parse usage from response using settlement service
235
411
  response_body = b""
236
412
  async for chunk in response.body_iterator:
237
413
  response_body += chunk
238
414
 
239
- usage = await self._extract_usage_from_response(response_body)
415
+ usage = await self._parse_usage_from_response(response_body)
240
416
 
241
417
  if not usage:
242
418
  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'}"
419
+ f"No usage data found in response for {path}. "
420
+ "Settlement service could not parse usage from response body."
244
421
  )
245
422
  # Return original response if no usage found
423
+ # Remove Content-Length header since we consumed the body iterator
424
+ new_headers = dict(response.headers)
425
+ new_headers.pop("content-length", None)
426
+ new_headers.pop("Content-Length", None)
427
+ new_headers.pop("content-encoding", None)
428
+ new_headers.pop("Content-Encoding", None)
246
429
  return Response(
247
430
  content=response_body,
248
431
  status_code=response.status_code,
249
- headers=dict(response.headers),
432
+ headers=new_headers,
250
433
  media_type=response.media_type,
251
434
  )
252
435
 
436
+ # Encrypt the agent response before payment verification
437
+ # This ensures users cannot see the output until payment is confirmed
438
+ try:
439
+ response_data = json.loads(response_body.decode("utf-8"))
440
+ # Encrypt sensitive output fields (output, response, result, message)
441
+ encrypted_response_data = self.encryptor.encrypt_response_data(
442
+ response_data
443
+ )
444
+ # Store original encrypted data for later decryption
445
+ original_encrypted_data = encrypted_response_data.copy()
446
+ except Exception as e:
447
+ logger.error(
448
+ f"Failed to encrypt response: {e}. "
449
+ "This is a security issue - cannot proceed without encryption.",
450
+ exc_info=True,
451
+ )
452
+ # If encryption fails, we cannot securely proceed
453
+ # Return error without exposing agent output
454
+ error_response = {
455
+ "error": "Internal server error",
456
+ "message": "Failed to encrypt response. Please contact support.",
457
+ "atp_usage": usage,
458
+ }
459
+ new_headers = dict(response.headers)
460
+ new_headers.pop("content-length", None)
461
+ new_headers.pop("Content-Length", None)
462
+ new_headers.pop("content-encoding", None)
463
+ new_headers.pop("Content-Encoding", None)
464
+ return Response(
465
+ content=json.dumps(error_response).encode("utf-8"),
466
+ status_code=500,
467
+ headers=new_headers,
468
+ media_type="application/json",
469
+ )
470
+
253
471
  # Calculate and deduct payment via settlement service
472
+ payment_result = None
473
+ settlement_error = None
474
+
254
475
  try:
255
476
  payment_result = await self.settlement_service_client.settle(
256
477
  private_key=private_key,
@@ -259,38 +480,156 @@ class ATPSettlementMiddleware(BaseHTTPMiddleware):
259
480
  output_cost_per_million_usd=self.output_cost_per_million_usd,
260
481
  recipient_pubkey=self._recipient_pubkey,
261
482
  payment_token=self.payment_token.value,
262
- treasury_pubkey=self._treasury_pubkey,
263
483
  skip_preflight=self.skip_preflight,
264
484
  commitment=self.commitment,
265
485
  )
486
+ except SettlementServiceError as e:
487
+ # Handle settlement service errors with detailed information
488
+ # The error already contains extracted details from the service response
489
+ error_dict = e.to_dict()
490
+
491
+ # Determine if this is a client error (4xx) or server error (5xx)
492
+ is_client_error = e.status_code and 400 <= e.status_code < 500
493
+
494
+ if self.fail_on_settlement_error:
495
+ # Raise HTTPException with appropriate status code
496
+ status_code = e.status_code or 500
497
+ detail = e.error_detail or str(e)
498
+ raise HTTPException(
499
+ status_code=status_code,
500
+ detail=detail,
501
+ )
502
+
503
+ # Store error info to include in response
504
+ settlement_error = error_dict.copy()
505
+ settlement_error["type"] = e.error_type or error_dict.get("type", "Settlement error")
506
+
507
+ # Log with appropriate level based on error type
508
+ if is_client_error:
509
+ logger.warning(
510
+ f"Settlement failed (client error {e.status_code}): {e.error_detail or str(e)}"
511
+ )
512
+ else:
513
+ logger.error(
514
+ f"Settlement failed (server error {e.status_code or 'unknown'}): {e.error_detail or str(e)}"
515
+ )
266
516
  except HTTPException:
267
- raise
517
+ # Re-raise HTTPExceptions (these are intentional errors like 401, 403, etc.)
518
+ if self.fail_on_settlement_error:
519
+ raise
520
+ settlement_error = {
521
+ "error": "Settlement failed",
522
+ "status_code": 500,
523
+ "detail": "Settlement service returned an error",
524
+ }
525
+ logger.warning("Settlement failed with HTTPException, but continuing with response")
268
526
  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)}",
527
+ # Handle unexpected errors
528
+ logger.error(f"Unexpected settlement error: {e}", exc_info=True)
529
+ if self.fail_on_settlement_error:
530
+ raise HTTPException(
531
+ status_code=500,
532
+ detail=f"Settlement failed: {str(e)}",
533
+ )
534
+ # Store error info to include in response
535
+ settlement_error = {
536
+ "error": "Settlement failed",
537
+ "message": str(e),
538
+ "type": type(e).__name__,
539
+ }
540
+ logger.warning(
541
+ f"Settlement failed but continuing with response: {e}"
273
542
  )
274
543
 
275
- # Always include settlement information in the response
544
+ # Process payment result and decrypt response only if payment succeeded
276
545
  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
- )
546
+ # Start with the encrypted response data
547
+ final_response_data = original_encrypted_data.copy()
548
+ final_response_data["atp_usage"] = usage
549
+
550
+ # Check if payment was successful
551
+ payment_succeeded = False
552
+ if payment_result:
553
+ # Check if payment status is "paid"
554
+ payment_status = payment_result.get("status", "").lower()
555
+ # Also check for transaction signature as additional confirmation
556
+ has_transaction = bool(
557
+ payment_result.get("transaction_signature")
558
+ )
559
+
560
+ if payment_status == "paid" and has_transaction:
561
+ payment_succeeded = True
562
+ # Decrypt the response now that payment is confirmed
563
+ final_response_data = self.encryptor.decrypt_response_data(
564
+ final_response_data
565
+ )
566
+ logger.info(
567
+ f"Payment confirmed (tx: {payment_result.get('transaction_signature', 'N/A')[:16]}...), "
568
+ "response decrypted"
569
+ )
570
+ else:
571
+ logger.warning(
572
+ f"Payment not confirmed. Status: '{payment_status}', "
573
+ f"Has transaction: {has_transaction}. "
574
+ "Response will remain encrypted."
575
+ )
576
+ final_response_data["atp_settlement"] = payment_result
577
+ elif settlement_error:
578
+ # Payment failed - keep response encrypted
579
+ final_response_data["atp_settlement"] = settlement_error
580
+ final_response_data["atp_settlement_status"] = "failed"
581
+ logger.warning(
582
+ "Payment failed, response remains encrypted. "
583
+ "User cannot see agent output."
584
+ )
585
+
586
+ # If payment didn't succeed, add a message indicating the response is encrypted
587
+ if not payment_succeeded:
588
+ final_response_data["atp_message"] = (
589
+ "Agent response is encrypted. Payment required to decrypt. "
590
+ "Please provide a valid wallet private key and ensure payment succeeds."
591
+ )
592
+
593
+ response_body = json.dumps(final_response_data).encode("utf-8")
285
594
  except Exception as e:
286
- logger.warning(
287
- f"Failed to add settlement info to response: {e}"
595
+ logger.error(
596
+ f"Failed to process payment and decrypt response: {e}",
597
+ exc_info=True,
288
598
  )
599
+ # On error, return encrypted response with error info
600
+ try:
601
+ error_response = original_encrypted_data.copy()
602
+ error_response["atp_usage"] = usage
603
+ error_response["atp_settlement_error"] = {
604
+ "error": "Failed to process payment",
605
+ "message": str(e),
606
+ }
607
+ error_response["atp_message"] = (
608
+ "Agent response is encrypted. Payment processing failed."
609
+ )
610
+ response_body = json.dumps(error_response).encode("utf-8")
611
+ except Exception as e2:
612
+ logger.error(
613
+ f"Failed to create error response: {e2}", exc_info=True
614
+ )
615
+ # Last resort: return original encrypted response
616
+ response_body = json.dumps(original_encrypted_data).encode(
617
+ "utf-8"
618
+ )
619
+
620
+ # Create new headers without Content-Length since we modified the body
621
+ # Starlette/FastAPI will recalculate it automatically
622
+ new_headers = dict(response.headers)
623
+ # Remove Content-Length and Content-Encoding headers as they're no longer valid
624
+ new_headers.pop("content-length", None)
625
+ new_headers.pop("Content-Length", None)
626
+ new_headers.pop("content-encoding", None)
627
+ new_headers.pop("Content-Encoding", None)
289
628
 
290
629
  return Response(
291
630
  content=response_body,
292
631
  status_code=response.status_code,
293
- headers=dict(response.headers),
632
+ headers=new_headers,
294
633
  media_type=response.media_type,
295
634
  )
296
635
 
@@ -310,7 +649,7 @@ def create_settlement_middleware(
310
649
  input_cost_per_million_usd=10.0,
311
650
  output_cost_per_million_usd=30.0,
312
651
  wallet_private_key_header="x-wallet-private-key",
313
- recipient_pubkey="YourPublicKeyHere", # Optional: defaults to SWARMS_TREASURY_PUBKEY
652
+ recipient_pubkey="YourPublicKeyHere", # Required: recipient wallet public key
314
653
  )
315
654
  app.add_middleware(middleware)
316
655
  """