voidly-pay 0.1.1__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.
voidly_pay/__init__.py ADDED
@@ -0,0 +1,479 @@
1
+ """Voidly Pay — Python SDK for agent-to-agent payments.
2
+
3
+ Quick start:
4
+
5
+ from voidly_pay import VoidlyPay
6
+ pay = VoidlyPay() # mints + persists keypair on first use
7
+ pay.transfer(to="did:voidly:provider", amount=0.5) # 0.5 credits
8
+
9
+ The SDK signs envelopes with Ed25519 and POSTs them to the configured API
10
+ host (default: https://api.voidly.ai). Keys are persisted to
11
+ ~/.voidly-pay/keypair.json with mode 0600.
12
+
13
+ For server-side x402 paywalls, see ``VoidlyPay.create_quote`` and
14
+ ``VoidlyPay.verify_payment``. For client-side fetch-with-pay, see
15
+ ``VoidlyPay.request_with_pay``.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import base64
21
+ import hashlib
22
+ import hmac
23
+ import json
24
+ import os
25
+ import secrets
26
+ import time
27
+ import uuid
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ from typing import Any, Iterable, Mapping, Optional
31
+
32
+ import nacl.signing
33
+ import requests
34
+
35
+ __version__ = "0.1.0"
36
+
37
+ DEFAULT_API = "https://api.voidly.ai"
38
+ MICRO_PER_CREDIT = 1_000_000
39
+ DEFAULT_EXPIRY_MINUTES = 30
40
+
41
+
42
+ # ─── Errors ────────────────────────────────────────────────────────────────
43
+
44
+
45
+ class VoidlyPayError(Exception):
46
+ def __init__(self, code: str, message: str, status: int, hint: str | None = None, raw: Any = None):
47
+ super().__init__(f"[{code}] {message}")
48
+ self.code = code
49
+ self.status = status
50
+ self.hint = hint
51
+ self.raw = raw
52
+
53
+
54
+ # ─── Canonical JSON ───────────────────────────────────────────────────────
55
+
56
+
57
+ def canonicalize(value: Any) -> str:
58
+ """Same canonical JSON the Worker expects: sorted keys, no whitespace,
59
+ omit None-valued keys, strict number/string serialization."""
60
+ if value is None:
61
+ return "null"
62
+ if isinstance(value, bool):
63
+ return "true" if value else "false"
64
+ if isinstance(value, int):
65
+ return str(value)
66
+ if isinstance(value, float):
67
+ # Server only accepts integers in amount fields. We still preserve
68
+ # repr for any float-valued metadata.
69
+ return repr(value)
70
+ if isinstance(value, str):
71
+ return json.dumps(value, ensure_ascii=False)
72
+ if isinstance(value, (list, tuple)):
73
+ return "[" + ",".join(canonicalize(v) for v in value) + "]"
74
+ if isinstance(value, Mapping):
75
+ keys = sorted(k for k, v in value.items() if v is not None)
76
+ return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + canonicalize(value[k]) for k in keys) + "}"
77
+ raise TypeError(f"unsupported type for canonicalization: {type(value)}")
78
+
79
+
80
+ def canonical_bytes(value: Any) -> bytes:
81
+ return canonicalize(value).encode("utf-8")
82
+
83
+
84
+ # ─── DID derivation ───────────────────────────────────────────────────────
85
+
86
+ _BASE58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
87
+
88
+
89
+ def _b58encode(data: bytes) -> str:
90
+ if not data:
91
+ return ""
92
+ n = int.from_bytes(data, "big")
93
+ out = ""
94
+ while n > 0:
95
+ n, rem = divmod(n, 58)
96
+ out = _BASE58[rem] + out
97
+ for b in data:
98
+ if b == 0:
99
+ out = _BASE58[0] + out
100
+ else:
101
+ break
102
+ return out
103
+
104
+
105
+ def did_from_pubkey(pubkey: bytes) -> str:
106
+ return f"did:voidly:{_b58encode(pubkey[:16])}"
107
+
108
+
109
+ # ─── Storage ──────────────────────────────────────────────────────────────
110
+
111
+
112
+ @dataclass
113
+ class Keypair:
114
+ did: str
115
+ secret_key: bytes
116
+ public_key: bytes
117
+
118
+
119
+ def _default_storage_path() -> Path:
120
+ home = Path.home()
121
+ return home / ".voidly-pay" / "keypair.json"
122
+
123
+
124
+ def _load_or_create_keypair(path: Path | None, did_override: str | None = None) -> Keypair:
125
+ if path is None:
126
+ path = _default_storage_path()
127
+ if path.exists():
128
+ try:
129
+ j = json.loads(path.read_text())
130
+ sk = base64.b64decode(j["secret_key"])
131
+ sk_obj = nacl.signing.SigningKey(sk[:32]) # nacl expects 32-byte seed
132
+ pk = bytes(sk_obj.verify_key)
133
+ did = did_override or j.get("did") or did_from_pubkey(pk)
134
+ return Keypair(did=did, secret_key=bytes(sk_obj._signing_key), public_key=pk)
135
+ except Exception:
136
+ # corrupt file — back it up and mint fresh
137
+ path.rename(path.with_suffix(".corrupt"))
138
+ sk_obj = nacl.signing.SigningKey.generate()
139
+ pk = bytes(sk_obj.verify_key)
140
+ did = did_override or did_from_pubkey(pk)
141
+ path.parent.mkdir(parents=True, exist_ok=True)
142
+ # nacl.signing internal "_signing_key" is the 64-byte tweetnacl-compatible key
143
+ sk_full = bytes(sk_obj._signing_key)
144
+ path.write_text(json.dumps({"did": did, "secret_key": base64.b64encode(sk_full).decode()}))
145
+ os.chmod(path, 0o600)
146
+ return Keypair(did=did, secret_key=sk_full, public_key=pk)
147
+
148
+
149
+ # ─── Main client ──────────────────────────────────────────────────────────
150
+
151
+
152
+ class VoidlyPay:
153
+ def __init__(
154
+ self,
155
+ api_url: str | None = None,
156
+ did: str | None = None,
157
+ secret_key: bytes | None = None,
158
+ storage_path: Path | str | None = None,
159
+ default_expiry_minutes: int = DEFAULT_EXPIRY_MINUTES,
160
+ session: requests.Session | None = None,
161
+ ):
162
+ self.api_url = (api_url or DEFAULT_API).rstrip("/")
163
+ self.default_expiry_ms = default_expiry_minutes * 60_000
164
+ self.session = session or requests.Session()
165
+ if secret_key is not None:
166
+ sk_obj = nacl.signing.SigningKey(secret_key[:32])
167
+ pk = bytes(sk_obj.verify_key)
168
+ self._kp = Keypair(did=did or did_from_pubkey(pk), secret_key=bytes(sk_obj._signing_key), public_key=pk)
169
+ else:
170
+ sp = Path(storage_path) if storage_path else None
171
+ self._kp = _load_or_create_keypair(sp, did)
172
+ self.did = self._kp.did
173
+
174
+ # ─── Crypto ────────────────────────────────────────────────────────────
175
+
176
+ @property
177
+ def public_key(self) -> str:
178
+ """Public key as base64 (32 bytes). Used to register with the relay."""
179
+ return base64.b64encode(self._kp.public_key).decode()
180
+
181
+ def _sign(self, env: dict) -> str:
182
+ msg = canonical_bytes(env)
183
+ sig = nacl.signing.SigningKey(self._kp.secret_key[:32]).sign(msg).signature
184
+ return base64.b64encode(sig).decode()
185
+
186
+ def _now_iso(self) -> str:
187
+ from datetime import datetime, timezone
188
+ return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
189
+
190
+ def _expires(self, ms: int | None = None) -> str:
191
+ from datetime import datetime, timezone, timedelta
192
+ return (datetime.now(timezone.utc) + timedelta(milliseconds=ms or self.default_expiry_ms)).isoformat(timespec="milliseconds").replace("+00:00", "Z")
193
+
194
+ # ─── HTTP ──────────────────────────────────────────────────────────────
195
+
196
+ def _req(self, method: str, path: str, body: Any = None, idempotency: str | None = None) -> dict:
197
+ headers = {"accept": "application/json"}
198
+ if body is not None:
199
+ headers["content-type"] = "application/json"
200
+ if idempotency:
201
+ headers["idempotency-key"] = idempotency
202
+ url = f"{self.api_url}{path}"
203
+ r = self.session.request(method, url, json=body, headers=headers, timeout=30)
204
+ try:
205
+ data = r.json() if r.text else {}
206
+ except json.JSONDecodeError:
207
+ data = {"raw": r.text}
208
+ if not r.ok:
209
+ err = data.get("error", {}) if isinstance(data, dict) else {}
210
+ raise VoidlyPayError(
211
+ code=err.get("code") or data.get("reason") or f"http_{r.status_code}",
212
+ message=err.get("message") or data.get("reason") or f"request failed: {r.status_code}",
213
+ status=r.status_code,
214
+ hint=err.get("hint"),
215
+ raw=data,
216
+ )
217
+ return data
218
+
219
+ # ─── Wallet ────────────────────────────────────────────────────────────
220
+
221
+ def ensure_wallet(self, did: str | None = None) -> dict:
222
+ return self._req("POST", "/v1/pay/wallet", {"did": did or self.did}).get("wallet", {})
223
+
224
+ def balance(self, did: str | None = None) -> dict:
225
+ target = did or self.did
226
+ r = self._req("GET", f"/v1/pay/wallet/{target}")
227
+ w = r.get("wallet", {})
228
+ return {
229
+ "balance_micro": w.get("balance_credits", 0),
230
+ "balance_credits": w.get("balance_credits", 0) / MICRO_PER_CREDIT,
231
+ "locked_micro": w.get("locked_credits", 0),
232
+ "daily_cap_micro": w.get("daily_cap_credits", 0),
233
+ "per_tx_cap_micro": w.get("per_tx_cap_credits", 0),
234
+ "frozen": bool(w.get("frozen")),
235
+ }
236
+
237
+ # ─── Transfer ──────────────────────────────────────────────────────────
238
+
239
+ def transfer(self, to: str, amount: float, memo: str | None = None, expires_in_minutes: int | None = None) -> dict:
240
+ try:
241
+ self.ensure_wallet()
242
+ except VoidlyPayError:
243
+ pass
244
+ env = {
245
+ "schema": "voidly-credit-transfer/v1",
246
+ "from_did": self.did,
247
+ "to_did": to,
248
+ "amount_micro": round(amount * MICRO_PER_CREDIT),
249
+ "memo": memo,
250
+ "nonce": uuid.uuid4().hex,
251
+ "issued_at": self._now_iso(),
252
+ "expires_at": self._expires((expires_in_minutes * 60_000) if expires_in_minutes else None),
253
+ }
254
+ body = {"envelope": env, "signature": self._sign(env)}
255
+ r = self._req("POST", "/v1/pay/transfer", body, idempotency=env["nonce"])
256
+ if r.get("status") != "settled":
257
+ raise VoidlyPayError(code=r.get("reason", "transfer_failed"), message=f"transfer failed: {r.get('reason')}", status=400, raw=r)
258
+ return r
259
+
260
+ def batch_transfer(self, items: Iterable[dict]) -> dict:
261
+ items_list = list(items)
262
+ if not items_list:
263
+ raise ValueError("batch_transfer: items empty")
264
+ env = {
265
+ "schema": "voidly-batch-transfer/v1",
266
+ "from_did": self.did,
267
+ "batch_id": uuid.uuid4().hex,
268
+ "items": [{"to_did": i["to"], "amount_micro": round(i["amount"] * MICRO_PER_CREDIT), "memo": i.get("memo")} for i in items_list],
269
+ "issued_at": self._now_iso(),
270
+ "expires_at": self._expires(),
271
+ }
272
+ body = {"envelope": env, "signature": self._sign(env)}
273
+ r = self._req("POST", "/v1/pay/batch", body, idempotency=env["batch_id"])
274
+ if r.get("status") != "settled":
275
+ raise VoidlyPayError(code=r.get("reason", "batch_failed"), message=f"batch failed: {r.get('reason')}", status=400, raw=r)
276
+ return r
277
+
278
+ def get_transfer(self, transfer_id: str) -> dict:
279
+ return self._req("GET", f"/v1/pay/transfer/{transfer_id}").get("transfer", {})
280
+
281
+ def history(self, did: str | None = None, limit: int | None = None, before: str | None = None) -> dict:
282
+ params = []
283
+ if limit:
284
+ params.append(f"limit={limit}")
285
+ if before:
286
+ params.append(f"before={requests.utils.quote(before)}")
287
+ qs = ("?" + "&".join(params)) if params else ""
288
+ return self._req("GET", f"/v1/pay/history/{did or self.did}{qs}")
289
+
290
+ # ─── Escrow ────────────────────────────────────────────────────────────
291
+
292
+ def open_escrow(self, to: str, amount: float, deadline_hours: int = 24, memo: str | None = None) -> dict:
293
+ from datetime import datetime, timezone, timedelta
294
+ dl = (datetime.now(timezone.utc) + timedelta(hours=deadline_hours)).isoformat(timespec="milliseconds").replace("+00:00", "Z")
295
+ env = {
296
+ "schema": "voidly-escrow-open/v1",
297
+ "from_did": self.did,
298
+ "to_did": to,
299
+ "amount_micro": round(amount * MICRO_PER_CREDIT),
300
+ "memo": memo,
301
+ "deadline_at": dl,
302
+ "nonce": uuid.uuid4().hex,
303
+ "issued_at": self._now_iso(),
304
+ "expires_at": self._expires(),
305
+ }
306
+ return self._req("POST", "/v1/pay/escrow/open", {"envelope": env, "signature": self._sign(env)}, idempotency=env["nonce"])
307
+
308
+ def release_escrow(self, escrow_id: str) -> dict:
309
+ env = {
310
+ "schema": "voidly-escrow-release/v1",
311
+ "escrow_id": escrow_id,
312
+ "actor_did": self.did,
313
+ "nonce": uuid.uuid4().hex,
314
+ "issued_at": self._now_iso(),
315
+ "expires_at": self._expires(),
316
+ }
317
+ return self._req("POST", "/v1/pay/escrow/release", {"envelope": env, "signature": self._sign(env)})
318
+
319
+ def refund_escrow(self, escrow_id: str, reason: str | None = None) -> dict:
320
+ env = {
321
+ "schema": "voidly-escrow-refund/v1",
322
+ "escrow_id": escrow_id,
323
+ "actor_did": self.did,
324
+ "reason": reason,
325
+ "nonce": uuid.uuid4().hex,
326
+ "issued_at": self._now_iso(),
327
+ "expires_at": self._expires(),
328
+ }
329
+ return self._req("POST", "/v1/pay/escrow/refund", {"envelope": env, "signature": self._sign(env)})
330
+
331
+ # ─── x402 (server-side) ────────────────────────────────────────────────
332
+
333
+ def create_quote(
334
+ self,
335
+ resource: str,
336
+ amount: float,
337
+ recipient: str | None = None,
338
+ method: str | None = None,
339
+ description: str | None = None,
340
+ metadata: dict | None = None,
341
+ ttl_seconds: int | None = None,
342
+ ) -> dict:
343
+ env = {
344
+ "schema": "voidly-x402-quote/v1",
345
+ "server_did": self.did,
346
+ "resource": resource,
347
+ "amount_micro": round(amount * MICRO_PER_CREDIT),
348
+ "recipient_did": recipient or self.did,
349
+ "method": method,
350
+ "description": description,
351
+ "metadata": metadata,
352
+ "ttl_seconds": ttl_seconds,
353
+ "nonce": uuid.uuid4().hex,
354
+ "issued_at": self._now_iso(),
355
+ "expires_at": self._expires(),
356
+ }
357
+ return self._req("POST", "/v1/pay/x402/quote", {"envelope": env, "signature": self._sign(env)})
358
+
359
+ def verify_payment(self, quote_id: str, transfer_id: str | None = None, payment_header: str | None = None) -> dict:
360
+ env = {
361
+ "schema": "voidly-x402-verify/v1",
362
+ "server_did": self.did,
363
+ "quote_id": quote_id,
364
+ "transfer_id": transfer_id,
365
+ "payment_header": payment_header,
366
+ "nonce": uuid.uuid4().hex,
367
+ "issued_at": self._now_iso(),
368
+ "expires_at": self._expires(),
369
+ }
370
+ return self._req("POST", "/v1/pay/x402/verify", {"envelope": env, "signature": self._sign(env)})
371
+
372
+ # ─── x402 (client-side: pay-on-402) ────────────────────────────────────
373
+
374
+ def request_with_pay(
375
+ self,
376
+ method: str,
377
+ url: str,
378
+ max_amount: float | None = None,
379
+ **kwargs,
380
+ ) -> requests.Response:
381
+ """Fetch a URL; if it returns 402, parse the quote, transfer, retry."""
382
+ r = self.session.request(method, url, **kwargs)
383
+ if r.status_code != 402:
384
+ return r
385
+ try:
386
+ body = r.json()
387
+ except json.JSONDecodeError:
388
+ raise VoidlyPayError(code="x402_body_unparseable", message="Server returned 402 but no JSON body", status=402)
389
+ accept = (body.get("x402") or {}).get("accepts", [None])[0]
390
+ if not accept:
391
+ raise VoidlyPayError(code="x402_no_accept", message="Server 402 missing x402.accepts", status=402, raw=body)
392
+ if accept.get("scheme") != "voidly-credit":
393
+ raise VoidlyPayError(code="x402_unsupported_scheme", message=f"Unsupported scheme: {accept.get('scheme')}", status=402)
394
+ credits = accept["amount_micro"] / MICRO_PER_CREDIT
395
+ if max_amount is not None and credits > max_amount:
396
+ raise VoidlyPayError(code="x402_amount_exceeds_max", message=f"Quote price {credits} exceeds maxAmount {max_amount}", status=402)
397
+ t = self.transfer(to=accept["recipient_did"], amount=credits, memo=f"x402: {accept['resource']}")
398
+ headers = dict(kwargs.pop("headers", {}) or {})
399
+ headers["X-Payment"] = f"voidly-credit transfer_id={t['transfer_id']}; quote_id={accept['quote_id']}"
400
+ return self.session.request(method, url, headers=headers, **kwargs)
401
+
402
+ # ─── Webhooks ──────────────────────────────────────────────────────────
403
+
404
+ def subscribe_webhook(
405
+ self,
406
+ url: str,
407
+ events: list[str] | None = None,
408
+ did_filter: str | None = None,
409
+ description: str | None = None,
410
+ ) -> dict:
411
+ env = {
412
+ "schema": "voidly-webhook-subscribe/v1",
413
+ "did": self.did,
414
+ "url": url,
415
+ "event_filter": events,
416
+ "did_filter": did_filter,
417
+ "description": description,
418
+ "nonce": uuid.uuid4().hex,
419
+ "issued_at": self._now_iso(),
420
+ "expires_at": self._expires(),
421
+ }
422
+ return self._req("POST", "/v1/pay/webhooks", {"envelope": env, "signature": self._sign(env)}, idempotency=env["nonce"])
423
+
424
+ # ─── Network reads ─────────────────────────────────────────────────────
425
+
426
+ def health(self) -> dict: return self._req("GET", "/v1/pay/health")
427
+ def manifest(self) -> dict: return self._req("GET", "/v1/pay/manifest.json")
428
+ def stats(self) -> dict: return self._req("GET", "/v1/pay/stats")
429
+ def activity(self, limit: int = 50) -> dict: return self._req("GET", f"/v1/pay/activity?limit={limit}")
430
+ def leaderboard(self, metric: str = "earned_24h", limit: int = 25) -> dict:
431
+ return self._req("GET", f"/v1/pay/leaderboard?metric={metric}&limit={limit}")
432
+ def feed(self, since: str | None = None, limit: int = 50) -> dict:
433
+ qs = f"?limit={limit}" + (f"&since={requests.utils.quote(since)}" if since else "")
434
+ return self._req("GET", f"/v1/pay/feed{qs}")
435
+ def trust(self, did: str | None = None) -> dict:
436
+ return self._req("GET", f"/v1/pay/trust/{did or self.did}")
437
+
438
+
439
+ # ─── Webhook signature verification ──────────────────────────────────────
440
+
441
+
442
+ def verify_webhook_signature(
443
+ body: str | bytes,
444
+ signature_header: str,
445
+ secret: str,
446
+ tolerance_seconds: int = 300,
447
+ ) -> bool:
448
+ """Verify the X-Voidly-Signature header on a webhook delivery.
449
+
450
+ Returns True iff the HMAC matches AND the timestamp is within tolerance.
451
+ Constant-time compare via hmac.compare_digest.
452
+ """
453
+ if not signature_header.startswith("t="):
454
+ return False
455
+ parts = dict(p.split("=", 1) for p in signature_header.split(","))
456
+ try:
457
+ ts = int(parts["t"])
458
+ except (KeyError, ValueError):
459
+ return False
460
+ sig = parts.get("v1", "")
461
+ if abs(int(time.time()) - ts) > tolerance_seconds:
462
+ return False
463
+ body_bytes = body.encode("utf-8") if isinstance(body, str) else body
464
+ key_bytes = bytes.fromhex(secret)
465
+ msg = f"t={ts}.".encode() + body_bytes
466
+ expected = hmac.new(key_bytes, msg, hashlib.sha256).hexdigest()
467
+ return hmac.compare_digest(expected, sig)
468
+
469
+
470
+ __all__ = [
471
+ "VoidlyPay",
472
+ "VoidlyPayError",
473
+ "verify_webhook_signature",
474
+ "did_from_pubkey",
475
+ "canonicalize",
476
+ "canonical_bytes",
477
+ "MICRO_PER_CREDIT",
478
+ "__version__",
479
+ ]
@@ -0,0 +1,220 @@
1
+ Metadata-Version: 2.4
2
+ Name: voidly-pay
3
+ Version: 0.1.1
4
+ Summary: Voidly Pay SDK — agent-to-agent payments for AI agents
5
+ Author-email: Voidly <team@voidly.ai>
6
+ License: MIT
7
+ Keywords: voidly,payments,ai-agents,x402,did,ed25519
8
+ Requires-Python: >=3.10
9
+ Description-Content-Type: text/markdown
10
+ Requires-Dist: pynacl>=1.5.0
11
+ Requires-Dist: requests>=2.31.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: pytest>=7.0; extra == "dev"
14
+ Requires-Dist: pytest-mock>=3.10; extra == "dev"
15
+ Requires-Dist: responses>=0.23; extra == "dev"
16
+
17
+ # voidly-pay (Python)
18
+
19
+ > **The marketplace AI agents browse for paid HTTP services.** Pay any of 17+ paid endpoints for &lt;$0.01 using one Ed25519 keypair. List your own paid endpoint in 60 seconds. Settles in &lt;200ms via x402 + USDC on Base mainnet.
20
+
21
+ [![PyPI version](https://img.shields.io/pypi/v/voidly-pay)](https://pypi.org/project/voidly-pay/)
22
+ [![x402](https://img.shields.io/badge/x402-canonical%20v2-blue)](https://www.x402.org)
23
+ [![vault](https://img.shields.io/badge/vault-Sourcify%20verified-emerald)](https://repo.sourcify.dev/contracts/full_match/8453/0xb592512932a7b354969bb48039c2dc7ad6ad1c12/)
24
+
25
+ ```bash
26
+ pip install voidly-pay
27
+ ```
28
+
29
+ ## 30-second tour
30
+
31
+ ```python
32
+ from voidly_pay import VoidlyPay
33
+
34
+ pay = VoidlyPay() # mints + persists keypair
35
+ print("DID:", pay.did) # did:voidly:...
36
+ pay.faucet() # 10 free credits
37
+
38
+ # Browse the marketplace — 17 paid endpoints + N third-party listings
39
+ mp = pay.request("GET", "/v1/pay/marketplace").json()
40
+ for item in mp["items"][:5]:
41
+ print(f"{item['name']:40s} ${item['pricing']['amount_usdc']}")
42
+
43
+ # Pay any paid endpoint via auto-x402
44
+ r = pay.request_with_pay(
45
+ "GET",
46
+ "https://api.voidly.ai/v1/pay/wiki?title=Alan%20Turing",
47
+ max_amount=0.005,
48
+ )
49
+ receipt = r.json()
50
+ print(receipt["extract"][:200])
51
+ ```
52
+
53
+ ## Pay anything that returns 402
54
+
55
+ ```python
56
+ # Universal x402 client — handles 402 → quote → settle → retry
57
+ r = pay.request_with_pay(
58
+ "POST",
59
+ "https://api.voidly.ai/v1/pay/extract",
60
+ json={"url": "https://arxiv.org/pdf/2507.14183.pdf"},
61
+ max_amount=0.01,
62
+ )
63
+ print(r.json()["text_length"])
64
+ ```
65
+
66
+ ## List your own paid endpoint
67
+
68
+ ```python
69
+ pay.create_listing(
70
+ name="My Paid API",
71
+ tagline="Pay 1¢ for X, get Y signed.",
72
+ url="https://my-api.example.com/expensive",
73
+ amount_usdc=0.01,
74
+ category="data",
75
+ tags=["json", "agents"],
76
+ )
77
+ # Now appears at /v1/pay/marketplace, every Voidly-aware agent sees it.
78
+ ```
79
+
80
+ Or browser-only (no install): [voidly.ai/pay/list-your-service](https://voidly.ai/pay/list-your-service).
81
+
82
+ ## Run a paid endpoint (FastAPI)
83
+
84
+ ```python
85
+ from fastapi import FastAPI, Depends
86
+ from voidly_pay import VoidlyPay
87
+ from voidly_pay.middleware import fastapi_x402
88
+
89
+ app = FastAPI()
90
+ pay = VoidlyPay()
91
+
92
+ # Charge $0.01 per request — settles atomically with the response.
93
+ @app.get("/expensive", dependencies=[Depends(fastapi_x402(pay, amount=0.01))])
94
+ def expensive():
95
+ return {"data": "the goods"}
96
+ ```
97
+
98
+ Flask:
99
+
100
+ ```python
101
+ from flask import Flask
102
+ from voidly_pay import VoidlyPay
103
+ from voidly_pay.middleware import flask_x402
104
+
105
+ app = Flask(__name__)
106
+ pay = VoidlyPay()
107
+
108
+ @app.route("/expensive")
109
+ @flask_x402(pay, amount=0.01)
110
+ def expensive():
111
+ return {"data": "the goods"}
112
+ ```
113
+
114
+ ## What you can do
115
+
116
+ | Primitive | Method |
117
+ |---|---|
118
+ | **Marketplace** | `pay.create_listing(...)`, `pay.list_listings()`, `pay.get_listing(id)` |
119
+ | **Pay any URL** | `pay.request_with_pay(method, url, max_amount=...)` |
120
+ | **Direct transfer** | `pay.transfer(to, amount)` |
121
+ | **Batch transfer** | `pay.batch_transfer([{...}, ...])` |
122
+ | **Escrow** | `pay.open_escrow(to, amount, deadline_hours)` |
123
+ | **Streams (per-token billing)** | `pay.open_stream(...)`, `pay.meter_stream(...)`, `pay.finalize_stream(...)` |
124
+ | **Subscriptions** | `pay.subscribe(...)`, `pay.cancel_subscription(...)` |
125
+ | **x402 server-side quote** | `pay.create_quote(resource, amount)` |
126
+ | **x402 server-side verify** | `pay.verify_payment(quote_id, transfer_id)` |
127
+ | **Webhooks** | `pay.subscribe_webhook(url, events=[...])` |
128
+ | **Trust check** | `pay.health_check()` (6-check report incl. on-chain vault read) |
129
+
130
+ ## What's in the marketplace today (Voidly's 17 paid endpoints)
131
+
132
+ | Endpoint | Price | What it does |
133
+ |---|---|---|
134
+ | `voidly_hash` | $0.001 | SHA-256/512 + signed receipt |
135
+ | `voidly_timestamp` | $0.001 | Proof-of-existence (OpenTimestamps-style) |
136
+ | `voidly_random` | $0.001 | Signed CSPRNG bytes |
137
+ | `voidly_qr` | $0.001 | QR-code PNG of any text/URL |
138
+ | `voidly_wiki` | $0.001 | Wikipedia summary + signed citation |
139
+ | `voidly_exchange` | $0.001 | Fiat/crypto exchange rates |
140
+ | `voidly_markdown` | $0.001 | HTML → clean markdown (10x reduction) |
141
+ | `voidly_meta` | $0.001 | URL metadata (og + title + canonical) |
142
+ | `voidly_extract` | $0.01 | PDF/document → plain text |
143
+ | `voidly_scrape` | $0.01 | Fetch any URL + Voidly-signed receipt |
144
+ | `voidly_fetch` | $0.05 | Country-pinned fetch via 37+ probe network |
145
+ | `probe_attest` | $0.005 | Multi-vantage signed reachability proof |
146
+ | `forecast_pro` | $0.01 | 30-day country-shutdown risk forecast |
147
+ | `claim_verify_pro` | $0.005 | Evidence-backed verification of claims |
148
+ | `incident_summary_pro` | $0.005 | Plain-English summary of an incident |
149
+ | `agent_discover_pro` | $0.005 | Premium ranked agent search |
150
+ | `incidents_export_pro` | $0.05 | Bulk export (high limit, no rate cap) |
151
+
152
+ Plus N third-party listings registered self-serve at [voidly.ai/pay/list-your-service](https://voidly.ai/pay/list-your-service).
153
+
154
+ Live machine-readable catalog: [api.voidly.ai/v1/pay/marketplace](https://api.voidly.ai/v1/pay/marketplace).
155
+
156
+ ## Configuration
157
+
158
+ ```python
159
+ pay = VoidlyPay(
160
+ api_url="https://api.voidly.ai",
161
+ secret_key=existing_key, # bring your own
162
+ storage_path="~/.my-keys/voidly.json",
163
+ default_expiry_minutes=30,
164
+ )
165
+ ```
166
+
167
+ ## Webhook verification
168
+
169
+ ```python
170
+ from voidly_pay import verify_webhook_signature
171
+
172
+ ok = verify_webhook_signature(
173
+ body=raw_body,
174
+ signature_header=headers["X-Voidly-Signature"],
175
+ secret=os.environ["VOIDLY_WEBHOOK_SECRET"],
176
+ )
177
+ ```
178
+
179
+ ## Why agents use this
180
+
181
+ | Problem | Voidly Pay solves it |
182
+ |---|---|
183
+ | **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 → 17 endpoints + open marketplace listings |
185
+ | **Don't want to manage 10 API keys** | One Ed25519 keypair, one wallet, every paid endpoint works |
186
+ | **Don't trust the agent's payment claims** | Every receipt is Ed25519-signed by Voidly. Verifiable offline. |
187
+ | **Need country-attested fetch** | 37+ probe network, signed (URL, country, ASN, probe-DID) |
188
+
189
+ ## Honest disclosure
190
+
191
+ The Voidly Pay vault on Base mainnet (`0xb592512932a7b354969bb48039c2dc7ad6ad1c12`, [Sourcify-verified](https://repo.sourcify.dev/contracts/full_match/8453/0xb592512932a7b354969bb48039c2dc7ad6ad1c12/)) currently holds **$4 USDC**. We have approximately zero sustained external paying users yet. Live reserves at [voidly.ai/pay/proof](https://voidly.ai/pay/proof).
192
+
193
+ We opened the marketplace before the demand exists because we believe agent adoption is gated on discoverability, not on payment-rail UX.
194
+
195
+ ## Framework adapters (use these for higher-level integration)
196
+
197
+ - **LangChain**: `pip install voidly-pay-langchain`
198
+ - **CrewAI**: `pip install voidly-pay-crewai`
199
+ - **Pydantic AI**: `pip install voidly-pay-pydantic-ai`
200
+ - **AutoGen**: `pip install voidly-pay-autogen`
201
+ - **LlamaIndex**: `pip install voidly-pay-llamaindex`
202
+
203
+ ## Links
204
+
205
+ - [Marketplace JSON](https://api.voidly.ai/v1/pay/marketplace)
206
+ - [/pay/install](https://voidly.ai/pay/install) — one-click MCP install (any client)
207
+ - [/pay/marketplace](https://voidly.ai/pay/marketplace) — visual browse
208
+ - [/pay/list-your-service](https://voidly.ai/pay/list-your-service) — list in 60s
209
+ - [/pay/claim](https://voidly.ai/pay/claim) — free 10-credit faucet
210
+ - [/pay/proof](https://voidly.ai/pay/proof) — live reserves dashboard
211
+ - [/pay/for-builders](https://voidly.ai/pay/for-builders) — every middleware + adapter
212
+ - [Voidly Pay landing](https://voidly.ai/pay)
213
+
214
+ ## Keywords
215
+
216
+ x402 · agent payments · python sdk · usdc · base mainnet · signed receipts · agent marketplace · pay per call · micropayments · fastapi x402 · flask x402 · langchain agent payments · crewai payments · llamaindex tools · pydantic-ai tools · autogen extensions
217
+
218
+ ## License
219
+
220
+ MIT
@@ -0,0 +1,5 @@
1
+ voidly_pay/__init__.py,sha256=lFZnWORvIOJGwTusTUDWrPTjHhWySKygETDMr_r66Ak,20654
2
+ voidly_pay-0.1.1.dist-info/METADATA,sha256=ZKJfhV504fLEI46LdkXyLAicnrblBWZ1fhPqRLOzQYs,8462
3
+ voidly_pay-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
4
+ voidly_pay-0.1.1.dist-info/top_level.txt,sha256=hlJmPLM5vAsdroCP3-ZkciqAng-l9IrpnRP60-L_VEM,11
5
+ voidly_pay-0.1.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ voidly_pay