voidly-pay 2.0.2__tar.gz → 3.0.0__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.
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/PKG-INFO +30 -18
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/README.md +28 -16
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/pyproject.toml +3 -3
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/tests/test_sdk.py +96 -0
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay/__init__.py +156 -11
- voidly_pay-3.0.0/voidly_pay/middleware/__init__.py +140 -0
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay.egg-info/PKG-INFO +30 -18
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay.egg-info/SOURCES.txt +2 -1
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/setup.cfg +0 -0
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay.egg-info/dependency_links.txt +0 -0
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay.egg-info/requires.txt +0 -0
- {voidly_pay-2.0.2 → voidly_pay-3.0.0}/voidly_pay.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidly-pay
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: voidpay SDK — zero-value internal test-credit tools for agent-to-agent transfers and x402 integration. Credits are not backed or redeemable.
|
|
5
5
|
Author-email: Voidly <team@voidly.ai>
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: voidly,payments,ai-agents,x402,did,ed25519
|
|
@@ -35,7 +35,7 @@ pay = VoidlyPay() # mints + persists keypair
|
|
|
35
35
|
print("DID:", pay.did) # did:voidly:...
|
|
36
36
|
pay.faucet() # 10 free credits
|
|
37
37
|
|
|
38
|
-
# Browse the marketplace —
|
|
38
|
+
# Browse the marketplace — 12 paid endpoints + N third-party listings
|
|
39
39
|
mp = pay.request("GET", "/v1/pay/marketplace").json()
|
|
40
40
|
for item in mp["items"][:5]:
|
|
41
41
|
print(f"{item['name']:40s} ${item['pricing']['amount_usdc']}")
|
|
@@ -123,11 +123,17 @@ def expensive():
|
|
|
123
123
|
| **Streams (per-token billing)** | `pay.open_stream(...)`, `pay.meter_stream(...)`, `pay.finalize_stream(...)` |
|
|
124
124
|
| **Subscriptions** | `pay.subscribe(...)`, `pay.cancel_subscription(...)` |
|
|
125
125
|
| **x402 server-side quote** | `pay.create_quote(resource, amount)` |
|
|
126
|
-
| **x402 server-side verify** | `pay.verify_payment(
|
|
126
|
+
| **x402 server-side verify** | `pay.verify_payment(payment_header=header, payer_proof=proof, expected_resource=resource, expected_method=method, min_amount=price, expected_metadata=metadata)` |
|
|
127
127
|
| **Webhooks** | `pay.subscribe_webhook(url, events=[...])` |
|
|
128
128
|
| **Trust check** | `pay.health_check()` (6-check report incl. on-chain vault read) |
|
|
129
129
|
|
|
130
|
-
## What's in the marketplace today (Voidly's
|
|
130
|
+
## What's in the marketplace today (Voidly's 12 paid endpoints)
|
|
131
|
+
|
|
132
|
+
Voidly does not sell its censorship data. The dataset is free and CC BY 4.0.
|
|
133
|
+
Every endpoint below charges for compute, or for an Ed25519 signature over data
|
|
134
|
+
that is itself free — never for the observatory. Five SKUs that failed that test
|
|
135
|
+
were retired on 2026-08-04 and now return HTTP 410 naming their free
|
|
136
|
+
replacement; see [voidly.ai/pay/changelog](https://voidly.ai/pay/changelog).
|
|
131
137
|
|
|
132
138
|
| Endpoint | Price | What it does |
|
|
133
139
|
|---|---|---|
|
|
@@ -141,13 +147,19 @@ def expensive():
|
|
|
141
147
|
| `voidly_meta` | $0.001 | URL metadata (og + title + canonical) |
|
|
142
148
|
| `voidly_extract` | $0.01 | PDF/document → plain text |
|
|
143
149
|
| `voidly_scrape` | $0.01 | Fetch any URL + Voidly-signed receipt |
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
|
150
|
+
| `probe_attest` | $0.005 | Ed25519 attestation over probe reachability data. The data is free at `/v1/probe/domain/{host}`; the charge is the signature. |
|
|
151
|
+
| `agent_discover_pro` | $0.005 | Ranked search over the voidpay agent registry (not observatory data). Free 20-result search at `/v1/agent/discover`. |
|
|
152
|
+
|
|
153
|
+
Retired 2026-08-04 — these now return **HTTP 410 Gone** with the free
|
|
154
|
+
replacement URL in the body. Do not route to them:
|
|
155
|
+
|
|
156
|
+
| Retired | Free replacement |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `voidly_fetch` / `POST /v1/pay/fetch` | `voidly_scrape` — the country-pinning was never implemented, so every call was already a Cloudflare-edge fetch |
|
|
159
|
+
| `forecast_pro` | `GET /v1/forecast/{cc}/multi-horizon` (free, genuinely modelled) |
|
|
160
|
+
| `claim_verify_pro` | `POST /verify-claim` (free) |
|
|
161
|
+
| `incident_summary_pro` | `GET /data/incidents/{id}/report?format=markdown` (free) |
|
|
162
|
+
| `incidents_export_pro` | `GET /data/incidents/export?format=csv` (free, uncapped) |
|
|
151
163
|
|
|
152
164
|
Plus N third-party listings registered self-serve at [voidly.ai/pay/list-your-service](https://voidly.ai/pay/list-your-service).
|
|
153
165
|
|
|
@@ -178,17 +190,17 @@ ok = verify_webhook_signature(
|
|
|
178
190
|
|
|
179
191
|
## Why agents use this
|
|
180
192
|
|
|
181
|
-
| Problem |
|
|
193
|
+
| Problem | voidpay solves it |
|
|
182
194
|
|---|---|
|
|
183
195
|
| **Need to add payment to your agent service** | x402 middleware ships for FastAPI, Flask, any web-fetch handler |
|
|
184
|
-
| **Need to discover paid services** | One install →
|
|
196
|
+
| **Need to discover paid services** | One install → 12 endpoints + open marketplace listings |
|
|
185
197
|
| **Don't want to manage 10 API keys** | One Ed25519 keypair, one wallet, every paid endpoint works |
|
|
186
198
|
| **Don't trust the agent's payment claims** | Every receipt is Ed25519-signed by Voidly. Verifiable offline. |
|
|
187
|
-
| **Need
|
|
199
|
+
| **Need censorship data** | It is free. `GET /data/incidents/export?format=csv`, CC BY 4.0, no wallet, no key. Voidly does not sell it. |
|
|
188
200
|
|
|
189
201
|
## Honest disclosure
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
**Stage 2 is being retired** (decided 2026-08-04). Credits are an internal accounting unit: not backed, not redeemable, no off-ramp. The vault on Base mainnet (`0xd25d3c6f32886b65356cc5c700382a8a02d84df5`, [Sourcify-verified](https://repo.sourcify.dev/contracts/full_match/8453/0xd25d3c6f32886b65356cc5c700382a8a02d84df5/)) holds 4.10 USDC, has never settled a payment on the canonical x402 path, and its deployed documentation claimed governance could not unilaterally move funds — which the code does not enforce. It stays on-chain as a public record at [voidly.ai/pay/proof](https://voidly.ai/pay/proof). We have approximately zero sustained external paying users.
|
|
192
204
|
|
|
193
205
|
We opened the marketplace before the demand exists because we believe agent adoption is gated on discoverability, not on payment-rail UX.
|
|
194
206
|
|
|
@@ -207,9 +219,9 @@ We opened the marketplace before the demand exists because we believe agent adop
|
|
|
207
219
|
- [/pay/marketplace](https://voidly.ai/pay/marketplace) — visual browse
|
|
208
220
|
- [/pay/list-your-service](https://voidly.ai/pay/list-your-service) — list in 60s
|
|
209
221
|
- [/pay/claim](https://voidly.ai/pay/claim) — free 10-credit faucet
|
|
210
|
-
- [/pay/proof](https://voidly.ai/pay/proof) —
|
|
222
|
+
- [/pay/proof](https://voidly.ai/pay/proof) — vault record (not a reserve; Stage 2 is being retired)
|
|
211
223
|
- [/pay/for-builders](https://voidly.ai/pay/for-builders) — every middleware + adapter
|
|
212
|
-
- [
|
|
224
|
+
- [voidpay landing](https://voidly.ai/pay)
|
|
213
225
|
|
|
214
226
|
## Keywords
|
|
215
227
|
|
|
@@ -19,7 +19,7 @@ pay = VoidlyPay() # mints + persists keypair
|
|
|
19
19
|
print("DID:", pay.did) # did:voidly:...
|
|
20
20
|
pay.faucet() # 10 free credits
|
|
21
21
|
|
|
22
|
-
# Browse the marketplace —
|
|
22
|
+
# Browse the marketplace — 12 paid endpoints + N third-party listings
|
|
23
23
|
mp = pay.request("GET", "/v1/pay/marketplace").json()
|
|
24
24
|
for item in mp["items"][:5]:
|
|
25
25
|
print(f"{item['name']:40s} ${item['pricing']['amount_usdc']}")
|
|
@@ -107,11 +107,17 @@ def expensive():
|
|
|
107
107
|
| **Streams (per-token billing)** | `pay.open_stream(...)`, `pay.meter_stream(...)`, `pay.finalize_stream(...)` |
|
|
108
108
|
| **Subscriptions** | `pay.subscribe(...)`, `pay.cancel_subscription(...)` |
|
|
109
109
|
| **x402 server-side quote** | `pay.create_quote(resource, amount)` |
|
|
110
|
-
| **x402 server-side verify** | `pay.verify_payment(
|
|
110
|
+
| **x402 server-side verify** | `pay.verify_payment(payment_header=header, payer_proof=proof, expected_resource=resource, expected_method=method, min_amount=price, expected_metadata=metadata)` |
|
|
111
111
|
| **Webhooks** | `pay.subscribe_webhook(url, events=[...])` |
|
|
112
112
|
| **Trust check** | `pay.health_check()` (6-check report incl. on-chain vault read) |
|
|
113
113
|
|
|
114
|
-
## What's in the marketplace today (Voidly's
|
|
114
|
+
## What's in the marketplace today (Voidly's 12 paid endpoints)
|
|
115
|
+
|
|
116
|
+
Voidly does not sell its censorship data. The dataset is free and CC BY 4.0.
|
|
117
|
+
Every endpoint below charges for compute, or for an Ed25519 signature over data
|
|
118
|
+
that is itself free — never for the observatory. Five SKUs that failed that test
|
|
119
|
+
were retired on 2026-08-04 and now return HTTP 410 naming their free
|
|
120
|
+
replacement; see [voidly.ai/pay/changelog](https://voidly.ai/pay/changelog).
|
|
115
121
|
|
|
116
122
|
| Endpoint | Price | What it does |
|
|
117
123
|
|---|---|---|
|
|
@@ -125,13 +131,19 @@ def expensive():
|
|
|
125
131
|
| `voidly_meta` | $0.001 | URL metadata (og + title + canonical) |
|
|
126
132
|
| `voidly_extract` | $0.01 | PDF/document → plain text |
|
|
127
133
|
| `voidly_scrape` | $0.01 | Fetch any URL + Voidly-signed receipt |
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
|
134
|
+
| `probe_attest` | $0.005 | Ed25519 attestation over probe reachability data. The data is free at `/v1/probe/domain/{host}`; the charge is the signature. |
|
|
135
|
+
| `agent_discover_pro` | $0.005 | Ranked search over the voidpay agent registry (not observatory data). Free 20-result search at `/v1/agent/discover`. |
|
|
136
|
+
|
|
137
|
+
Retired 2026-08-04 — these now return **HTTP 410 Gone** with the free
|
|
138
|
+
replacement URL in the body. Do not route to them:
|
|
139
|
+
|
|
140
|
+
| Retired | Free replacement |
|
|
141
|
+
|---|---|
|
|
142
|
+
| `voidly_fetch` / `POST /v1/pay/fetch` | `voidly_scrape` — the country-pinning was never implemented, so every call was already a Cloudflare-edge fetch |
|
|
143
|
+
| `forecast_pro` | `GET /v1/forecast/{cc}/multi-horizon` (free, genuinely modelled) |
|
|
144
|
+
| `claim_verify_pro` | `POST /verify-claim` (free) |
|
|
145
|
+
| `incident_summary_pro` | `GET /data/incidents/{id}/report?format=markdown` (free) |
|
|
146
|
+
| `incidents_export_pro` | `GET /data/incidents/export?format=csv` (free, uncapped) |
|
|
135
147
|
|
|
136
148
|
Plus N third-party listings registered self-serve at [voidly.ai/pay/list-your-service](https://voidly.ai/pay/list-your-service).
|
|
137
149
|
|
|
@@ -162,17 +174,17 @@ ok = verify_webhook_signature(
|
|
|
162
174
|
|
|
163
175
|
## Why agents use this
|
|
164
176
|
|
|
165
|
-
| Problem |
|
|
177
|
+
| Problem | voidpay solves it |
|
|
166
178
|
|---|---|
|
|
167
179
|
| **Need to add payment to your agent service** | x402 middleware ships for FastAPI, Flask, any web-fetch handler |
|
|
168
|
-
| **Need to discover paid services** | One install →
|
|
180
|
+
| **Need to discover paid services** | One install → 12 endpoints + open marketplace listings |
|
|
169
181
|
| **Don't want to manage 10 API keys** | One Ed25519 keypair, one wallet, every paid endpoint works |
|
|
170
182
|
| **Don't trust the agent's payment claims** | Every receipt is Ed25519-signed by Voidly. Verifiable offline. |
|
|
171
|
-
| **Need
|
|
183
|
+
| **Need censorship data** | It is free. `GET /data/incidents/export?format=csv`, CC BY 4.0, no wallet, no key. Voidly does not sell it. |
|
|
172
184
|
|
|
173
185
|
## Honest disclosure
|
|
174
186
|
|
|
175
|
-
|
|
187
|
+
**Stage 2 is being retired** (decided 2026-08-04). Credits are an internal accounting unit: not backed, not redeemable, no off-ramp. The vault on Base mainnet (`0xd25d3c6f32886b65356cc5c700382a8a02d84df5`, [Sourcify-verified](https://repo.sourcify.dev/contracts/full_match/8453/0xd25d3c6f32886b65356cc5c700382a8a02d84df5/)) holds 4.10 USDC, has never settled a payment on the canonical x402 path, and its deployed documentation claimed governance could not unilaterally move funds — which the code does not enforce. It stays on-chain as a public record at [voidly.ai/pay/proof](https://voidly.ai/pay/proof). We have approximately zero sustained external paying users.
|
|
176
188
|
|
|
177
189
|
We opened the marketplace before the demand exists because we believe agent adoption is gated on discoverability, not on payment-rail UX.
|
|
178
190
|
|
|
@@ -191,9 +203,9 @@ We opened the marketplace before the demand exists because we believe agent adop
|
|
|
191
203
|
- [/pay/marketplace](https://voidly.ai/pay/marketplace) — visual browse
|
|
192
204
|
- [/pay/list-your-service](https://voidly.ai/pay/list-your-service) — list in 60s
|
|
193
205
|
- [/pay/claim](https://voidly.ai/pay/claim) — free 10-credit faucet
|
|
194
|
-
- [/pay/proof](https://voidly.ai/pay/proof) —
|
|
206
|
+
- [/pay/proof](https://voidly.ai/pay/proof) — vault record (not a reserve; Stage 2 is being retired)
|
|
195
207
|
- [/pay/for-builders](https://voidly.ai/pay/for-builders) — every middleware + adapter
|
|
196
|
-
- [
|
|
208
|
+
- [voidpay landing](https://voidly.ai/pay)
|
|
197
209
|
|
|
198
210
|
## Keywords
|
|
199
211
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "voidly-pay"
|
|
3
|
-
version = "
|
|
4
|
-
description = "
|
|
3
|
+
version = "3.0.0"
|
|
4
|
+
description = "voidpay SDK — zero-value internal test-credit tools for agent-to-agent transfers and x402 integration. Credits are not backed or redeemable."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -20,4 +20,4 @@ requires = ["setuptools>=61"]
|
|
|
20
20
|
build-backend = "setuptools.build_meta"
|
|
21
21
|
|
|
22
22
|
[tool.setuptools]
|
|
23
|
-
packages = ["voidly_pay"]
|
|
23
|
+
packages = ["voidly_pay", "voidly_pay.middleware"]
|
|
@@ -143,6 +143,102 @@ class TransferTests(unittest.TestCase):
|
|
|
143
143
|
self.assertEqual(ctx.exception.code, "insufficient_balance")
|
|
144
144
|
|
|
145
145
|
|
|
146
|
+
class X402Tests(unittest.TestCase):
|
|
147
|
+
def test_verify_payment_signs_every_merchant_expectation(self):
|
|
148
|
+
sess, captured = fake_session({
|
|
149
|
+
"POST /v1/pay/x402/verify": resp_json({"ok": True, "quote_id": "q-1", "transfer_id": "t-1"}),
|
|
150
|
+
})
|
|
151
|
+
pay = VoidlyPay(api_url="http://x.example", secret_key=bytes(nacl.signing.SigningKey.generate()._signing_key), session=sess)
|
|
152
|
+
pay.verify_payment(
|
|
153
|
+
payment_header="voidly-credit transfer_id=t-1; quote_id=q-1",
|
|
154
|
+
payer_proof="signed-payer-proof",
|
|
155
|
+
expected_resource=["service:read", "service:read:v2"],
|
|
156
|
+
expected_method="POST",
|
|
157
|
+
min_amount=0.004,
|
|
158
|
+
expected_recipient="did:voidly:merchant-payee",
|
|
159
|
+
expected_currency="voidly-credit",
|
|
160
|
+
expected_metadata={"order_id": "order-a", "sku": "report"},
|
|
161
|
+
)
|
|
162
|
+
call = next(c for c in captured if c["url"].endswith("/v1/pay/x402/verify"))
|
|
163
|
+
env = call["json_body"]["envelope"]
|
|
164
|
+
self.assertEqual(env["payer_proof"], "signed-payer-proof")
|
|
165
|
+
self.assertEqual(env["expected_resource"], ["service:read", "service:read:v2"])
|
|
166
|
+
self.assertEqual(env["expected_method"], "POST")
|
|
167
|
+
self.assertEqual(env["expected_recipient_did"], "did:voidly:merchant-payee")
|
|
168
|
+
self.assertEqual(env["min_amount_micro"], 4_000)
|
|
169
|
+
self.assertEqual(env["expected_currency"], "voidly-credit")
|
|
170
|
+
self.assertEqual(env["expected_metadata"], {"order_id": "order-a", "sku": "report"})
|
|
171
|
+
|
|
172
|
+
def test_verify_payment_returns_structured_4xx_refusal(self):
|
|
173
|
+
sess, _ = fake_session({
|
|
174
|
+
"POST /v1/pay/x402/verify": resp_json(
|
|
175
|
+
{"ok": False, "reason": "quote_already_consumed"}, status=409,
|
|
176
|
+
),
|
|
177
|
+
})
|
|
178
|
+
pay = VoidlyPay(api_url="http://x.example", secret_key=bytes(nacl.signing.SigningKey.generate()._signing_key), session=sess)
|
|
179
|
+
result = pay.verify_payment(
|
|
180
|
+
payment_header="voidly-credit transfer_id=t-1; quote_id=q-1",
|
|
181
|
+
payer_proof="signed-payer-proof",
|
|
182
|
+
expected_resource="/goods",
|
|
183
|
+
expected_method=None,
|
|
184
|
+
min_amount=0.001,
|
|
185
|
+
expected_metadata={},
|
|
186
|
+
)
|
|
187
|
+
self.assertEqual(result, {"ok": False, "reason": "quote_already_consumed"})
|
|
188
|
+
|
|
189
|
+
def test_verify_payment_rejects_hollow_terms_without_request(self):
|
|
190
|
+
sess, captured = fake_session({})
|
|
191
|
+
pay = VoidlyPay(api_url="http://x.example", secret_key=bytes(nacl.signing.SigningKey.generate()._signing_key), session=sess)
|
|
192
|
+
with self.assertRaises(VoidlyPayError):
|
|
193
|
+
pay.verify_payment(
|
|
194
|
+
payment_header="x", payer_proof="proof", expected_resource="", expected_method=None, min_amount=0.001, expected_metadata={},
|
|
195
|
+
)
|
|
196
|
+
with self.assertRaises(VoidlyPayError):
|
|
197
|
+
pay.verify_payment(
|
|
198
|
+
payment_header="x", payer_proof="proof", expected_resource="/r", expected_method=None, min_amount=0.0000001, expected_metadata={},
|
|
199
|
+
)
|
|
200
|
+
self.assertEqual(captured, [])
|
|
201
|
+
|
|
202
|
+
def test_request_with_pay_relays_a_bound_payer_proof(self):
|
|
203
|
+
quote = {
|
|
204
|
+
"x402": {"accepts": [{
|
|
205
|
+
"scheme": "voidly-credit", "quote_id": "q-1", "recipient_did": "did:voidly:merchant",
|
|
206
|
+
"resource": "/goods", "amount_micro": 1_000,
|
|
207
|
+
}]},
|
|
208
|
+
}
|
|
209
|
+
first = resp_json(quote, status=402)
|
|
210
|
+
second = resp_json({"goods": True})
|
|
211
|
+
transfer = resp_json({"transfer_id": "t-1", "status": "settled"})
|
|
212
|
+
sess, captured = fake_session({
|
|
213
|
+
"GET /goods": first,
|
|
214
|
+
"POST /v1/pay/wallet": resp_json({"wallet": {}}),
|
|
215
|
+
"POST /v1/pay/transfer": transfer,
|
|
216
|
+
})
|
|
217
|
+
calls = {"goods": 0}
|
|
218
|
+
|
|
219
|
+
def route(method, url, **kwargs):
|
|
220
|
+
from urllib.parse import urlparse
|
|
221
|
+
if method.upper() == "GET" and urlparse(url).path == "/goods":
|
|
222
|
+
captured.append({"method": "GET", "url": url, "json_body": None, "headers": kwargs.get("headers", {})})
|
|
223
|
+
calls["goods"] += 1
|
|
224
|
+
return first if calls["goods"] == 1 else second
|
|
225
|
+
return fake_session({
|
|
226
|
+
"POST /v1/pay/wallet": resp_json({"wallet": {}}),
|
|
227
|
+
"POST /v1/pay/transfer": transfer,
|
|
228
|
+
})[0].request(method, url, **kwargs)
|
|
229
|
+
|
|
230
|
+
sess.request = mock.Mock(side_effect=route)
|
|
231
|
+
pay = VoidlyPay(api_url="http://x.example", secret_key=bytes(nacl.signing.SigningKey.generate()._signing_key), session=sess)
|
|
232
|
+
result = pay.request_with_pay("GET", "http://merchant.example/goods")
|
|
233
|
+
self.assertEqual(result.status_code, 200)
|
|
234
|
+
retry = [c for c in captured if c["method"] == "GET"][-1]
|
|
235
|
+
self.assertIn("X-Payment", retry["headers"])
|
|
236
|
+
proof = retry["headers"]["X-Voidly-Payer-Proof"]
|
|
237
|
+
decoded = json.loads(base64.b64decode(proof).decode())
|
|
238
|
+
self.assertEqual(decoded["envelope"]["quote_id"], "q-1")
|
|
239
|
+
self.assertEqual(decoded["envelope"]["transfer_id"], "t-1")
|
|
240
|
+
|
|
241
|
+
|
|
146
242
|
class WebhookSignatureTests(unittest.TestCase):
|
|
147
243
|
def test_valid_signature(self):
|
|
148
244
|
secret = "deadbeef" * 8
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""voidpay — Python SDK for agent-to-agent payments.
|
|
2
2
|
|
|
3
3
|
Quick start:
|
|
4
4
|
|
|
@@ -21,6 +21,7 @@ import base64
|
|
|
21
21
|
import hashlib
|
|
22
22
|
import hmac
|
|
23
23
|
import json
|
|
24
|
+
import math
|
|
24
25
|
import os
|
|
25
26
|
import secrets
|
|
26
27
|
import time
|
|
@@ -32,7 +33,7 @@ from typing import Any, Iterable, Mapping, Optional
|
|
|
32
33
|
import nacl.signing
|
|
33
34
|
import requests
|
|
34
35
|
|
|
35
|
-
__version__ = "0.
|
|
36
|
+
__version__ = "3.0.0"
|
|
36
37
|
|
|
37
38
|
DEFAULT_API = "https://api.voidly.ai"
|
|
38
39
|
MICRO_PER_CREDIT = 1_000_000
|
|
@@ -193,8 +194,33 @@ class VoidlyPay:
|
|
|
193
194
|
|
|
194
195
|
# ─── HTTP ──────────────────────────────────────────────────────────────
|
|
195
196
|
|
|
196
|
-
def
|
|
197
|
+
def proof_header(self, did: str | None = None) -> dict:
|
|
198
|
+
"""Mint a short-lived ownership proof for ``did`` (default: our own).
|
|
199
|
+
|
|
200
|
+
voidpay redacts relationship metadata -- counterparty DIDs, memos,
|
|
201
|
+
task payloads and row ids -- from every unauthenticated read, because
|
|
202
|
+
those endpoints are public and a stranger could otherwise rebuild who
|
|
203
|
+
deals with whom. This header proves you are a party to the rows you
|
|
204
|
+
are asking for, and the server then returns the full view.
|
|
205
|
+
|
|
206
|
+
A 60-second Ed25519 signature over a caller-chosen window: no new key
|
|
207
|
+
material, no round-trip, nothing to store.
|
|
208
|
+
"""
|
|
209
|
+
env = {
|
|
210
|
+
"schema": "voidly-pay-history-read/v1",
|
|
211
|
+
"did": did or self.did,
|
|
212
|
+
"nonce": uuid.uuid4().hex,
|
|
213
|
+
"issued_at": self._now_iso(),
|
|
214
|
+
"expires_at": self._expires(60_000),
|
|
215
|
+
}
|
|
216
|
+
blob = json.dumps({"envelope": env, "signature": self._sign(env)}, separators=(",", ":"))
|
|
217
|
+
return {"x-voidly-proof": base64.b64encode(blob.encode()).decode()}
|
|
218
|
+
|
|
219
|
+
def _req(self, method: str, path: str, body: Any = None, idempotency: str | None = None,
|
|
220
|
+
headers_extra: dict | None = None) -> dict:
|
|
197
221
|
headers = {"accept": "application/json"}
|
|
222
|
+
if headers_extra:
|
|
223
|
+
headers.update(headers_extra)
|
|
198
224
|
if body is not None:
|
|
199
225
|
headers["content-type"] = "application/json"
|
|
200
226
|
if idempotency:
|
|
@@ -231,10 +257,28 @@ class VoidlyPay:
|
|
|
231
257
|
Idempotent: re-calling for an already-registered DID is safe.
|
|
232
258
|
Uses the test-mode endpoint which auto-grants a starter balance
|
|
233
259
|
(Stage 1) for instant onboarding.
|
|
260
|
+
|
|
261
|
+
The route used to accept a bare {did, signing_public_key} from any
|
|
262
|
+
caller. Since it mints 1,000 credits, that let anyone print against any
|
|
263
|
+
DID they cared to name. It now requires a signed proof of control,
|
|
264
|
+
which costs this SDK nothing — it already holds the key the DID is
|
|
265
|
+
derived from.
|
|
234
266
|
"""
|
|
235
|
-
|
|
267
|
+
envelope = {
|
|
268
|
+
"schema": "voidly-pay-test-wallet-create/v1",
|
|
236
269
|
"did": self.did,
|
|
237
270
|
"signing_public_key": self.public_key,
|
|
271
|
+
"nonce": os.urandom(16).hex(),
|
|
272
|
+
"issued_at": self._now_iso(),
|
|
273
|
+
# The server caps the signed window at 10 minutes; the SDK-wide
|
|
274
|
+
# default expiry is 30, so this one is set explicitly rather than
|
|
275
|
+
# inherited — an inherited 30 would be rejected as
|
|
276
|
+
# envelope_window_too_long.
|
|
277
|
+
"expires_at": self._expires(5 * 60_000),
|
|
278
|
+
}
|
|
279
|
+
return self._req("POST", "/v1/pay/test/wallet/create", {
|
|
280
|
+
"envelope": envelope,
|
|
281
|
+
"signature": self._sign(envelope),
|
|
238
282
|
})
|
|
239
283
|
|
|
240
284
|
def faucet(self) -> dict:
|
|
@@ -337,7 +381,9 @@ class VoidlyPay:
|
|
|
337
381
|
return r
|
|
338
382
|
|
|
339
383
|
def get_transfer(self, transfer_id: str) -> dict:
|
|
340
|
-
|
|
384
|
+
# Counterparties and memo are withheld from non-parties; prove we are one.
|
|
385
|
+
return self._req("GET", f"/v1/pay/transfer/{transfer_id}",
|
|
386
|
+
headers_extra=self.proof_header()).get("transfer", {})
|
|
341
387
|
|
|
342
388
|
def history(self, did: str | None = None, limit: int | None = None, before: str | None = None) -> dict:
|
|
343
389
|
params = []
|
|
@@ -346,7 +392,9 @@ class VoidlyPay:
|
|
|
346
392
|
if before:
|
|
347
393
|
params.append(f"before={requests.utils.quote(before)}")
|
|
348
394
|
qs = ("?" + "&".join(params)) if params else ""
|
|
349
|
-
|
|
395
|
+
target = did or self.did
|
|
396
|
+
return self._req("GET", f"/v1/pay/history/{target}{qs}",
|
|
397
|
+
headers_extra=self.proof_header(target))
|
|
350
398
|
|
|
351
399
|
# ─── Escrow ────────────────────────────────────────────────────────────
|
|
352
400
|
|
|
@@ -417,18 +465,106 @@ class VoidlyPay:
|
|
|
417
465
|
}
|
|
418
466
|
return self._req("POST", "/v1/pay/x402/quote", {"envelope": env, "signature": self._sign(env)})
|
|
419
467
|
|
|
420
|
-
def verify_payment(
|
|
468
|
+
def verify_payment(
|
|
469
|
+
self,
|
|
470
|
+
quote_id: str = "",
|
|
471
|
+
transfer_id: str | None = None,
|
|
472
|
+
payment_header: str | None = None,
|
|
473
|
+
*,
|
|
474
|
+
payer_proof: str | None,
|
|
475
|
+
expected_resource: str | list[str],
|
|
476
|
+
expected_method: str | None,
|
|
477
|
+
min_amount: float,
|
|
478
|
+
expected_metadata: dict[str, Any],
|
|
479
|
+
expected_recipient: str | None = None,
|
|
480
|
+
expected_currency: str = "voidly-credit",
|
|
481
|
+
) -> dict:
|
|
482
|
+
"""One-shot verify a customer's payment against this merchant's terms.
|
|
483
|
+
|
|
484
|
+
The payer proof and every authorization fact are required keyword
|
|
485
|
+
arguments. Older call shapes therefore fail locally instead of asking
|
|
486
|
+
the facilitator whether an attacker-selected quote was merely
|
|
487
|
+
internally consistent.
|
|
488
|
+
"""
|
|
489
|
+
if isinstance(min_amount, bool) or not isinstance(min_amount, (int, float)) or not math.isfinite(min_amount):
|
|
490
|
+
min_amount_micro = 0
|
|
491
|
+
else:
|
|
492
|
+
min_amount_micro = round(min_amount * MICRO_PER_CREDIT)
|
|
493
|
+
if min_amount_micro <= 0 or min_amount_micro > 2**53 - 1:
|
|
494
|
+
raise VoidlyPayError(
|
|
495
|
+
code="x402_expected_amount_invalid",
|
|
496
|
+
message="verify_payment min_amount must resolve to at least one safe micro-credit",
|
|
497
|
+
status=400,
|
|
498
|
+
)
|
|
499
|
+
resources = [expected_resource] if isinstance(expected_resource, str) else expected_resource
|
|
500
|
+
if (
|
|
501
|
+
not isinstance(resources, list)
|
|
502
|
+
or not 1 <= len(resources) <= 16
|
|
503
|
+
or any(not isinstance(resource, str) or not 1 <= len(resource) <= 512 for resource in resources)
|
|
504
|
+
):
|
|
505
|
+
raise VoidlyPayError(
|
|
506
|
+
code="x402_expected_resource_invalid",
|
|
507
|
+
message="verify_payment expected_resource must name one to sixteen non-empty resources",
|
|
508
|
+
status=400,
|
|
509
|
+
)
|
|
510
|
+
if not isinstance(expected_metadata, dict):
|
|
511
|
+
raise VoidlyPayError(
|
|
512
|
+
code="x402_expected_metadata_invalid",
|
|
513
|
+
message="verify_payment expected_metadata must be a mapping",
|
|
514
|
+
status=400,
|
|
515
|
+
)
|
|
516
|
+
if expected_method is not None and (
|
|
517
|
+
not isinstance(expected_method, str) or not 1 <= len(expected_method) <= 16
|
|
518
|
+
):
|
|
519
|
+
raise VoidlyPayError(
|
|
520
|
+
code="x402_expected_method_invalid",
|
|
521
|
+
message="verify_payment expected_method must be a non-empty HTTP method or None for a methodless quote",
|
|
522
|
+
status=400,
|
|
523
|
+
)
|
|
421
524
|
env = {
|
|
422
525
|
"schema": "voidly-x402-verify/v1",
|
|
423
526
|
"server_did": self.did,
|
|
424
527
|
"quote_id": quote_id,
|
|
425
528
|
"transfer_id": transfer_id,
|
|
426
529
|
"payment_header": payment_header,
|
|
530
|
+
"payer_proof": payer_proof,
|
|
531
|
+
"expected_resource": expected_resource,
|
|
532
|
+
"expected_method": expected_method,
|
|
533
|
+
"expected_recipient_did": self.did if expected_recipient is None else expected_recipient,
|
|
534
|
+
"min_amount_micro": min_amount_micro,
|
|
535
|
+
"expected_currency": expected_currency,
|
|
536
|
+
"expected_metadata": expected_metadata,
|
|
427
537
|
"nonce": uuid.uuid4().hex,
|
|
428
538
|
"issued_at": self._now_iso(),
|
|
429
539
|
"expires_at": self._expires(),
|
|
430
540
|
}
|
|
431
|
-
|
|
541
|
+
try:
|
|
542
|
+
return self._req("POST", "/v1/pay/x402/verify", {"envelope": env, "signature": self._sign(env)})
|
|
543
|
+
except VoidlyPayError as error:
|
|
544
|
+
# A verifier refusal is a normal result for server middleware. Do
|
|
545
|
+
# not turn an invalid/replayed payment into an application 500.
|
|
546
|
+
# Infrastructure failures remain exceptions.
|
|
547
|
+
if 400 <= error.status < 500:
|
|
548
|
+
raw = error.raw if isinstance(error.raw, dict) else {}
|
|
549
|
+
raw_error = raw.get("error") if isinstance(raw.get("error"), dict) else {}
|
|
550
|
+
reason = raw.get("reason") or raw_error.get("code") or error.code
|
|
551
|
+
return {"ok": False, "reason": reason}
|
|
552
|
+
raise
|
|
553
|
+
|
|
554
|
+
def payer_proof(self, quote_id: str, transfer_id: str, ttl_ms: int = 120_000) -> str:
|
|
555
|
+
"""Mint the single-use ``x-voidly-payer-proof`` value for one pair."""
|
|
556
|
+
bounded_ttl_ms = max(1, min(int(ttl_ms), 120_000))
|
|
557
|
+
env = {
|
|
558
|
+
"schema": "voidly-pay-payer-binding/v1",
|
|
559
|
+
"did": self.did,
|
|
560
|
+
"quote_id": quote_id,
|
|
561
|
+
"transfer_id": transfer_id,
|
|
562
|
+
"nonce": uuid.uuid4().hex,
|
|
563
|
+
"issued_at": self._now_iso(),
|
|
564
|
+
"expires_at": self._expires(bounded_ttl_ms),
|
|
565
|
+
}
|
|
566
|
+
blob = json.dumps({"envelope": env, "signature": self._sign(env)}, separators=(",", ":"))
|
|
567
|
+
return base64.b64encode(blob.encode("utf-8")).decode("ascii")
|
|
432
568
|
|
|
433
569
|
# ─── x402 (client-side: pay-on-402) ────────────────────────────────────
|
|
434
570
|
|
|
@@ -462,6 +598,7 @@ class VoidlyPay:
|
|
|
462
598
|
t = self.transfer(to=accept["recipient_did"], amount=credits, memo=f"x402: {accept['resource']}")
|
|
463
599
|
headers = dict(kwargs.pop("headers", {}) or {})
|
|
464
600
|
headers["X-Payment"] = f"voidly-credit transfer_id={t['transfer_id']}; quote_id={accept['quote_id']}"
|
|
601
|
+
headers["X-Voidly-Payer-Proof"] = self.payer_proof(accept["quote_id"], t["transfer_id"])
|
|
465
602
|
return self.session.request(method, url, headers=headers, **kwargs)
|
|
466
603
|
|
|
467
604
|
# ─── Webhooks ──────────────────────────────────────────────────────────
|
|
@@ -576,17 +713,25 @@ class VoidlyPay:
|
|
|
576
713
|
return self._req("POST", "/v1/pay/hire", {"envelope": env, "signature": self._sign(env)})
|
|
577
714
|
|
|
578
715
|
def hire_get(self, hire_id: str) -> dict:
|
|
579
|
-
|
|
716
|
+
# A hire row names who bought what and carries the task payload, so it
|
|
717
|
+
# is redacted for anyone who is not the requester or the provider.
|
|
718
|
+
return self._req("GET", f"/v1/pay/hire/{hire_id}", headers_extra=self.proof_header())
|
|
580
719
|
|
|
581
720
|
def hires_incoming(self, did: str | None = None, state: str | None = None, limit: int = 50) -> dict:
|
|
582
721
|
target = did or self.did
|
|
583
722
|
qs = f"?limit={limit}" + (f"&state={requests.utils.quote(state)}" if state else "")
|
|
584
|
-
|
|
723
|
+
# A provider polling for work MUST send the proof: without it the list
|
|
724
|
+
# comes back with no hire ids and no inputs. That is deliberate —
|
|
725
|
+
# naming a DID is not the same as controlling it, and /v1/agent/discover
|
|
726
|
+
# hands out every DID for free.
|
|
727
|
+
return self._req("GET", f"/v1/pay/hire/incoming/{target}{qs}",
|
|
728
|
+
headers_extra=self.proof_header(target))
|
|
585
729
|
|
|
586
730
|
def hires_outgoing(self, did: str | None = None, state: str | None = None, limit: int = 50) -> dict:
|
|
587
731
|
target = did or self.did
|
|
588
732
|
qs = f"?limit={limit}" + (f"&state={requests.utils.quote(state)}" if state else "")
|
|
589
|
-
return self._req("GET", f"/v1/pay/hire/outgoing/{target}{qs}"
|
|
733
|
+
return self._req("GET", f"/v1/pay/hire/outgoing/{target}{qs}",
|
|
734
|
+
headers_extra=self.proof_header(target))
|
|
590
735
|
|
|
591
736
|
def hire_and_wait(
|
|
592
737
|
self,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""voidpay middleware for popular Python web frameworks.
|
|
2
|
+
|
|
3
|
+
Each helper turns a plain handler into an x402-protected paywall.
|
|
4
|
+
Pay-by-call: the function-level decorator (FastAPI dependency / Flask
|
|
5
|
+
decorator) is what most users want.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, Awaitable, Callable
|
|
11
|
+
|
|
12
|
+
from ..__init__ import VoidlyPay
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def fastapi_x402(pay: VoidlyPay, amount: float, *, resource: str | None = None,
|
|
16
|
+
recipient: str | None = None, description: str | None = None,
|
|
17
|
+
metadata: dict[str, Any] | None = None,
|
|
18
|
+
ttl_seconds: int | None = None):
|
|
19
|
+
"""FastAPI dependency. Returns a callable to use as a Depends().
|
|
20
|
+
|
|
21
|
+
Usage::
|
|
22
|
+
|
|
23
|
+
from fastapi import Depends, FastAPI
|
|
24
|
+
from voidly_pay import VoidlyPay
|
|
25
|
+
from voidly_pay.middleware import fastapi_x402
|
|
26
|
+
|
|
27
|
+
app = FastAPI()
|
|
28
|
+
pay = VoidlyPay()
|
|
29
|
+
require_payment = fastapi_x402(pay, amount=0.01)
|
|
30
|
+
|
|
31
|
+
@app.get("/expensive")
|
|
32
|
+
def expensive(payment = Depends(require_payment)):
|
|
33
|
+
return {"data": "the goods", "payer": payment["payer_did"]}
|
|
34
|
+
"""
|
|
35
|
+
from fastapi import Header, HTTPException, Request, Response
|
|
36
|
+
|
|
37
|
+
async def _dep(
|
|
38
|
+
request: Request,
|
|
39
|
+
x_payment: str | None = Header(None),
|
|
40
|
+
x_voidly_payer_proof: str | None = Header(None),
|
|
41
|
+
):
|
|
42
|
+
if x_payment is None:
|
|
43
|
+
quote = pay.create_quote(
|
|
44
|
+
resource=resource or request.url.path,
|
|
45
|
+
amount=amount,
|
|
46
|
+
recipient=recipient,
|
|
47
|
+
method=request.method,
|
|
48
|
+
description=description,
|
|
49
|
+
metadata=metadata,
|
|
50
|
+
ttl_seconds=ttl_seconds,
|
|
51
|
+
)
|
|
52
|
+
raise HTTPException(
|
|
53
|
+
status_code=402,
|
|
54
|
+
detail=quote["payment_required_response"],
|
|
55
|
+
headers={"WWW-Authenticate": "x402"},
|
|
56
|
+
)
|
|
57
|
+
v = pay.verify_payment(
|
|
58
|
+
payment_header=x_payment,
|
|
59
|
+
payer_proof=x_voidly_payer_proof,
|
|
60
|
+
expected_resource=resource or request.url.path,
|
|
61
|
+
expected_method=request.method,
|
|
62
|
+
min_amount=amount,
|
|
63
|
+
expected_recipient=recipient or pay.did,
|
|
64
|
+
expected_currency="voidly-credit",
|
|
65
|
+
expected_metadata=metadata or {},
|
|
66
|
+
)
|
|
67
|
+
if not v.get("ok"):
|
|
68
|
+
raise HTTPException(
|
|
69
|
+
status_code=402,
|
|
70
|
+
detail={"error": {"code": "x402_verify_failed", "message": v.get("reason")}},
|
|
71
|
+
headers={"WWW-Authenticate": "x402"},
|
|
72
|
+
)
|
|
73
|
+
return v
|
|
74
|
+
return _dep
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def flask_x402(pay: VoidlyPay, amount: float, *, resource: str | None = None,
|
|
78
|
+
recipient: str | None = None, description: str | None = None,
|
|
79
|
+
metadata: dict[str, Any] | None = None,
|
|
80
|
+
ttl_seconds: int | None = None):
|
|
81
|
+
"""Flask decorator factory. Wrap a view function.
|
|
82
|
+
|
|
83
|
+
Usage::
|
|
84
|
+
|
|
85
|
+
from flask import Flask
|
|
86
|
+
from voidly_pay import VoidlyPay
|
|
87
|
+
from voidly_pay.middleware import flask_x402
|
|
88
|
+
|
|
89
|
+
app = Flask(__name__)
|
|
90
|
+
pay = VoidlyPay()
|
|
91
|
+
require_payment = flask_x402(pay, amount=0.01)
|
|
92
|
+
|
|
93
|
+
@app.get("/expensive")
|
|
94
|
+
@require_payment
|
|
95
|
+
def expensive():
|
|
96
|
+
return {"data": "the goods"}
|
|
97
|
+
"""
|
|
98
|
+
from functools import wraps
|
|
99
|
+
from flask import jsonify, request
|
|
100
|
+
|
|
101
|
+
def decorator(view: Callable):
|
|
102
|
+
@wraps(view)
|
|
103
|
+
def _wrap(*args, **kwargs):
|
|
104
|
+
payment_header = request.headers.get("X-Payment")
|
|
105
|
+
if not payment_header:
|
|
106
|
+
quote = pay.create_quote(
|
|
107
|
+
resource=resource or request.path,
|
|
108
|
+
amount=amount,
|
|
109
|
+
recipient=recipient,
|
|
110
|
+
method=request.method,
|
|
111
|
+
description=description,
|
|
112
|
+
metadata=metadata,
|
|
113
|
+
ttl_seconds=ttl_seconds,
|
|
114
|
+
)
|
|
115
|
+
resp = jsonify(quote["payment_required_response"])
|
|
116
|
+
resp.status_code = 402
|
|
117
|
+
resp.headers["WWW-Authenticate"] = "x402"
|
|
118
|
+
return resp
|
|
119
|
+
v = pay.verify_payment(
|
|
120
|
+
payment_header=payment_header,
|
|
121
|
+
payer_proof=request.headers.get("X-Voidly-Payer-Proof"),
|
|
122
|
+
expected_resource=resource or request.path,
|
|
123
|
+
expected_method=request.method,
|
|
124
|
+
min_amount=amount,
|
|
125
|
+
expected_recipient=recipient or pay.did,
|
|
126
|
+
expected_currency="voidly-credit",
|
|
127
|
+
expected_metadata=metadata or {},
|
|
128
|
+
)
|
|
129
|
+
if not v.get("ok"):
|
|
130
|
+
resp = jsonify({"error": {"code": "x402_verify_failed", "message": v.get("reason")}})
|
|
131
|
+
resp.status_code = 402
|
|
132
|
+
resp.headers["WWW-Authenticate"] = "x402"
|
|
133
|
+
return resp
|
|
134
|
+
request.voidly_payment = v # type: ignore[attr-defined]
|
|
135
|
+
return view(*args, **kwargs)
|
|
136
|
+
return _wrap
|
|
137
|
+
return decorator
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
__all__ = ["fastapi_x402", "flask_x402"]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: voidly-pay
|
|
3
|
-
Version:
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 3.0.0
|
|
4
|
+
Summary: voidpay SDK — zero-value internal test-credit tools for agent-to-agent transfers and x402 integration. Credits are not backed or redeemable.
|
|
5
5
|
Author-email: Voidly <team@voidly.ai>
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: voidly,payments,ai-agents,x402,did,ed25519
|
|
@@ -35,7 +35,7 @@ pay = VoidlyPay() # mints + persists keypair
|
|
|
35
35
|
print("DID:", pay.did) # did:voidly:...
|
|
36
36
|
pay.faucet() # 10 free credits
|
|
37
37
|
|
|
38
|
-
# Browse the marketplace —
|
|
38
|
+
# Browse the marketplace — 12 paid endpoints + N third-party listings
|
|
39
39
|
mp = pay.request("GET", "/v1/pay/marketplace").json()
|
|
40
40
|
for item in mp["items"][:5]:
|
|
41
41
|
print(f"{item['name']:40s} ${item['pricing']['amount_usdc']}")
|
|
@@ -123,11 +123,17 @@ def expensive():
|
|
|
123
123
|
| **Streams (per-token billing)** | `pay.open_stream(...)`, `pay.meter_stream(...)`, `pay.finalize_stream(...)` |
|
|
124
124
|
| **Subscriptions** | `pay.subscribe(...)`, `pay.cancel_subscription(...)` |
|
|
125
125
|
| **x402 server-side quote** | `pay.create_quote(resource, amount)` |
|
|
126
|
-
| **x402 server-side verify** | `pay.verify_payment(
|
|
126
|
+
| **x402 server-side verify** | `pay.verify_payment(payment_header=header, payer_proof=proof, expected_resource=resource, expected_method=method, min_amount=price, expected_metadata=metadata)` |
|
|
127
127
|
| **Webhooks** | `pay.subscribe_webhook(url, events=[...])` |
|
|
128
128
|
| **Trust check** | `pay.health_check()` (6-check report incl. on-chain vault read) |
|
|
129
129
|
|
|
130
|
-
## What's in the marketplace today (Voidly's
|
|
130
|
+
## What's in the marketplace today (Voidly's 12 paid endpoints)
|
|
131
|
+
|
|
132
|
+
Voidly does not sell its censorship data. The dataset is free and CC BY 4.0.
|
|
133
|
+
Every endpoint below charges for compute, or for an Ed25519 signature over data
|
|
134
|
+
that is itself free — never for the observatory. Five SKUs that failed that test
|
|
135
|
+
were retired on 2026-08-04 and now return HTTP 410 naming their free
|
|
136
|
+
replacement; see [voidly.ai/pay/changelog](https://voidly.ai/pay/changelog).
|
|
131
137
|
|
|
132
138
|
| Endpoint | Price | What it does |
|
|
133
139
|
|---|---|---|
|
|
@@ -141,13 +147,19 @@ def expensive():
|
|
|
141
147
|
| `voidly_meta` | $0.001 | URL metadata (og + title + canonical) |
|
|
142
148
|
| `voidly_extract` | $0.01 | PDF/document → plain text |
|
|
143
149
|
| `voidly_scrape` | $0.01 | Fetch any URL + Voidly-signed receipt |
|
|
144
|
-
| `
|
|
145
|
-
| `
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
|
150
|
+
| `probe_attest` | $0.005 | Ed25519 attestation over probe reachability data. The data is free at `/v1/probe/domain/{host}`; the charge is the signature. |
|
|
151
|
+
| `agent_discover_pro` | $0.005 | Ranked search over the voidpay agent registry (not observatory data). Free 20-result search at `/v1/agent/discover`. |
|
|
152
|
+
|
|
153
|
+
Retired 2026-08-04 — these now return **HTTP 410 Gone** with the free
|
|
154
|
+
replacement URL in the body. Do not route to them:
|
|
155
|
+
|
|
156
|
+
| Retired | Free replacement |
|
|
157
|
+
|---|---|
|
|
158
|
+
| `voidly_fetch` / `POST /v1/pay/fetch` | `voidly_scrape` — the country-pinning was never implemented, so every call was already a Cloudflare-edge fetch |
|
|
159
|
+
| `forecast_pro` | `GET /v1/forecast/{cc}/multi-horizon` (free, genuinely modelled) |
|
|
160
|
+
| `claim_verify_pro` | `POST /verify-claim` (free) |
|
|
161
|
+
| `incident_summary_pro` | `GET /data/incidents/{id}/report?format=markdown` (free) |
|
|
162
|
+
| `incidents_export_pro` | `GET /data/incidents/export?format=csv` (free, uncapped) |
|
|
151
163
|
|
|
152
164
|
Plus N third-party listings registered self-serve at [voidly.ai/pay/list-your-service](https://voidly.ai/pay/list-your-service).
|
|
153
165
|
|
|
@@ -178,17 +190,17 @@ ok = verify_webhook_signature(
|
|
|
178
190
|
|
|
179
191
|
## Why agents use this
|
|
180
192
|
|
|
181
|
-
| Problem |
|
|
193
|
+
| Problem | voidpay solves it |
|
|
182
194
|
|---|---|
|
|
183
195
|
| **Need to add payment to your agent service** | x402 middleware ships for FastAPI, Flask, any web-fetch handler |
|
|
184
|
-
| **Need to discover paid services** | One install →
|
|
196
|
+
| **Need to discover paid services** | One install → 12 endpoints + open marketplace listings |
|
|
185
197
|
| **Don't want to manage 10 API keys** | One Ed25519 keypair, one wallet, every paid endpoint works |
|
|
186
198
|
| **Don't trust the agent's payment claims** | Every receipt is Ed25519-signed by Voidly. Verifiable offline. |
|
|
187
|
-
| **Need
|
|
199
|
+
| **Need censorship data** | It is free. `GET /data/incidents/export?format=csv`, CC BY 4.0, no wallet, no key. Voidly does not sell it. |
|
|
188
200
|
|
|
189
201
|
## Honest disclosure
|
|
190
202
|
|
|
191
|
-
|
|
203
|
+
**Stage 2 is being retired** (decided 2026-08-04). Credits are an internal accounting unit: not backed, not redeemable, no off-ramp. The vault on Base mainnet (`0xd25d3c6f32886b65356cc5c700382a8a02d84df5`, [Sourcify-verified](https://repo.sourcify.dev/contracts/full_match/8453/0xd25d3c6f32886b65356cc5c700382a8a02d84df5/)) holds 4.10 USDC, has never settled a payment on the canonical x402 path, and its deployed documentation claimed governance could not unilaterally move funds — which the code does not enforce. It stays on-chain as a public record at [voidly.ai/pay/proof](https://voidly.ai/pay/proof). We have approximately zero sustained external paying users.
|
|
192
204
|
|
|
193
205
|
We opened the marketplace before the demand exists because we believe agent adoption is gated on discoverability, not on payment-rail UX.
|
|
194
206
|
|
|
@@ -207,9 +219,9 @@ We opened the marketplace before the demand exists because we believe agent adop
|
|
|
207
219
|
- [/pay/marketplace](https://voidly.ai/pay/marketplace) — visual browse
|
|
208
220
|
- [/pay/list-your-service](https://voidly.ai/pay/list-your-service) — list in 60s
|
|
209
221
|
- [/pay/claim](https://voidly.ai/pay/claim) — free 10-credit faucet
|
|
210
|
-
- [/pay/proof](https://voidly.ai/pay/proof) —
|
|
222
|
+
- [/pay/proof](https://voidly.ai/pay/proof) — vault record (not a reserve; Stage 2 is being retired)
|
|
211
223
|
- [/pay/for-builders](https://voidly.ai/pay/for-builders) — every middleware + adapter
|
|
212
|
-
- [
|
|
224
|
+
- [voidpay landing](https://voidly.ai/pay)
|
|
213
225
|
|
|
214
226
|
## Keywords
|
|
215
227
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|