keptvow 0.1.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.
keptvow-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: keptvow
3
+ Version: 0.1.0
4
+ Summary: Check who your AI agent is about to pay before an x402 payment goes out: ok, careful or stop.
5
+ Author-email: Keptvow <support@keptvow.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://keptvow.com
8
+ Project-URL: Documentation, https://keptvow.com/docs
9
+ Keywords: x402,ai-agents,payments,trust,erc-8004,base,usdc
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # keptvow
16
+
17
+ Check who your AI agent is about to pay **before** an x402 payment goes out.
18
+
19
+ Every answer is one word — **ok**, **careful** or **stop** — with the reason, built from real
20
+ USDC payments on Base (how many different buyers paid the seller, how many came back), reports
21
+ from buyers who did or didn't get what they paid for, and daily checks that the seller's
22
+ services answer. Free, no sign-up, no key, no dependencies.
23
+
24
+ ```sh
25
+ pip install keptvow
26
+ ```
27
+
28
+ ## Ask about one wallet
29
+
30
+ ```python
31
+ import keptvow
32
+
33
+ result = keptvow.check("0xSELLER", amount_usd=5)
34
+ result["verdict"] # "ok", "careful" or "stop"
35
+ result["advice"] # one plain sentence saying why
36
+ ```
37
+
38
+ ## Check a seller's 402 answer before paying
39
+
40
+ Works with `requests` and `httpx` responses:
41
+
42
+ ```python
43
+ r = session.get(url)
44
+ if r.status_code == 402:
45
+ keptvow.guard(r) # raises keptvow.KeptvowStop for a wallet with a bad record
46
+ # ...go on and pay with your x402 client
47
+ ```
48
+
49
+ `guard(r, allow_careful=False)` also blocks wallets with no track record;
50
+ `fail_open=False` blocks payment when Keptvow can't be reached.
51
+
52
+ ## Report whether you got what you paid for
53
+
54
+ ```python
55
+ keptvow.report_outcome(paid_response) # reads the PAYMENT-RESPONSE receipt
56
+ keptvow.report_outcome("0xTX", delivered=False) # or name the payment yourself
57
+ ```
58
+
59
+ Sent in the background and never raises. Keptvow checks the payment on-chain, so only real
60
+ buyers count — this is what lets honest sellers earn **ok** and flags the ones that take the
61
+ money and deliver nothing.
62
+
63
+ Set `KEPTVOW_URL` to use another Keptvow address.
64
+
65
+ ## Other ways in
66
+
67
+ No package needed: `GET https://keptvow.com/v1/check?pay_to=0x…&amount_usd=5`, an MCP server at
68
+ `https://keptvow.com/mcp`, and a guide for AI agents at <https://keptvow.com/llms.txt>. Everything
69
+ else: <https://keptvow.com/docs>.
@@ -0,0 +1,55 @@
1
+ # keptvow
2
+
3
+ Check who your AI agent is about to pay **before** an x402 payment goes out.
4
+
5
+ Every answer is one word — **ok**, **careful** or **stop** — with the reason, built from real
6
+ USDC payments on Base (how many different buyers paid the seller, how many came back), reports
7
+ from buyers who did or didn't get what they paid for, and daily checks that the seller's
8
+ services answer. Free, no sign-up, no key, no dependencies.
9
+
10
+ ```sh
11
+ pip install keptvow
12
+ ```
13
+
14
+ ## Ask about one wallet
15
+
16
+ ```python
17
+ import keptvow
18
+
19
+ result = keptvow.check("0xSELLER", amount_usd=5)
20
+ result["verdict"] # "ok", "careful" or "stop"
21
+ result["advice"] # one plain sentence saying why
22
+ ```
23
+
24
+ ## Check a seller's 402 answer before paying
25
+
26
+ Works with `requests` and `httpx` responses:
27
+
28
+ ```python
29
+ r = session.get(url)
30
+ if r.status_code == 402:
31
+ keptvow.guard(r) # raises keptvow.KeptvowStop for a wallet with a bad record
32
+ # ...go on and pay with your x402 client
33
+ ```
34
+
35
+ `guard(r, allow_careful=False)` also blocks wallets with no track record;
36
+ `fail_open=False` blocks payment when Keptvow can't be reached.
37
+
38
+ ## Report whether you got what you paid for
39
+
40
+ ```python
41
+ keptvow.report_outcome(paid_response) # reads the PAYMENT-RESPONSE receipt
42
+ keptvow.report_outcome("0xTX", delivered=False) # or name the payment yourself
43
+ ```
44
+
45
+ Sent in the background and never raises. Keptvow checks the payment on-chain, so only real
46
+ buyers count — this is what lets honest sellers earn **ok** and flags the ones that take the
47
+ money and deliver nothing.
48
+
49
+ Set `KEPTVOW_URL` to use another Keptvow address.
50
+
51
+ ## Other ways in
52
+
53
+ No package needed: `GET https://keptvow.com/v1/check?pay_to=0x…&amount_usd=5`, an MCP server at
54
+ `https://keptvow.com/mcp`, and a guide for AI agents at <https://keptvow.com/llms.txt>. Everything
55
+ else: <https://keptvow.com/docs>.
@@ -0,0 +1,22 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "keptvow"
7
+ version = "0.1.0"
8
+ description = "Check who your AI agent is about to pay before an x402 payment goes out: ok, careful or stop."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ authors = [{ name = "Keptvow", email = "support@keptvow.com" }]
13
+ dependencies = []
14
+ keywords = ["x402", "ai-agents", "payments", "trust", "erc-8004", "base", "usdc"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ ]
19
+
20
+ [project.urls]
21
+ Homepage = "https://keptvow.com"
22
+ Documentation = "https://keptvow.com/docs"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,248 @@
1
+ """Check who your AI agent is about to pay before an x402 payment goes out.
2
+
3
+ import keptvow
4
+
5
+ result = keptvow.check("0xSELLER", amount_usd=5)
6
+ result["verdict"] # "ok", "careful" or "stop"
7
+ result["advice"] # one plain sentence saying why
8
+
9
+ With requests or httpx, check a seller's 402 answer before paying it, and report afterwards
10
+ whether the result arrived:
11
+
12
+ r = session.get(url)
13
+ if r.status_code == 402:
14
+ keptvow.guard(r) # raises keptvow.KeptvowStop for a wallet with a bad record
15
+ ...
16
+ keptvow.report_outcome(paid_response) # in the background; never raises
17
+
18
+ Every answer is signed by Keptvow (result["signed"], checkable at
19
+ https://keptvow.com/.well-known/keptvow-signer.json) and says how long it stays fresh, so checking
20
+ the same seller again within five minutes reuses the answer. With a key, the same seller is billed
21
+ at most once a day, and delivery reports sent with that key make its checks free.
22
+
23
+ Free, no sign-up, no key. No dependencies. Set KEPTVOW_URL to use another Keptvow address.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import base64
29
+ import json
30
+ import os
31
+ import threading
32
+ import time
33
+ import urllib.error
34
+ import urllib.parse
35
+ import urllib.request
36
+ from typing import Any, Callable, Iterable, Optional
37
+
38
+ __all__ = ["check", "guard", "report_outcome", "payment_options", "KeptvowStop", "DEFAULT_URL"]
39
+ __version__ = "0.1.0"
40
+
41
+ DEFAULT_URL = "https://keptvow.com"
42
+
43
+ # USDC (6 decimals) on Base, Base Sepolia and Ethereum, so prices can be judged in dollars.
44
+ _USDC = {
45
+ "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
46
+ "0x036cbd53842c5426634e7929541ec2318f3dcf7e",
47
+ "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
48
+ }
49
+
50
+
51
+ class KeptvowStop(Exception):
52
+ """Raised instead of paying a wallet Keptvow says to stop at. `.check` holds the full answer."""
53
+
54
+ def __init__(self, check: dict):
55
+ super().__init__(f"Keptvow: not paying {check.get('pay_to')}: {check.get('advice')}")
56
+ self.check = check
57
+
58
+
59
+ def _base(base_url: Optional[str]) -> str:
60
+ return (base_url or os.environ.get("KEPTVOW_URL") or DEFAULT_URL).rstrip("/")
61
+
62
+
63
+ def _request(method: str, url: str, body: Optional[dict], headers: dict, timeout: float) -> tuple[int, dict]:
64
+ data = json.dumps(body).encode() if body is not None else None
65
+ if data is not None:
66
+ headers = {**headers, "Content-Type": "application/json"}
67
+ req = urllib.request.Request(url, data=data, method=method, headers={"Accept": "application/json", **headers})
68
+ try:
69
+ with urllib.request.urlopen(req, timeout=timeout) as r:
70
+ return r.status, json.loads(r.read() or b"{}")
71
+ except urllib.error.HTTPError as e:
72
+ try:
73
+ return e.code, json.loads(e.read() or b"{}")
74
+ except ValueError:
75
+ return e.code, {}
76
+
77
+
78
+ # Answers still fresh, by base, wallet and amount: reused until their signed "Valid until".
79
+ _fresh: dict = {}
80
+ _fresh_lock = threading.Lock()
81
+ _CACHE_MAX = 1000
82
+
83
+
84
+ def check(
85
+ pay_to: str,
86
+ amount_usd: Optional[float] = None,
87
+ *,
88
+ api_key: Optional[str] = None,
89
+ base_url: Optional[str] = None,
90
+ cache: bool = True,
91
+ timeout: float = 5.0,
92
+ ) -> dict:
93
+ """Asks Keptvow about one wallet before paying it.
94
+
95
+ Returns {"verdict": "ok" | "careful" | "stop", "advice": ..., "evidence": ..., "signed": ...}.
96
+ A fresh answer for the same wallet and amount is reused unless cache=False.
97
+ Raises RuntimeError when Keptvow refuses the question (e.g. not a wallet address).
98
+ """
99
+ base = _base(base_url)
100
+ key = (base, pay_to.strip().lower(), amount_usd)
101
+ if cache:
102
+ with _fresh_lock:
103
+ hit = _fresh.get(key)
104
+ if hit and hit[0] > time.time() * 1000:
105
+ return hit[1]
106
+ q = {"pay_to": pay_to}
107
+ if amount_usd is not None:
108
+ q["amount_usd"] = str(amount_usd)
109
+ headers = {"X-Api-Key": api_key} if api_key else {}
110
+ status, j = _request("GET", f"{base}/v1/check?{urllib.parse.urlencode(q)}", None, headers, timeout)
111
+ if status >= 400:
112
+ raise RuntimeError(j.get("error") or f"Keptvow answered {status}")
113
+ until = (j.get("signed") or {}).get("valid_until_ms") if isinstance(j, dict) else None
114
+ if cache and isinstance(until, (int, float)) and until > time.time() * 1000:
115
+ with _fresh_lock:
116
+ if len(_fresh) >= _CACHE_MAX:
117
+ _fresh.pop(next(iter(_fresh)))
118
+ _fresh[key] = (until, j)
119
+ return j
120
+
121
+
122
+ def _header(response: Any, name: str) -> Optional[str]:
123
+ headers = getattr(response, "headers", None) or {}
124
+ try:
125
+ return headers.get(name) or headers.get(name.lower()) or headers.get(name.upper())
126
+ except AttributeError:
127
+ return None
128
+
129
+
130
+ def payment_options(response: Any) -> list[dict]:
131
+ """The payment options a 402 answer offers, from its JSON body or PAYMENT-REQUIRED header."""
132
+ try:
133
+ body = response.json()
134
+ if isinstance(body, dict) and isinstance(body.get("accepts"), list):
135
+ return [a for a in body["accepts"] if isinstance(a, dict)]
136
+ except Exception:
137
+ pass
138
+ header = _header(response, "payment-required")
139
+ if header:
140
+ try:
141
+ j = json.loads(base64.b64decode(header))
142
+ if isinstance(j.get("accepts"), list):
143
+ return [a for a in j["accepts"] if isinstance(a, dict)]
144
+ except Exception:
145
+ pass
146
+ return []
147
+
148
+
149
+ def guard(
150
+ response: Any,
151
+ *,
152
+ allow_careful: bool = True,
153
+ fail_open: bool = True,
154
+ on_check: Optional[Callable[[dict], None]] = None,
155
+ api_key: Optional[str] = None,
156
+ base_url: Optional[str] = None,
157
+ cache: bool = True,
158
+ timeout: float = 5.0,
159
+ ) -> list[dict]:
160
+ """Checks every wallet a 402 Payment Required answer asks to be paid at.
161
+
162
+ Works with requests and httpx responses (anything with .json() and .headers). Raises
163
+ KeptvowStop for a wallet with a bad record — or, with allow_careful=False, one with no track
164
+ record. If Keptvow can't be reached, payment goes ahead unless fail_open=False.
165
+ Returns the check results.
166
+ """
167
+ results = []
168
+ for option in payment_options(response):
169
+ pay_to = option.get("payTo")
170
+ if not isinstance(pay_to, str):
171
+ continue
172
+ units = option.get("maxAmountRequired", option.get("amount"))
173
+ amount = None
174
+ if units is not None and str(option.get("asset", "")).lower() in _USDC:
175
+ try:
176
+ amount = float(units) / 1e6
177
+ except (TypeError, ValueError):
178
+ amount = None
179
+ try:
180
+ result = check(pay_to, amount, api_key=api_key, base_url=base_url, cache=cache, timeout=timeout)
181
+ except Exception:
182
+ if fail_open:
183
+ continue
184
+ raise
185
+ if on_check:
186
+ on_check(result)
187
+ results.append(result)
188
+ if result.get("verdict") == "stop" or (not allow_careful and result.get("verdict") == "careful"):
189
+ raise KeptvowStop(result)
190
+ return results
191
+
192
+
193
+ def report_outcome(
194
+ response_or_tx: Any,
195
+ *,
196
+ delivered: Optional[bool] = None,
197
+ pay_to: Optional[str] = None,
198
+ api_key: Optional[str] = None,
199
+ base_url: Optional[str] = None,
200
+ background: bool = True,
201
+ timeout: float = 5.0,
202
+ ) -> bool:
203
+ """Tells Keptvow whether a paid request delivered.
204
+
205
+ Pass the paid response (its PAYMENT-RESPONSE receipt names the transaction, and its status
206
+ says whether it delivered), or a transaction hash with delivered=True/False. Keptvow checks
207
+ the payment on-chain, so only real buyers count. With api_key, that seller's checks on the
208
+ key come back free. Never raises; returns False when there was nothing to report.
209
+ """
210
+ body: dict = {}
211
+ if isinstance(response_or_tx, str):
212
+ body["tx"] = response_or_tx
213
+ else:
214
+ header = _header(response_or_tx, "payment-response") or _header(response_or_tx, "x-payment-response")
215
+ if not header:
216
+ return False
217
+ try:
218
+ receipt = json.loads(base64.b64decode(header))
219
+ except Exception:
220
+ return False
221
+ if receipt.get("success") is False:
222
+ return False
223
+ tx = receipt.get("transaction") or receipt.get("txHash")
224
+ if not tx:
225
+ return False
226
+ body["tx"] = tx
227
+ status = getattr(response_or_tx, "status_code", None) or getattr(response_or_tx, "status", None)
228
+ if isinstance(status, int):
229
+ body["status"] = status
230
+ if delivered is not None:
231
+ body["delivered"] = delivered
232
+ elif "status" not in body:
233
+ return False
234
+ if pay_to:
235
+ body["pay_to"] = pay_to
236
+ url = f"{_base(base_url)}/v1/outcomes"
237
+
238
+ def send() -> None:
239
+ try:
240
+ _request("POST", url, body, {"X-Api-Key": api_key} if api_key else {}, timeout)
241
+ except Exception:
242
+ pass
243
+
244
+ if background:
245
+ threading.Thread(target=send, daemon=True).start()
246
+ else:
247
+ send()
248
+ return True
@@ -0,0 +1,69 @@
1
+ Metadata-Version: 2.4
2
+ Name: keptvow
3
+ Version: 0.1.0
4
+ Summary: Check who your AI agent is about to pay before an x402 payment goes out: ok, careful or stop.
5
+ Author-email: Keptvow <support@keptvow.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://keptvow.com
8
+ Project-URL: Documentation, https://keptvow.com/docs
9
+ Keywords: x402,ai-agents,payments,trust,erc-8004,base,usdc
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+
15
+ # keptvow
16
+
17
+ Check who your AI agent is about to pay **before** an x402 payment goes out.
18
+
19
+ Every answer is one word — **ok**, **careful** or **stop** — with the reason, built from real
20
+ USDC payments on Base (how many different buyers paid the seller, how many came back), reports
21
+ from buyers who did or didn't get what they paid for, and daily checks that the seller's
22
+ services answer. Free, no sign-up, no key, no dependencies.
23
+
24
+ ```sh
25
+ pip install keptvow
26
+ ```
27
+
28
+ ## Ask about one wallet
29
+
30
+ ```python
31
+ import keptvow
32
+
33
+ result = keptvow.check("0xSELLER", amount_usd=5)
34
+ result["verdict"] # "ok", "careful" or "stop"
35
+ result["advice"] # one plain sentence saying why
36
+ ```
37
+
38
+ ## Check a seller's 402 answer before paying
39
+
40
+ Works with `requests` and `httpx` responses:
41
+
42
+ ```python
43
+ r = session.get(url)
44
+ if r.status_code == 402:
45
+ keptvow.guard(r) # raises keptvow.KeptvowStop for a wallet with a bad record
46
+ # ...go on and pay with your x402 client
47
+ ```
48
+
49
+ `guard(r, allow_careful=False)` also blocks wallets with no track record;
50
+ `fail_open=False` blocks payment when Keptvow can't be reached.
51
+
52
+ ## Report whether you got what you paid for
53
+
54
+ ```python
55
+ keptvow.report_outcome(paid_response) # reads the PAYMENT-RESPONSE receipt
56
+ keptvow.report_outcome("0xTX", delivered=False) # or name the payment yourself
57
+ ```
58
+
59
+ Sent in the background and never raises. Keptvow checks the payment on-chain, so only real
60
+ buyers count — this is what lets honest sellers earn **ok** and flags the ones that take the
61
+ money and deliver nothing.
62
+
63
+ Set `KEPTVOW_URL` to use another Keptvow address.
64
+
65
+ ## Other ways in
66
+
67
+ No package needed: `GET https://keptvow.com/v1/check?pay_to=0x…&amount_usd=5`, an MCP server at
68
+ `https://keptvow.com/mcp`, and a guide for AI agents at <https://keptvow.com/llms.txt>. Everything
69
+ else: <https://keptvow.com/docs>.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ src/keptvow/__init__.py
4
+ src/keptvow.egg-info/PKG-INFO
5
+ src/keptvow.egg-info/SOURCES.txt
6
+ src/keptvow.egg-info/dependency_links.txt
7
+ src/keptvow.egg-info/top_level.txt
8
+ tests/test_keptvow.py
@@ -0,0 +1 @@
1
+ keptvow
@@ -0,0 +1,107 @@
1
+ """Runs without a network: Keptvow's answers are faked. Set KEPTVOW_TEST_URL to also ask a
2
+ running Keptvow server for real."""
3
+
4
+ import base64
5
+ import json
6
+ import os
7
+ import time
8
+ import unittest
9
+
10
+ import keptvow
11
+
12
+ SCAM = "0x" + "c3" * 20
13
+ GOOD = "0x" + "a1" * 20
14
+ SIGNED = "0x" + "5e" * 20
15
+ USDC = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
16
+
17
+
18
+ class FakeResponse:
19
+ def __init__(self, status, body=None, headers=None):
20
+ self.status_code = status
21
+ self._body = body
22
+ self.headers = headers or {}
23
+
24
+ def json(self):
25
+ if self._body is None:
26
+ raise ValueError("no body")
27
+ return self._body
28
+
29
+
30
+ class Test(unittest.TestCase):
31
+ def setUp(self):
32
+ self.calls = []
33
+ self.report_headers = []
34
+ keptvow._fresh.clear()
35
+ self.real = keptvow._request
36
+
37
+ def fake(method, url, body, headers, timeout):
38
+ self.calls.append((method, url, body))
39
+ if "/v1/check" in url:
40
+ q = dict(p.split("=", 1) for p in url.split("?", 1)[1].split("&"))
41
+ if q["pay_to"] == "down":
42
+ raise OSError("offline")
43
+ signed = {"valid_until_ms": (time.time() + 60) * 1000} if q["pay_to"] == SIGNED else None
44
+ return 200, {"pay_to": q["pay_to"], "verdict": "stop" if q["pay_to"] == SCAM else "ok", "advice": "test", "signed": signed}
45
+ self.report_headers.append(headers)
46
+ return 202, {"status": "queued"}
47
+
48
+ keptvow._request = fake
49
+
50
+ def tearDown(self):
51
+ keptvow._request = self.real
52
+
53
+ def test_check_asks_about_one_wallet(self):
54
+ r = keptvow.check(GOOD, 5, base_url="https://k.test")
55
+ self.assertEqual(r["verdict"], "ok")
56
+ self.assertIn("amount_usd=5", self.calls[-1][1])
57
+
58
+ def test_guard_stops_a_bad_seller_and_passes_a_good_one(self):
59
+ bad = FakeResponse(402, {"accepts": [{"payTo": SCAM, "maxAmountRequired": "2000000", "asset": USDC}]})
60
+ with self.assertRaises(keptvow.KeptvowStop) as e:
61
+ keptvow.guard(bad)
62
+ self.assertEqual(e.exception.check["pay_to"], SCAM)
63
+ self.assertIn("amount_usd=2.0", self.calls[-1][1])
64
+ # The payment request can also come in the PAYMENT-REQUIRED header.
65
+ header = base64.b64encode(json.dumps({"accepts": [{"payTo": GOOD}]}).encode()).decode()
66
+ self.assertEqual(keptvow.guard(FakeResponse(402, headers={"PAYMENT-REQUIRED": header}))[0]["verdict"], "ok")
67
+
68
+ def test_unreachable_keptvow_pays_on_unless_told_not_to(self):
69
+ r = FakeResponse(402, {"accepts": [{"payTo": "down"}]})
70
+ self.assertEqual(keptvow.guard(r), [])
71
+ with self.assertRaises(OSError):
72
+ keptvow.guard(r, fail_open=False)
73
+
74
+ def test_outcomes_come_from_the_payment_receipt(self):
75
+ receipt = base64.b64encode(json.dumps({"success": True, "transaction": "0x" + "ab" * 32}).encode()).decode()
76
+ self.assertTrue(keptvow.report_outcome(FakeResponse(200, {}, {"PAYMENT-RESPONSE": receipt}), background=False))
77
+ self.assertEqual(self.calls[-1][2], {"tx": "0x" + "ab" * 32, "status": 200})
78
+ self.assertFalse(keptvow.report_outcome(FakeResponse(200, {}, {})), "no receipt, nothing to report")
79
+ self.assertTrue(keptvow.report_outcome("0x" + "cd" * 32, delivered=False, background=False))
80
+ self.assertEqual(self.calls[-1][2], {"tx": "0x" + "cd" * 32, "delivered": False})
81
+
82
+ def test_a_fresh_signed_answer_is_reused(self):
83
+ keptvow.check(SIGNED)
84
+ keptvow.check(SIGNED.upper().replace("0X", "0x"))
85
+ self.assertEqual(sum(1 for c in self.calls if SIGNED in c[1].lower()), 1)
86
+ keptvow.check(SIGNED, cache=False)
87
+ self.assertEqual(sum(1 for c in self.calls if SIGNED in c[1].lower()), 2)
88
+ keptvow.check(GOOD)
89
+ keptvow.check(GOOD)
90
+ self.assertEqual(sum(1 for c in self.calls if GOOD in c[1]), 2, "unsigned answers are never reused")
91
+
92
+ def test_a_report_sent_with_a_key_carries_it(self):
93
+ keptvow.report_outcome("0x" + "ab" * 32, delivered=True, api_key="k_test", background=False)
94
+ self.assertEqual(self.report_headers[-1].get("X-Api-Key"), "k_test")
95
+
96
+
97
+ @unittest.skipUnless(os.environ.get("KEPTVOW_TEST_URL"), "set KEPTVOW_TEST_URL to test against a server")
98
+ class Live(unittest.TestCase):
99
+ def test_a_real_server_answers(self):
100
+ r = keptvow.check("0x" + "12" * 20, base_url=os.environ["KEPTVOW_TEST_URL"])
101
+ self.assertIn(r["verdict"], ("ok", "careful", "stop"))
102
+ with self.assertRaises(RuntimeError):
103
+ keptvow.check("not-a-wallet", base_url=os.environ["KEPTVOW_TEST_URL"])
104
+
105
+
106
+ if __name__ == "__main__":
107
+ unittest.main()