pay-skill 0.1.2__tar.gz
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.
- pay_skill-0.1.2/.gitignore +28 -0
- pay_skill-0.1.2/PKG-INFO +182 -0
- pay_skill-0.1.2/README.md +154 -0
- pay_skill-0.1.2/pyproject.toml +66 -0
- pay_skill-0.1.2/src/payskill/__init__.py +30 -0
- pay_skill-0.1.2/src/payskill/auth.py +101 -0
- pay_skill-0.1.2/src/payskill/client.py +508 -0
- pay_skill-0.1.2/src/payskill/eip3009.py +91 -0
- pay_skill-0.1.2/src/payskill/errors.py +39 -0
- pay_skill-0.1.2/src/payskill/models.py +94 -0
- pay_skill-0.1.2/src/payskill/ows_signer.py +159 -0
- pay_skill-0.1.2/src/payskill/py.typed +0 -0
- pay_skill-0.1.2/src/payskill/signer.py +139 -0
- pay_skill-0.1.2/tests/__init__.py +0 -0
- pay_skill-0.1.2/tests/test_auth_rejection.py +183 -0
- pay_skill-0.1.2/tests/test_client.py +268 -0
- pay_skill-0.1.2/tests/test_crypto.py +119 -0
- pay_skill-0.1.2/tests/test_e2e.py +205 -0
- pay_skill-0.1.2/tests/test_errors.py +32 -0
- pay_skill-0.1.2/tests/test_ows_integration.py +112 -0
- pay_skill-0.1.2/tests/test_ows_signer.py +276 -0
- pay_skill-0.1.2/tests/test_signer.py +43 -0
- pay_skill-0.1.2/tests/test_validation.py +53 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.egg-info/
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
.eggs/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
htmlcov/
|
|
13
|
+
.coverage
|
|
14
|
+
|
|
15
|
+
# TypeScript
|
|
16
|
+
node_modules/
|
|
17
|
+
typescript/dist/
|
|
18
|
+
*.tsbuildinfo
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.vscode/
|
|
22
|
+
.idea/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
pay_skill-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pay-skill
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Python SDK for pay — payment infrastructure for AI agents
|
|
5
|
+
Project-URL: Homepage, https://pay-skill.com
|
|
6
|
+
Project-URL: Repository, https://github.com/pay-skill/pay-sdk
|
|
7
|
+
Author-email: "pay-skill.com" <hello@pay-skill.com>
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Requires-Dist: eth-account>=0.11
|
|
15
|
+
Requires-Dist: httpx>=0.27
|
|
16
|
+
Requires-Dist: pydantic>=2.0
|
|
17
|
+
Provides-Extra: dev
|
|
18
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
19
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
20
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
24
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
25
|
+
Provides-Extra: ows
|
|
26
|
+
Requires-Dist: open-wallet-standard>=1.2; extra == 'ows'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pay-sdk
|
|
30
|
+
|
|
31
|
+
Python SDK for [pay](https://pay-skill.com) — payment infrastructure for AI agents. USDC on Base.
|
|
32
|
+
|
|
33
|
+
Three primitives: direct payments, tabs (pre-funded metered accounts), and x402 HTTP paywalls.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pay-sdk
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Requires Python 3.10+.
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from payskill import PayClient
|
|
47
|
+
|
|
48
|
+
client = PayClient(signer="cli") # uses `pay sign` subprocess
|
|
49
|
+
|
|
50
|
+
# Pay another agent $5
|
|
51
|
+
result = client.pay_direct("0xprovider...", 5_000_000, memo="task-42")
|
|
52
|
+
print(result.tx_hash)
|
|
53
|
+
|
|
54
|
+
# Open a metered tab
|
|
55
|
+
tab = client.open_tab("0xprovider...", 20_000_000, max_charge_per_call=500_000)
|
|
56
|
+
|
|
57
|
+
# x402 request (SDK handles payment automatically)
|
|
58
|
+
response = client.request("https://api.example.com/data")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
All amounts are in USDC micro-units (6 decimals). `$1.00 = 1_000_000`.
|
|
62
|
+
|
|
63
|
+
## API Reference
|
|
64
|
+
|
|
65
|
+
### PayClient
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from payskill import PayClient
|
|
69
|
+
|
|
70
|
+
client = PayClient(
|
|
71
|
+
api_url="https://pay-skill.com/api/v1", # default
|
|
72
|
+
signer="cli", # "cli", "raw", "custom", or a Signer instance
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Direct Payment
|
|
77
|
+
|
|
78
|
+
One-shot USDC transfer. $1.00 minimum.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
result = client.pay_direct(to, amount, memo="")
|
|
82
|
+
# Returns: DirectPaymentResult(tx_hash, status, amount, fee)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Tab Management
|
|
86
|
+
|
|
87
|
+
Pre-funded metered account. $5.00 minimum to open.
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
# Open
|
|
91
|
+
tab = client.open_tab(provider, amount, max_charge_per_call)
|
|
92
|
+
|
|
93
|
+
# Query
|
|
94
|
+
tabs = client.list_tabs()
|
|
95
|
+
tab = client.get_tab(tab_id)
|
|
96
|
+
|
|
97
|
+
# Top up (no extra activation fee)
|
|
98
|
+
tab = client.top_up_tab(tab_id, amount)
|
|
99
|
+
|
|
100
|
+
# Close (either party, unilateral)
|
|
101
|
+
tab = client.close_tab(tab_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Returns `Tab(tab_id, provider, amount, balance_remaining, total_charged, charge_count, max_charge_per_call, status)`.
|
|
105
|
+
|
|
106
|
+
### x402 Requests
|
|
107
|
+
|
|
108
|
+
Transparent HTTP 402 handling. The SDK detects `402 Payment Required`, pays (via direct or tab), and retries.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
response = client.request(url, method="GET", body=None, headers=None)
|
|
112
|
+
# Returns: httpx.Response
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
If the provider requires tab settlement, the SDK auto-opens a tab at 10x the per-call price (minimum $5).
|
|
116
|
+
|
|
117
|
+
### Wallet
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
status = client.get_status()
|
|
121
|
+
# Returns: StatusResponse(address, balance, open_tabs)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Webhooks
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
wh = client.register_webhook(url, events=["tab.opened"], secret="whsec_...")
|
|
128
|
+
webhooks = client.list_webhooks()
|
|
129
|
+
client.delete_webhook(webhook_id)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Funding
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
link = client.create_fund_link(amount=10_000_000) # Coinbase Onramp
|
|
136
|
+
link = client.create_withdraw_link(amount=5_000_000)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Signer Modes
|
|
140
|
+
|
|
141
|
+
| Mode | Usage | When |
|
|
142
|
+
|------|-------|------|
|
|
143
|
+
| `"cli"` | Subprocess call to `pay sign` | Default. Key in OS keychain. |
|
|
144
|
+
| `"raw"` | `PAYSKILL_KEY` env var | Dev/testing only. |
|
|
145
|
+
| `"custom"` | Your own callback | Custom key management. |
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
# CLI signer (default)
|
|
149
|
+
client = PayClient(signer="cli")
|
|
150
|
+
|
|
151
|
+
# Raw key (dev only)
|
|
152
|
+
client = PayClient(signer="raw", key="0xdead...")
|
|
153
|
+
|
|
154
|
+
# Custom callback
|
|
155
|
+
from payskill.signer import CallbackSigner
|
|
156
|
+
signer = CallbackSigner(callback=lambda hash_bytes: my_sign(hash_bytes))
|
|
157
|
+
client = PayClient(signer=signer)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Error Handling
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
from payskill.errors import (
|
|
164
|
+
PayError, # Base class
|
|
165
|
+
PayValidationError, # Bad input (has .field)
|
|
166
|
+
PayNetworkError, # Connection failed
|
|
167
|
+
PayServerError, # Server returned error (has .status_code)
|
|
168
|
+
PayInsufficientFundsError, # Not enough USDC
|
|
169
|
+
)
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Configuration
|
|
173
|
+
|
|
174
|
+
| Env Var | Purpose |
|
|
175
|
+
|---------|---------|
|
|
176
|
+
| `PAYSKILL_KEY` | Private key for raw signer mode |
|
|
177
|
+
|
|
178
|
+
The API URL is configurable via the `api_url` parameter. Default: `https://pay-skill.com/api/v1`.
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# pay-sdk
|
|
2
|
+
|
|
3
|
+
Python SDK for [pay](https://pay-skill.com) — payment infrastructure for AI agents. USDC on Base.
|
|
4
|
+
|
|
5
|
+
Three primitives: direct payments, tabs (pre-funded metered accounts), and x402 HTTP paywalls.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install pay-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Python 3.10+.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from payskill import PayClient
|
|
19
|
+
|
|
20
|
+
client = PayClient(signer="cli") # uses `pay sign` subprocess
|
|
21
|
+
|
|
22
|
+
# Pay another agent $5
|
|
23
|
+
result = client.pay_direct("0xprovider...", 5_000_000, memo="task-42")
|
|
24
|
+
print(result.tx_hash)
|
|
25
|
+
|
|
26
|
+
# Open a metered tab
|
|
27
|
+
tab = client.open_tab("0xprovider...", 20_000_000, max_charge_per_call=500_000)
|
|
28
|
+
|
|
29
|
+
# x402 request (SDK handles payment automatically)
|
|
30
|
+
response = client.request("https://api.example.com/data")
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
All amounts are in USDC micro-units (6 decimals). `$1.00 = 1_000_000`.
|
|
34
|
+
|
|
35
|
+
## API Reference
|
|
36
|
+
|
|
37
|
+
### PayClient
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from payskill import PayClient
|
|
41
|
+
|
|
42
|
+
client = PayClient(
|
|
43
|
+
api_url="https://pay-skill.com/api/v1", # default
|
|
44
|
+
signer="cli", # "cli", "raw", "custom", or a Signer instance
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Direct Payment
|
|
49
|
+
|
|
50
|
+
One-shot USDC transfer. $1.00 minimum.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
result = client.pay_direct(to, amount, memo="")
|
|
54
|
+
# Returns: DirectPaymentResult(tx_hash, status, amount, fee)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Tab Management
|
|
58
|
+
|
|
59
|
+
Pre-funded metered account. $5.00 minimum to open.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
# Open
|
|
63
|
+
tab = client.open_tab(provider, amount, max_charge_per_call)
|
|
64
|
+
|
|
65
|
+
# Query
|
|
66
|
+
tabs = client.list_tabs()
|
|
67
|
+
tab = client.get_tab(tab_id)
|
|
68
|
+
|
|
69
|
+
# Top up (no extra activation fee)
|
|
70
|
+
tab = client.top_up_tab(tab_id, amount)
|
|
71
|
+
|
|
72
|
+
# Close (either party, unilateral)
|
|
73
|
+
tab = client.close_tab(tab_id)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Returns `Tab(tab_id, provider, amount, balance_remaining, total_charged, charge_count, max_charge_per_call, status)`.
|
|
77
|
+
|
|
78
|
+
### x402 Requests
|
|
79
|
+
|
|
80
|
+
Transparent HTTP 402 handling. The SDK detects `402 Payment Required`, pays (via direct or tab), and retries.
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
response = client.request(url, method="GET", body=None, headers=None)
|
|
84
|
+
# Returns: httpx.Response
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
If the provider requires tab settlement, the SDK auto-opens a tab at 10x the per-call price (minimum $5).
|
|
88
|
+
|
|
89
|
+
### Wallet
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
status = client.get_status()
|
|
93
|
+
# Returns: StatusResponse(address, balance, open_tabs)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Webhooks
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
wh = client.register_webhook(url, events=["tab.opened"], secret="whsec_...")
|
|
100
|
+
webhooks = client.list_webhooks()
|
|
101
|
+
client.delete_webhook(webhook_id)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Funding
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
link = client.create_fund_link(amount=10_000_000) # Coinbase Onramp
|
|
108
|
+
link = client.create_withdraw_link(amount=5_000_000)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Signer Modes
|
|
112
|
+
|
|
113
|
+
| Mode | Usage | When |
|
|
114
|
+
|------|-------|------|
|
|
115
|
+
| `"cli"` | Subprocess call to `pay sign` | Default. Key in OS keychain. |
|
|
116
|
+
| `"raw"` | `PAYSKILL_KEY` env var | Dev/testing only. |
|
|
117
|
+
| `"custom"` | Your own callback | Custom key management. |
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# CLI signer (default)
|
|
121
|
+
client = PayClient(signer="cli")
|
|
122
|
+
|
|
123
|
+
# Raw key (dev only)
|
|
124
|
+
client = PayClient(signer="raw", key="0xdead...")
|
|
125
|
+
|
|
126
|
+
# Custom callback
|
|
127
|
+
from payskill.signer import CallbackSigner
|
|
128
|
+
signer = CallbackSigner(callback=lambda hash_bytes: my_sign(hash_bytes))
|
|
129
|
+
client = PayClient(signer=signer)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Error Handling
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from payskill.errors import (
|
|
136
|
+
PayError, # Base class
|
|
137
|
+
PayValidationError, # Bad input (has .field)
|
|
138
|
+
PayNetworkError, # Connection failed
|
|
139
|
+
PayServerError, # Server returned error (has .status_code)
|
|
140
|
+
PayInsufficientFundsError, # Not enough USDC
|
|
141
|
+
)
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Configuration
|
|
145
|
+
|
|
146
|
+
| Env Var | Purpose |
|
|
147
|
+
|---------|---------|
|
|
148
|
+
| `PAYSKILL_KEY` | Private key for raw signer mode |
|
|
149
|
+
|
|
150
|
+
The API URL is configurable via the `api_url` parameter. Default: `https://pay-skill.com/api/v1`.
|
|
151
|
+
|
|
152
|
+
## License
|
|
153
|
+
|
|
154
|
+
MIT
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pay-skill"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "Python SDK for pay — payment infrastructure for AI agents"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{name = "pay-skill.com", email = "hello@pay-skill.com"}]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Topic :: Office/Business :: Financial",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
dependencies = [
|
|
21
|
+
"httpx>=0.27",
|
|
22
|
+
"pydantic>=2.0",
|
|
23
|
+
"eth-account>=0.11",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://pay-skill.com"
|
|
28
|
+
Repository = "https://github.com/pay-skill/pay-sdk"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
ows = [
|
|
32
|
+
"open-wallet-standard>=1.2",
|
|
33
|
+
]
|
|
34
|
+
dev = [
|
|
35
|
+
"pytest>=8.0",
|
|
36
|
+
"pytest-asyncio>=0.23",
|
|
37
|
+
"pytest-cov>=5.0",
|
|
38
|
+
"pytest-timeout>=2.3",
|
|
39
|
+
"pytest-httpx>=0.30",
|
|
40
|
+
"ruff>=0.4",
|
|
41
|
+
"mypy>=1.10",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/payskill"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
target-version = "py310"
|
|
49
|
+
line-length = 100
|
|
50
|
+
|
|
51
|
+
[tool.ruff.lint]
|
|
52
|
+
select = ["E", "W", "F", "I", "N", "UP", "B", "S"]
|
|
53
|
+
ignore = ["S101", "S603"]
|
|
54
|
+
|
|
55
|
+
[tool.mypy]
|
|
56
|
+
python_version = "3.10"
|
|
57
|
+
strict = true
|
|
58
|
+
ignore_missing_imports = true
|
|
59
|
+
warn_unused_ignores = false
|
|
60
|
+
|
|
61
|
+
[tool.pytest.ini_options]
|
|
62
|
+
asyncio_mode = "auto"
|
|
63
|
+
testpaths = ["tests"]
|
|
64
|
+
markers = [
|
|
65
|
+
"e2e: end-to-end tests against live testnet (requires PAYSKILL_TESTNET_KEY)",
|
|
66
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""pay SDK — payment infrastructure for AI agents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from payskill.auth import build_auth_headers, derive_address
|
|
8
|
+
from payskill.client import PayClient
|
|
9
|
+
from payskill.errors import PayError, PayNetworkError, PayValidationError
|
|
10
|
+
from payskill.models import DirectPaymentResult, Tab, TabStatus
|
|
11
|
+
|
|
12
|
+
# OWS signer is optional — only available if open-wallet-standard is installed.
|
|
13
|
+
OwsSigner: Any
|
|
14
|
+
try:
|
|
15
|
+
from payskill.ows_signer import OwsSigner
|
|
16
|
+
except ImportError:
|
|
17
|
+
OwsSigner = None
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"PayClient",
|
|
21
|
+
"PayError",
|
|
22
|
+
"PayNetworkError",
|
|
23
|
+
"PayValidationError",
|
|
24
|
+
"DirectPaymentResult",
|
|
25
|
+
"Tab",
|
|
26
|
+
"TabStatus",
|
|
27
|
+
"build_auth_headers",
|
|
28
|
+
"derive_address",
|
|
29
|
+
"OwsSigner",
|
|
30
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""EIP-712 authentication for pay API requests.
|
|
2
|
+
|
|
3
|
+
Every authenticated request includes four headers:
|
|
4
|
+
X-Pay-Agent — wallet address (0x-prefixed, checksummed)
|
|
5
|
+
X-Pay-Signature — EIP-712 signature (0x-prefixed hex, 65 bytes)
|
|
6
|
+
X-Pay-Timestamp — unix timestamp in seconds
|
|
7
|
+
X-Pay-Nonce — random 32-byte hex (0x-prefixed)
|
|
8
|
+
|
|
9
|
+
The EIP-712 domain:
|
|
10
|
+
name: "pay"
|
|
11
|
+
version: "0.1"
|
|
12
|
+
chainId: <from config>
|
|
13
|
+
verifyingContract: <router address>
|
|
14
|
+
|
|
15
|
+
The typed data:
|
|
16
|
+
APIRequest(string method, string path, uint256 timestamp, bytes32 nonce)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from eth_account import Account
|
|
25
|
+
from eth_account.messages import encode_typed_data
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_auth_headers(
|
|
29
|
+
private_key: str,
|
|
30
|
+
method: str,
|
|
31
|
+
path: str,
|
|
32
|
+
chain_id: int,
|
|
33
|
+
router_address: str,
|
|
34
|
+
) -> dict[str, str]:
|
|
35
|
+
"""Build X-Pay-* auth headers for an API request.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
private_key: Hex-encoded private key (with or without 0x prefix).
|
|
39
|
+
method: HTTP method (GET, POST, DELETE, etc.).
|
|
40
|
+
path: Request path (e.g., /api/v1/direct).
|
|
41
|
+
chain_id: Chain ID for EIP-712 domain.
|
|
42
|
+
router_address: Router contract address for EIP-712 domain.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Dict with X-Pay-Agent, X-Pay-Signature, X-Pay-Timestamp, X-Pay-Nonce.
|
|
46
|
+
"""
|
|
47
|
+
account = Account.from_key(private_key)
|
|
48
|
+
timestamp = int(time.time())
|
|
49
|
+
nonce = "0x" + os.urandom(32).hex()
|
|
50
|
+
|
|
51
|
+
domain_data = {
|
|
52
|
+
"name": "pay",
|
|
53
|
+
"version": "0.1",
|
|
54
|
+
"chainId": chain_id,
|
|
55
|
+
"verifyingContract": router_address,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
message_types = {
|
|
59
|
+
"APIRequest": [
|
|
60
|
+
{"name": "method", "type": "string"},
|
|
61
|
+
{"name": "path", "type": "string"},
|
|
62
|
+
{"name": "timestamp", "type": "uint256"},
|
|
63
|
+
{"name": "nonce", "type": "bytes32"},
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
message_data = {
|
|
68
|
+
"method": method.upper(),
|
|
69
|
+
"path": path,
|
|
70
|
+
"timestamp": timestamp,
|
|
71
|
+
"nonce": bytes.fromhex(nonce[2:]),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
signable = encode_typed_data(
|
|
75
|
+
domain_data,
|
|
76
|
+
message_types,
|
|
77
|
+
message_data,
|
|
78
|
+
)
|
|
79
|
+
signed = account.sign_message(signable)
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"X-Pay-Agent": account.address,
|
|
83
|
+
"X-Pay-Signature": signed.signature.hex()
|
|
84
|
+
if isinstance(signed.signature, bytes)
|
|
85
|
+
else hex(signed.signature),
|
|
86
|
+
"X-Pay-Timestamp": str(timestamp),
|
|
87
|
+
"X-Pay-Nonce": nonce,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def derive_address(private_key: str) -> str:
|
|
92
|
+
"""Derive an Ethereum address from a private key.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
private_key: Hex-encoded private key (with or without 0x prefix).
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Checksummed Ethereum address.
|
|
99
|
+
"""
|
|
100
|
+
account = Account.from_key(private_key)
|
|
101
|
+
return str(account.address)
|