atp-protocol 1.0.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 ADDED
@@ -0,0 +1,11 @@
1
+ """ATP Protocol package."""
2
+
3
+ from atp.middleware import (
4
+ ATPSettlementMiddleware,
5
+ create_settlement_middleware,
6
+ )
7
+
8
+ __all__ = [
9
+ "ATPSettlementMiddleware",
10
+ "create_settlement_middleware",
11
+ ]
atp/config.py ADDED
@@ -0,0 +1,77 @@
1
+ """
2
+ Central configuration for the ATP Gateway.
3
+
4
+ Keep all env parsing + constants here so the rest of the code can be imported cleanly.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from typing import Optional
11
+
12
+ from dotenv import load_dotenv
13
+
14
+ load_dotenv()
15
+
16
+
17
+ def _bool_env(name: str, default: bool = False) -> bool:
18
+ v = os.getenv(name)
19
+ if v is None:
20
+ return default
21
+ return v.strip().lower() in {"1", "true", "yes", "y", "on"}
22
+
23
+
24
+ def _float_env(name: str) -> Optional[float]:
25
+ v = os.getenv(name)
26
+ if v is None:
27
+ return None
28
+ v = v.strip()
29
+ if not v:
30
+ return None
31
+ return float(v)
32
+
33
+
34
+ SWARMS_API_KEY = os.getenv("SWARMS_API_KEY")
35
+ SWARMS_API_URL = os.getenv(
36
+ "SWARMS_API_URL", "https://api.swarms.world/v1/agent/completions"
37
+ )
38
+
39
+ SOLANA_RPC_URL = os.getenv(
40
+ "SOLANA_RPC_URL", "https://api.mainnet-beta.solana.com"
41
+ )
42
+ AGENT_TREASURY_PUBKEY = os.getenv("AGENT_TREASURY_PUBKEY")
43
+
44
+ JOB_TTL_SECONDS = int(os.getenv("JOB_TTL_SECONDS", "600"))
45
+
46
+ # Swarms Treasury for settlement fees
47
+ SWARMS_TREASURY_PUBKEY = os.getenv(
48
+ "SWARMS_TREASURY_PUBKEY",
49
+ "7MaX4muAn8ZQREJxnupm8sgokwFHujgrGfH9Qn81BuEV",
50
+ )
51
+ SETTLEMENT_FEE_PERCENT = float(
52
+ os.getenv("SETTLEMENT_FEE_PERCENT", "0.05")
53
+ )
54
+
55
+ # USDC Token Configuration (Solana Mainnet)
56
+ USDC_MINT_ADDRESS = os.getenv(
57
+ "USDC_MINT_ADDRESS",
58
+ "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
59
+ )
60
+ USDC_DECIMALS = int(os.getenv("USDC_DECIMALS", "6"))
61
+
62
+ SUPPORTED_PAYMENT_TOKENS = ["SOL", "USDC"]
63
+
64
+ # Agent pricing configuration (host-provided)
65
+ INPUT_COST_PER_MILLION_USD = _float_env("INPUT_COST_PER_MILLION_USD")
66
+ OUTPUT_COST_PER_MILLION_USD = _float_env(
67
+ "OUTPUT_COST_PER_MILLION_USD"
68
+ )
69
+
70
+ # Verbose Solana debug logging (server-side). Enable temporarily for diagnosing RPC type mismatches.
71
+ # WARNING: do not enable permanently in high-traffic production environments.
72
+ ATP_SOLANA_DEBUG = _bool_env("ATP_SOLANA_DEBUG", default=False)
73
+
74
+ # Settlement Service URL
75
+ ATP_SETTLEMENT_URL = os.getenv(
76
+ "ATP_SETTLEMENT_URL", "http://localhost:8001"
77
+ )
atp/middleware.py ADDED
@@ -0,0 +1,343 @@
1
+ """
2
+ FastAPI middleware for ATP settlement on any endpoint.
3
+
4
+ This middleware enables automatic payment deduction from Solana wallets
5
+ based on token usage (input/output tokens) for any configured endpoint.
6
+
7
+ The middleware delegates all settlement logic to the ATP Settlement Service,
8
+ ensuring immutable and centralized settlement operations.
9
+
10
+ The middleware accepts wallet private keys directly via headers, making it
11
+ simple to use without requiring API key management. Users can add their
12
+ own API key handling layer if needed.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from typing import Any, Callable, Dict, List, Optional, Set
19
+
20
+ from fastapi import HTTPException, Request, Response
21
+ from loguru import logger
22
+ from starlette.middleware.base import BaseHTTPMiddleware
23
+ from starlette.types import ASGIApp
24
+
25
+ from atp import config
26
+ from atp.schemas import PaymentToken
27
+ from atp.settlement_client import SettlementServiceClient
28
+
29
+
30
+ class ATPSettlementMiddleware(BaseHTTPMiddleware):
31
+ """
32
+ FastAPI middleware that automatically deducts payment from Solana wallets
33
+ based on token usage for configured endpoints.
34
+
35
+ The middleware delegates all settlement logic to the ATP Settlement Service,
36
+ ensuring immutable and centralized settlement operations.
37
+
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.
40
+
41
+ Payments are split automatically:
42
+ - Treasury (SWARMS_TREASURY_PUBKEY) receives the processing fee
43
+ - Recipient (endpoint host) receives the remainder
44
+
45
+ Usage:
46
+ app.add_middleware(
47
+ ATPSettlementMiddleware,
48
+ allowed_endpoints=["/v1/chat", "/v1/completions"],
49
+ input_cost_per_million_usd=10.0,
50
+ output_cost_per_million_usd=30.0,
51
+ wallet_private_key_header="x-wallet-private-key",
52
+ 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
55
+ )
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ app: ASGIApp,
61
+ *,
62
+ allowed_endpoints: List[str],
63
+ input_cost_per_million_usd: float,
64
+ output_cost_per_million_usd: float,
65
+ wallet_private_key_header: str = "x-wallet-private-key",
66
+ payment_token: PaymentToken = PaymentToken.SOL,
67
+ recipient_pubkey: Optional[str] = None,
68
+ skip_preflight: bool = False,
69
+ commitment: str = "confirmed",
70
+ usage_response_key: str = "usage",
71
+ include_usage_in_response: bool = True,
72
+ require_wallet: bool = True,
73
+ settlement_service_url: Optional[str] = None,
74
+ ):
75
+ """
76
+ Initialize the ATP settlement middleware.
77
+
78
+ The middleware delegates all settlement logic to the ATP Settlement Service.
79
+ All settlement operations are handled by the immutable settlement service.
80
+
81
+ Args:
82
+ app: The ASGI application.
83
+ allowed_endpoints: List of endpoint paths to apply settlement to (e.g., ["/v1/chat"]).
84
+ Supports path patterns - exact matches only.
85
+ input_cost_per_million_usd: Cost per million input tokens in USD.
86
+ output_cost_per_million_usd: Cost per million output tokens in USD.
87
+ wallet_private_key_header: HTTP header name containing the wallet private key
88
+ (default: "x-wallet-private-key"). The private key should be in JSON array
89
+ format (e.g., "[1,2,3,...]") or base58 string format.
90
+ payment_token: Token to use for payment (SOL or USDC).
91
+ recipient_pubkey: Solana public key of the recipient wallet (the endpoint host).
92
+ This wallet receives the main payment (after processing fee). Required.
93
+ skip_preflight: Whether to skip preflight simulation for Solana transactions.
94
+ commitment: Solana commitment level (processed|confirmed|finalized).
95
+ usage_response_key: Key in response JSON where usage data is located (default: "usage").
96
+ include_usage_in_response: Whether to add usage/cost info to the response.
97
+ require_wallet: Whether to require wallet private key (if False, skips settlement when missing).
98
+ settlement_service_url: Base URL of the settlement service. If not provided, uses
99
+ ATP_SETTLEMENT_URL environment variable (default: http://localhost:8001).
100
+ The middleware always uses the settlement service for all settlement operations.
101
+ """
102
+ super().__init__(app)
103
+ self.allowed_endpoints: Set[str] = set(allowed_endpoints)
104
+ self.input_cost_per_million_usd = input_cost_per_million_usd
105
+ self.output_cost_per_million_usd = output_cost_per_million_usd
106
+ self.wallet_private_key_header = (
107
+ wallet_private_key_header.lower()
108
+ )
109
+ self.payment_token = payment_token
110
+ # Recipient pubkey - configurable, the endpoint host receives the main payment
111
+ self._recipient_pubkey = recipient_pubkey
112
+ if not self._recipient_pubkey:
113
+ raise ValueError("recipient_pubkey must be provided")
114
+ # Treasury pubkey - always uses SWARMS_TREASURY_PUBKEY for processing fees
115
+ self._treasury_pubkey = config.SWARMS_TREASURY_PUBKEY
116
+ if not self._treasury_pubkey:
117
+ raise ValueError(
118
+ "SWARMS_TREASURY_PUBKEY must be set in configuration"
119
+ )
120
+ self.skip_preflight = skip_preflight
121
+ self.commitment = commitment
122
+ self.usage_response_key = usage_response_key
123
+ self.include_usage_in_response = include_usage_in_response
124
+ self.require_wallet = require_wallet
125
+ # Always use settlement service - initialize client with config value or provided URL
126
+ service_url = (
127
+ settlement_service_url or config.ATP_SETTLEMENT_URL
128
+ )
129
+ self.settlement_service_client = SettlementServiceClient(
130
+ base_url=service_url
131
+ )
132
+
133
+ def _should_process(self, path: str) -> bool:
134
+ """Check if the request path should be processed by this middleware."""
135
+ return path in self.allowed_endpoints
136
+
137
+ def _extract_wallet_private_key(
138
+ self, request: Request
139
+ ) -> Optional[str]:
140
+ """Extract wallet private key from request headers."""
141
+ return request.headers.get(self.wallet_private_key_header)
142
+
143
+ async def _extract_usage_from_response(
144
+ self, response_body: bytes
145
+ ) -> Optional[Dict[str, Any]]:
146
+ """
147
+ Extract usage information from response body.
148
+
149
+ Tries multiple strategies:
150
+ 1. Look for usage data at the configured usage_response_key
151
+ 2. Check if the entire response contains usage-like keys
152
+ 3. Try nested structures (usage.usage, meta.usage, etc.)
153
+
154
+ The usage data is then sent to the settlement service for parsing,
155
+ so we just need to extract the raw usage object.
156
+ """
157
+ try:
158
+ body_str = response_body.decode("utf-8")
159
+ if not body_str.strip():
160
+ return None
161
+ data = json.loads(body_str)
162
+
163
+ # Strategy 1: Try the configured usage key first
164
+ usage = data.get(self.usage_response_key)
165
+ if usage and isinstance(usage, dict):
166
+ return usage
167
+
168
+ # Strategy 2: Check if the entire response is usage-like
169
+ if isinstance(data, dict):
170
+ # Check for common usage keys at top level
171
+ usage_keys = [
172
+ "input_tokens",
173
+ "output_tokens",
174
+ "prompt_tokens",
175
+ "completion_tokens",
176
+ "total_tokens",
177
+ "tokens",
178
+ "promptTokenCount",
179
+ "candidatesTokenCount",
180
+ "totalTokenCount",
181
+ ]
182
+ if any(key in data for key in usage_keys):
183
+ return data
184
+
185
+ # Strategy 3: Try nested structures
186
+ # Check for usage nested in common locations
187
+ for nested_key in [
188
+ "usage",
189
+ "token_usage",
190
+ "tokens",
191
+ "statistics",
192
+ "meta",
193
+ ]:
194
+ if nested_key in data and isinstance(
195
+ data[nested_key], dict
196
+ ):
197
+ nested_usage = data[nested_key]
198
+ # Check if it looks like usage data
199
+ if any(
200
+ key in nested_usage
201
+ for key in [
202
+ "input_tokens",
203
+ "output_tokens",
204
+ "prompt_tokens",
205
+ "completion_tokens",
206
+ "tokens",
207
+ ]
208
+ ):
209
+ return nested_usage
210
+
211
+ return None
212
+ except (json.JSONDecodeError, UnicodeDecodeError) as e:
213
+ logger.debug(
214
+ f"Failed to parse response body for usage: {e}"
215
+ )
216
+ return None
217
+
218
+ async def dispatch(
219
+ self, request: Request, call_next: Callable
220
+ ) -> Response:
221
+ """Process the request and apply settlement if applicable."""
222
+ path = request.url.path
223
+
224
+ # Skip if not in allowed endpoints
225
+ if not self._should_process(path):
226
+ return await call_next(request)
227
+
228
+ # Extract wallet private key
229
+ private_key = self._extract_wallet_private_key(request)
230
+ if not private_key:
231
+ if self.require_wallet:
232
+ raise HTTPException(
233
+ status_code=401,
234
+ detail=f"Missing wallet private key in header: {self.wallet_private_key_header}",
235
+ )
236
+ # If wallet not required, skip settlement
237
+ return await call_next(request)
238
+
239
+ # Execute the endpoint
240
+ response = await call_next(request)
241
+
242
+ # Only process successful responses
243
+ if response.status_code >= 400:
244
+ return response
245
+
246
+ # Extract usage from response
247
+ response_body = b""
248
+ async for chunk in response.body_iterator:
249
+ response_body += chunk
250
+
251
+ usage = await self._extract_usage_from_response(response_body)
252
+
253
+ if not usage:
254
+ logger.warning(
255
+ 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'}"
256
+ )
257
+ # Return original response if no usage found
258
+ return Response(
259
+ content=response_body,
260
+ status_code=response.status_code,
261
+ headers=dict(response.headers),
262
+ media_type=response.media_type,
263
+ )
264
+
265
+ # Calculate and deduct payment via settlement service
266
+ try:
267
+ payment_result = await self.settlement_service_client.settle(
268
+ private_key=private_key,
269
+ usage=usage,
270
+ input_cost_per_million_usd=self.input_cost_per_million_usd,
271
+ output_cost_per_million_usd=self.output_cost_per_million_usd,
272
+ recipient_pubkey=self._recipient_pubkey,
273
+ payment_token=self.payment_token.value,
274
+ treasury_pubkey=self._treasury_pubkey,
275
+ skip_preflight=self.skip_preflight,
276
+ commitment=self.commitment,
277
+ )
278
+ except HTTPException:
279
+ raise
280
+ except Exception as e:
281
+ logger.error(f"Settlement error: {e}", exc_info=True)
282
+ raise HTTPException(
283
+ status_code=500,
284
+ detail=f"Settlement failed: {str(e)}",
285
+ )
286
+
287
+ # Modify response to include usage/payment info if requested
288
+ if self.include_usage_in_response:
289
+ try:
290
+ response_data = json.loads(
291
+ response_body.decode("utf-8")
292
+ )
293
+ response_data["atp_settlement"] = payment_result
294
+ response_data["atp_usage"] = usage
295
+ response_body = json.dumps(response_data).encode(
296
+ "utf-8"
297
+ )
298
+ except Exception as e:
299
+ logger.warning(
300
+ f"Failed to add settlement info to response: {e}"
301
+ )
302
+
303
+ return Response(
304
+ content=response_body,
305
+ status_code=response.status_code,
306
+ headers=dict(response.headers),
307
+ media_type=response.media_type,
308
+ )
309
+
310
+
311
+ def create_settlement_middleware(
312
+ allowed_endpoints: List[str],
313
+ input_cost_per_million_usd: float,
314
+ output_cost_per_million_usd: float,
315
+ **kwargs: Any,
316
+ ) -> type[ATPSettlementMiddleware]:
317
+ """
318
+ Factory function to create a configured ATP settlement middleware.
319
+
320
+ Example:
321
+ middleware = create_settlement_middleware(
322
+ allowed_endpoints=["/v1/chat", "/v1/completions"],
323
+ input_cost_per_million_usd=10.0,
324
+ output_cost_per_million_usd=30.0,
325
+ wallet_private_key_header="x-wallet-private-key",
326
+ recipient_pubkey="YourPublicKeyHere", # Optional: defaults to SWARMS_TREASURY_PUBKEY
327
+ )
328
+ app.add_middleware(middleware)
329
+ """
330
+ return type(
331
+ "ConfiguredATPSettlementMiddleware",
332
+ (ATPSettlementMiddleware,),
333
+ {
334
+ "__init__": lambda self, app: ATPSettlementMiddleware.__init__(
335
+ self,
336
+ app,
337
+ allowed_endpoints=allowed_endpoints,
338
+ input_cost_per_million_usd=input_cost_per_million_usd,
339
+ output_cost_per_million_usd=output_cost_per_million_usd,
340
+ **kwargs,
341
+ )
342
+ },
343
+ )
atp/schemas.py ADDED
@@ -0,0 +1,171 @@
1
+ from enum import Enum
2
+ from typing import Any, Dict, List, Optional, Union
3
+
4
+ from pydantic import BaseModel, Field
5
+ from swarms.schemas.mcp_schemas import (
6
+ MCPConnection,
7
+ MultipleMCPConnections,
8
+ )
9
+
10
+
11
+ class AgentSpec(BaseModel):
12
+ agent_name: Optional[str] = Field(
13
+ # default=None,
14
+ description="The unique name assigned to the agent, which identifies its role and functionality within the swarm.",
15
+ )
16
+ description: Optional[str] = Field(
17
+ default=None,
18
+ description="A detailed explanation of the agent's purpose, capabilities, and any specific tasks it is designed to perform.",
19
+ )
20
+ system_prompt: Optional[str] = Field(
21
+ default=None,
22
+ description="The initial instruction or context provided to the agent, guiding its behavior and responses during execution.",
23
+ )
24
+ model_name: Optional[str] = Field(
25
+ default="gpt-4.1",
26
+ description="The name of the AI model that the agent will utilize for processing tasks and generating outputs. For example: gpt-4o, gpt-4o-mini, openai/o3-mini",
27
+ )
28
+ auto_generate_prompt: Optional[bool] = Field(
29
+ default=False,
30
+ description="A flag indicating whether the agent should automatically create prompts based on the task requirements.",
31
+ )
32
+ max_tokens: Optional[int] = Field(
33
+ default=8192,
34
+ description="The maximum number of tokens that the agent is allowed to generate in its responses, limiting output length.",
35
+ )
36
+ temperature: Optional[float] = Field(
37
+ default=0.5,
38
+ description="A parameter that controls the randomness of the agent's output; lower values result in more deterministic responses.",
39
+ )
40
+ role: Optional[str] = Field(
41
+ default="worker",
42
+ description="The designated role of the agent within the swarm, which influences its behavior and interaction with other agents.",
43
+ )
44
+ max_loops: Optional[int] = Field(
45
+ default=1,
46
+ description="The maximum number of times the agent is allowed to repeat its task, enabling iterative processing if necessary.",
47
+ )
48
+ tools_list_dictionary: Optional[List[Dict[Any, Any]]] = Field(
49
+ default=None,
50
+ description="A dictionary of tools that the agent can use to complete its task.",
51
+ )
52
+ mcp_url: Optional[str] = Field(
53
+ default=None,
54
+ description="The URL of the MCP server that the agent can use to complete its task.",
55
+ )
56
+ streaming_on: Optional[bool] = Field(
57
+ default=False,
58
+ description="A flag indicating whether the agent should stream its output.",
59
+ )
60
+ llm_args: Optional[Dict[str, Any]] = Field(
61
+ default=None,
62
+ description="Additional arguments to pass to the LLM such as top_p, frequency_penalty, presence_penalty, etc.",
63
+ )
64
+ dynamic_temperature_enabled: Optional[bool] = Field(
65
+ default=True,
66
+ description="A flag indicating whether the agent should dynamically adjust its temperature based on the task.",
67
+ )
68
+
69
+ mcp_config: Optional[MCPConnection] = Field(
70
+ default=None,
71
+ description="The MCP connection to use for the agent.",
72
+ )
73
+
74
+ mcp_configs: Optional[MultipleMCPConnections] = Field(
75
+ default=None,
76
+ description="The MCP connections to use for the agent. This is a list of MCP connections. Includes multiple MCP connections.",
77
+ )
78
+
79
+ tool_call_summary: Optional[bool] = Field(
80
+ default=True,
81
+ description="A parameter enabling an agent to summarize tool calls.",
82
+ )
83
+
84
+ reasoning_effort: Optional[str] = Field(
85
+ default=None,
86
+ description="The effort to put into reasoning.",
87
+ )
88
+
89
+ thinking_tokens: Optional[int] = Field(
90
+ default=None,
91
+ description="The number of tokens to use for thinking.",
92
+ )
93
+
94
+ reasoning_enabled: Optional[bool] = Field(
95
+ default=False,
96
+ description="A parameter enabling an agent to use reasoning.",
97
+ )
98
+
99
+ class Config:
100
+ arbitrary_types_allowed = True
101
+
102
+
103
+ class PaymentToken(str, Enum):
104
+ """Supported payment tokens on Solana."""
105
+
106
+ SOL = "SOL"
107
+ USDC = "USDC"
108
+
109
+
110
+ class AgentTask(BaseModel):
111
+ """Complete agent task request requiring full agent specification."""
112
+
113
+ agent_config: AgentSpec = Field(
114
+ ...,
115
+ description="Complete agent configuration specification matching the Swarms API AgentSpec schema",
116
+ )
117
+ task: str = Field(
118
+ ...,
119
+ description="The task or query to execute",
120
+ example="Analyze the latest SOL/USDC liquidity pool data and provide trading recommendations.",
121
+ )
122
+ user_wallet: str = Field(
123
+ ...,
124
+ description="The Solana public key of the sender for payment verification",
125
+ )
126
+ payment_token: PaymentToken = Field(
127
+ default=PaymentToken.SOL,
128
+ description="Payment token to use for settlement (SOL or USDC)",
129
+ )
130
+ history: Optional[Union[Dict[Any, Any], List[Dict[str, str]]]] = (
131
+ Field(
132
+ default=None,
133
+ description="Optional conversation history for context",
134
+ )
135
+ )
136
+ img: Optional[str] = Field(
137
+ default=None,
138
+ description="Optional image URL for vision tasks",
139
+ )
140
+ imgs: Optional[List[str]] = Field(
141
+ default=None,
142
+ description="Optional list of image URLs for vision tasks",
143
+ )
144
+
145
+
146
+ class SettleTrade(BaseModel):
147
+ """Settlement request that asks the facilitator to sign+send the payment tx.
148
+
149
+ WARNING: This is custodial-like behavior. The private key is used in-memory only
150
+ for the duration of this request and is not persisted.
151
+ """
152
+
153
+ job_id: str = Field(
154
+ ..., description="Job ID from the trade creation response"
155
+ )
156
+ private_key: str = Field(
157
+ ...,
158
+ description=(
159
+ "Payer private key encoded as a string. Supported formats:\n"
160
+ "- Base58 keypair (common Solana secret key string)\n"
161
+ "- JSON array of ints (e.g. '[12,34,...]')"
162
+ ),
163
+ )
164
+ skip_preflight: bool = Field(
165
+ default=False,
166
+ description="Whether to skip preflight simulation",
167
+ )
168
+ commitment: str = Field(
169
+ default="confirmed",
170
+ description="Confirmation level to wait for (processed|confirmed|finalized)",
171
+ )