atp-protocol 1.0.0__py3-none-any.whl → 1.2.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 +4 -7
- atp/config.py +1 -1
- atp/middleware.py +19 -32
- {atp_protocol-1.0.0.dist-info → atp_protocol-1.2.0.dist-info}/METADATA +90 -181
- atp_protocol-1.2.0.dist-info/RECORD +9 -0
- atp/solana_utils.py +0 -1001
- atp/token_prices.py +0 -97
- atp/utils.py +0 -174
- atp/vault.py +0 -75
- atp_protocol-1.0.0.dist-info/RECORD +0 -13
- {atp_protocol-1.0.0.dist-info → atp_protocol-1.2.0.dist-info}/LICENSE +0 -0
- {atp_protocol-1.0.0.dist-info → atp_protocol-1.2.0.dist-info}/WHEEL +0 -0
atp/token_prices.py
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Dict
|
|
5
|
-
|
|
6
|
-
import httpx
|
|
7
|
-
from loguru import logger
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TokenPriceFetcher:
|
|
11
|
-
"""Fetches real-time token prices from CoinGecko API."""
|
|
12
|
-
|
|
13
|
-
COINGECKO_URL = "https://api.coingecko.com/api/v3/simple/price"
|
|
14
|
-
CACHE_TTL_SECONDS = 60 # Cache price for 60 seconds
|
|
15
|
-
|
|
16
|
-
def __init__(self):
|
|
17
|
-
self._cached_prices: Dict[str, float] = {}
|
|
18
|
-
self._cache_timestamps: Dict[str, float] = {}
|
|
19
|
-
self._lock = asyncio.Lock()
|
|
20
|
-
|
|
21
|
-
async def get_price_usd(self, token: str = "SOL") -> float:
|
|
22
|
-
import time
|
|
23
|
-
|
|
24
|
-
# USDC is pegged to USD
|
|
25
|
-
if token.upper() == "USDC":
|
|
26
|
-
return 1.0
|
|
27
|
-
|
|
28
|
-
async with self._lock:
|
|
29
|
-
current_time = time.time()
|
|
30
|
-
cache_key = token.upper()
|
|
31
|
-
|
|
32
|
-
cached_price = self._cached_prices.get(cache_key)
|
|
33
|
-
cached_timestamp = self._cache_timestamps.get(
|
|
34
|
-
cache_key, 0
|
|
35
|
-
)
|
|
36
|
-
if (
|
|
37
|
-
cached_price
|
|
38
|
-
and (current_time - cached_timestamp)
|
|
39
|
-
< self.CACHE_TTL_SECONDS
|
|
40
|
-
):
|
|
41
|
-
return cached_price
|
|
42
|
-
|
|
43
|
-
coingecko_ids = {"SOL": "solana"}
|
|
44
|
-
coingecko_id = coingecko_ids.get(token.upper())
|
|
45
|
-
if not coingecko_id:
|
|
46
|
-
logger.warning(
|
|
47
|
-
f"Unknown token: {token}, defaulting to $1.00"
|
|
48
|
-
)
|
|
49
|
-
return 1.0
|
|
50
|
-
|
|
51
|
-
try:
|
|
52
|
-
async with httpx.AsyncClient() as client:
|
|
53
|
-
response = await client.get(
|
|
54
|
-
self.COINGECKO_URL,
|
|
55
|
-
params={
|
|
56
|
-
"ids": coingecko_id,
|
|
57
|
-
"vs_currencies": "usd",
|
|
58
|
-
},
|
|
59
|
-
timeout=10.0,
|
|
60
|
-
)
|
|
61
|
-
response.raise_for_status()
|
|
62
|
-
data = response.json()
|
|
63
|
-
|
|
64
|
-
price = data.get(coingecko_id, {}).get("usd")
|
|
65
|
-
if price is None:
|
|
66
|
-
raise ValueError(
|
|
67
|
-
f"{token} price not found in response"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
self._cached_prices[cache_key] = float(price)
|
|
71
|
-
self._cache_timestamps[cache_key] = current_time
|
|
72
|
-
return float(price)
|
|
73
|
-
|
|
74
|
-
except httpx.HTTPError as e:
|
|
75
|
-
logger.warning(
|
|
76
|
-
f"Failed to fetch {token} price from CoinGecko: {e}"
|
|
77
|
-
)
|
|
78
|
-
if cached_price:
|
|
79
|
-
return cached_price
|
|
80
|
-
return 150.0
|
|
81
|
-
except Exception as e:
|
|
82
|
-
logger.error(
|
|
83
|
-
f"Unexpected error fetching {token} price: {e}"
|
|
84
|
-
)
|
|
85
|
-
if cached_price:
|
|
86
|
-
return cached_price
|
|
87
|
-
return 150.0
|
|
88
|
-
|
|
89
|
-
async def get_sol_price_usd(self) -> float:
|
|
90
|
-
return await self.get_price_usd("SOL")
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
token_price_fetcher = TokenPriceFetcher()
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
# if __name__ == "__main__":
|
|
97
|
-
# print(asyncio.run(token_price_fetcher.get_price_usd("SOL")))
|
atp/utils.py
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from typing import Any, Dict, Optional
|
|
4
|
-
|
|
5
|
-
from atp import config
|
|
6
|
-
from atp.schemas import PaymentToken
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def calculate_payment_amounts(
|
|
10
|
-
usd_cost: float,
|
|
11
|
-
token_price_usd: float,
|
|
12
|
-
payment_token: PaymentToken,
|
|
13
|
-
) -> Dict[str, Any]:
|
|
14
|
-
"""
|
|
15
|
-
Calculate payment amounts with settlement fee taken from the total.
|
|
16
|
-
|
|
17
|
-
Returns amounts in the smallest unit (lamports for SOL, micro-units for USDC).
|
|
18
|
-
"""
|
|
19
|
-
total_amount_token = usd_cost / token_price_usd
|
|
20
|
-
fee_amount_token = (
|
|
21
|
-
total_amount_token * config.SETTLEMENT_FEE_PERCENT
|
|
22
|
-
)
|
|
23
|
-
agent_amount_token = total_amount_token - fee_amount_token
|
|
24
|
-
|
|
25
|
-
decimals = (
|
|
26
|
-
9
|
|
27
|
-
if payment_token == PaymentToken.SOL
|
|
28
|
-
else config.USDC_DECIMALS
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
total_amount_units = int(total_amount_token * 10**decimals)
|
|
32
|
-
fee_amount_units = int(fee_amount_token * 10**decimals)
|
|
33
|
-
agent_amount_units = total_amount_units - fee_amount_units
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
"total_amount_units": total_amount_units,
|
|
37
|
-
"agent_amount_units": agent_amount_units,
|
|
38
|
-
"fee_amount_units": fee_amount_units,
|
|
39
|
-
"total_amount_token": total_amount_token,
|
|
40
|
-
"agent_amount_token": agent_amount_token,
|
|
41
|
-
"fee_amount_token": fee_amount_token,
|
|
42
|
-
"decimals": decimals,
|
|
43
|
-
"fee_percent": config.SETTLEMENT_FEE_PERCENT * 100,
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def _safe_int(v: Any) -> Optional[int]:
|
|
48
|
-
if v is None:
|
|
49
|
-
return None
|
|
50
|
-
if isinstance(v, bool):
|
|
51
|
-
return None
|
|
52
|
-
if isinstance(v, int):
|
|
53
|
-
return v
|
|
54
|
-
if isinstance(v, float):
|
|
55
|
-
return int(v)
|
|
56
|
-
if isinstance(v, str):
|
|
57
|
-
s = v.strip()
|
|
58
|
-
if not s:
|
|
59
|
-
return None
|
|
60
|
-
try:
|
|
61
|
-
return int(float(s))
|
|
62
|
-
except Exception:
|
|
63
|
-
return None
|
|
64
|
-
return None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def extract_usage_token_counts(
|
|
68
|
-
usage: Any,
|
|
69
|
-
) -> Dict[str, Optional[int]]:
|
|
70
|
-
"""
|
|
71
|
-
Normalize token counts from various possible upstream shapes.
|
|
72
|
-
Common keys:
|
|
73
|
-
- prompt_tokens / completion_tokens / total_tokens (OpenAI-like)
|
|
74
|
-
- input_tokens / output_tokens / total_tokens
|
|
75
|
-
"""
|
|
76
|
-
if not isinstance(usage, dict):
|
|
77
|
-
return {
|
|
78
|
-
"input_tokens": None,
|
|
79
|
-
"output_tokens": None,
|
|
80
|
-
"total_tokens": None,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
input_tokens = _safe_int(usage.get("input_tokens"))
|
|
84
|
-
if input_tokens is None:
|
|
85
|
-
input_tokens = _safe_int(usage.get("prompt_tokens"))
|
|
86
|
-
|
|
87
|
-
output_tokens = _safe_int(usage.get("output_tokens"))
|
|
88
|
-
if output_tokens is None:
|
|
89
|
-
output_tokens = _safe_int(usage.get("completion_tokens"))
|
|
90
|
-
|
|
91
|
-
total_tokens = _safe_int(usage.get("total_tokens"))
|
|
92
|
-
if (
|
|
93
|
-
total_tokens is None
|
|
94
|
-
and input_tokens is not None
|
|
95
|
-
and output_tokens is not None
|
|
96
|
-
):
|
|
97
|
-
total_tokens = input_tokens + output_tokens
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
"input_tokens": input_tokens,
|
|
101
|
-
"output_tokens": output_tokens,
|
|
102
|
-
"total_tokens": total_tokens,
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def compute_usd_cost_from_usage(usage: Any) -> Dict[str, Any]:
|
|
107
|
-
"""
|
|
108
|
-
Returns a pricing breakdown dict containing:
|
|
109
|
-
- usd_cost (float)
|
|
110
|
-
- source: per_million_rates | upstream_total_cost | fallback_default
|
|
111
|
-
- token counts and configured rates
|
|
112
|
-
"""
|
|
113
|
-
usage = usage if isinstance(usage, dict) else {}
|
|
114
|
-
counts = extract_usage_token_counts(usage)
|
|
115
|
-
|
|
116
|
-
can_compute_input = (
|
|
117
|
-
config.INPUT_COST_PER_MILLION_USD is not None
|
|
118
|
-
and counts["input_tokens"] is not None
|
|
119
|
-
)
|
|
120
|
-
can_compute_output = (
|
|
121
|
-
config.OUTPUT_COST_PER_MILLION_USD is not None
|
|
122
|
-
and counts["output_tokens"] is not None
|
|
123
|
-
)
|
|
124
|
-
|
|
125
|
-
if can_compute_input or can_compute_output:
|
|
126
|
-
input_cost = (
|
|
127
|
-
(counts["input_tokens"] or 0)
|
|
128
|
-
/ 1_000_000.0
|
|
129
|
-
* (config.INPUT_COST_PER_MILLION_USD or 0.0)
|
|
130
|
-
)
|
|
131
|
-
output_cost = (
|
|
132
|
-
(counts["output_tokens"] or 0)
|
|
133
|
-
/ 1_000_000.0
|
|
134
|
-
* (config.OUTPUT_COST_PER_MILLION_USD or 0.0)
|
|
135
|
-
)
|
|
136
|
-
usd_cost = float(input_cost + output_cost)
|
|
137
|
-
return {
|
|
138
|
-
"usd_cost": usd_cost,
|
|
139
|
-
"source": "per_million_rates",
|
|
140
|
-
"input_tokens": counts["input_tokens"],
|
|
141
|
-
"output_tokens": counts["output_tokens"],
|
|
142
|
-
"total_tokens": counts["total_tokens"],
|
|
143
|
-
"input_cost_per_million_usd": config.INPUT_COST_PER_MILLION_USD,
|
|
144
|
-
"output_cost_per_million_usd": config.OUTPUT_COST_PER_MILLION_USD,
|
|
145
|
-
"input_cost_usd": input_cost,
|
|
146
|
-
"output_cost_usd": output_cost,
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
upstream_total_cost = usage.get("total_cost")
|
|
150
|
-
try:
|
|
151
|
-
if upstream_total_cost is not None:
|
|
152
|
-
usd_cost = float(upstream_total_cost)
|
|
153
|
-
return {
|
|
154
|
-
"usd_cost": usd_cost,
|
|
155
|
-
"source": "upstream_total_cost",
|
|
156
|
-
"input_tokens": counts["input_tokens"],
|
|
157
|
-
"output_tokens": counts["output_tokens"],
|
|
158
|
-
"total_tokens": counts["total_tokens"],
|
|
159
|
-
"input_cost_per_million_usd": config.INPUT_COST_PER_MILLION_USD,
|
|
160
|
-
"output_cost_per_million_usd": config.OUTPUT_COST_PER_MILLION_USD,
|
|
161
|
-
}
|
|
162
|
-
except Exception:
|
|
163
|
-
pass
|
|
164
|
-
|
|
165
|
-
usd_cost = 0.01
|
|
166
|
-
return {
|
|
167
|
-
"usd_cost": usd_cost,
|
|
168
|
-
"source": "fallback_default",
|
|
169
|
-
"input_tokens": counts["input_tokens"],
|
|
170
|
-
"output_tokens": counts["output_tokens"],
|
|
171
|
-
"total_tokens": counts["total_tokens"],
|
|
172
|
-
"input_cost_per_million_usd": config.INPUT_COST_PER_MILLION_USD,
|
|
173
|
-
"output_cost_per_million_usd": config.OUTPUT_COST_PER_MILLION_USD,
|
|
174
|
-
}
|
atp/vault.py
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import asyncio
|
|
4
|
-
from typing import Any, Dict, Optional
|
|
5
|
-
|
|
6
|
-
from loguru import logger
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class InMemoryVault:
|
|
10
|
-
"""In-process job vault with TTL expiration (for local dev/testing).
|
|
11
|
-
|
|
12
|
-
Note: data is not shared across processes and is lost on restart.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
def __init__(self, default_ttl: int = 600):
|
|
16
|
-
self.default_ttl = default_ttl
|
|
17
|
-
self._lock = asyncio.Lock()
|
|
18
|
-
# key: job_id -> {"data": dict, "expires_at": float}
|
|
19
|
-
self._store: Dict[str, Dict[str, Any]] = {}
|
|
20
|
-
|
|
21
|
-
async def connect(self) -> None:
|
|
22
|
-
logger.warning(
|
|
23
|
-
"Using in-memory job vault. Jobs will be lost on restart."
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
async def disconnect(self) -> None:
|
|
27
|
-
return None
|
|
28
|
-
|
|
29
|
-
def _is_expired(self, expires_at: float) -> bool:
|
|
30
|
-
import time
|
|
31
|
-
|
|
32
|
-
return time.time() >= expires_at
|
|
33
|
-
|
|
34
|
-
async def store(
|
|
35
|
-
self,
|
|
36
|
-
job_id: str,
|
|
37
|
-
data: Dict[str, Any],
|
|
38
|
-
ttl: Optional[int] = None,
|
|
39
|
-
) -> None:
|
|
40
|
-
import time
|
|
41
|
-
|
|
42
|
-
ttl = ttl or self.default_ttl
|
|
43
|
-
expires_at = time.time() + ttl
|
|
44
|
-
async with self._lock:
|
|
45
|
-
self._store[job_id] = {
|
|
46
|
-
"data": data,
|
|
47
|
-
"expires_at": expires_at,
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async def retrieve(self, job_id: str) -> Optional[Dict[str, Any]]:
|
|
51
|
-
async with self._lock:
|
|
52
|
-
entry = self._store.get(job_id)
|
|
53
|
-
if not entry:
|
|
54
|
-
return None
|
|
55
|
-
if self._is_expired(entry["expires_at"]):
|
|
56
|
-
self._store.pop(job_id, None)
|
|
57
|
-
return None
|
|
58
|
-
return entry["data"]
|
|
59
|
-
|
|
60
|
-
async def delete(self, job_id: str) -> bool:
|
|
61
|
-
async with self._lock:
|
|
62
|
-
existed = job_id in self._store
|
|
63
|
-
self._store.pop(job_id, None)
|
|
64
|
-
return existed
|
|
65
|
-
|
|
66
|
-
async def pop(self, job_id: str) -> Optional[Dict[str, Any]]:
|
|
67
|
-
async with self._lock:
|
|
68
|
-
entry = self._store.get(job_id)
|
|
69
|
-
if not entry:
|
|
70
|
-
return None
|
|
71
|
-
if self._is_expired(entry["expires_at"]):
|
|
72
|
-
self._store.pop(job_id, None)
|
|
73
|
-
return None
|
|
74
|
-
self._store.pop(job_id, None)
|
|
75
|
-
return entry["data"]
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
atp/__init__.py,sha256=lv9yAwpBiySYw2ECZagHUmZHtKXrbkCxF1XoF0IuYeI,205
|
|
2
|
-
atp/config.py,sha256=m5hsDeMgbtqMUu9uO4G_ht6WBD8T5FNm-ga2tE5oRmU,2030
|
|
3
|
-
atp/middleware.py,sha256=lR0PX2O3ka3u_Wj1T8T5dSm56DxEzduXHkNyk-230Nc,13891
|
|
4
|
-
atp/schemas.py,sha256=iASVBPpAhrO-LDXs2UC9ABtfV1oDYsu4kZcz3dVeJNA,6220
|
|
5
|
-
atp/settlement_client.py,sha256=jfvhxDqp2kDISWG7tjX5s88goAPxp-lMXvuJtpMyOys,6742
|
|
6
|
-
atp/solana_utils.py,sha256=T4KtMZkXVmUENsV5u0V3Yxd9Cp11YkVEpYt9lvMfZJI,36767
|
|
7
|
-
atp/token_prices.py,sha256=A5EeNXkLfePktzQieGUVP5SGE5_KsClFVuL6B0vTr5k,3077
|
|
8
|
-
atp/utils.py,sha256=VnYg6vmggUtrugwFc9Ecr8y45cxrFWv598WPK0w9y-0,5516
|
|
9
|
-
atp/vault.py,sha256=ZMcrjCulI9j36nZtBw0kfL4k0Ul7LgUKyEedtN7o2ks,2193
|
|
10
|
-
atp_protocol-1.0.0.dist-info/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
11
|
-
atp_protocol-1.0.0.dist-info/METADATA,sha256=gIHO-KWejBOhVkvHFstqCBcqgZMZ6El3WNqGA7GLoO8,18442
|
|
12
|
-
atp_protocol-1.0.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
13
|
-
atp_protocol-1.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|