settled-x402 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- settled_x402/__init__.py +113 -0
- settled_x402/__main__.py +4 -0
- settled_x402/_networks.py +138 -0
- settled_x402/cli.py +184 -0
- settled_x402/core.py +1294 -0
- settled_x402/guard.py +305 -0
- settled_x402/py.typed +0 -0
- settled_x402/spend.py +448 -0
- settled_x402-0.1.0.dist-info/METADATA +274 -0
- settled_x402-0.1.0.dist-info/RECORD +13 -0
- settled_x402-0.1.0.dist-info/WHEEL +4 -0
- settled_x402-0.1.0.dist-info/entry_points.txt +2 -0
- settled_x402-0.1.0.dist-info/licenses/LICENSE +21 -0
settled_x402/__init__.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""settled-x402: check an x402 quote against Settled's independent evidence before your agent pays.
|
|
2
|
+
|
|
3
|
+
from settled_x402 import settled_guard
|
|
4
|
+
settled_guard(x402_client, max_usd_per_call=0.25, daily_budget_usd=5)
|
|
5
|
+
|
|
6
|
+
Docs: https://settled.tools/for-agents · the JavaScript twin: npm `settled-x402`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .core import (
|
|
14
|
+
DEFAULT_BASE_URL,
|
|
15
|
+
VERSION,
|
|
16
|
+
Answer,
|
|
17
|
+
Checker,
|
|
18
|
+
EndpointInfo,
|
|
19
|
+
OptionResult,
|
|
20
|
+
ParsedAccept,
|
|
21
|
+
ParsedQuote,
|
|
22
|
+
QuoteSummary,
|
|
23
|
+
SettledBlockedError,
|
|
24
|
+
SettledResult,
|
|
25
|
+
SettledSummary,
|
|
26
|
+
acheck,
|
|
27
|
+
ainspect,
|
|
28
|
+
assess,
|
|
29
|
+
block_message,
|
|
30
|
+
check,
|
|
31
|
+
decode_payment_required_header,
|
|
32
|
+
inspect,
|
|
33
|
+
normalize_network,
|
|
34
|
+
parse_quote,
|
|
35
|
+
quote_from_response,
|
|
36
|
+
)
|
|
37
|
+
from .guard import guard_all, settled_guard
|
|
38
|
+
from .spend import (
|
|
39
|
+
RARITIES,
|
|
40
|
+
RARITY_LEGENDARY_OVER_USD,
|
|
41
|
+
FileLedger,
|
|
42
|
+
LedgerInfo,
|
|
43
|
+
MemoryLedger,
|
|
44
|
+
SpendInfo,
|
|
45
|
+
SpendLedger,
|
|
46
|
+
SpendLimits,
|
|
47
|
+
SpendQuestion,
|
|
48
|
+
SpendToday,
|
|
49
|
+
default_budget_file,
|
|
50
|
+
rarity_of,
|
|
51
|
+
spent_today,
|
|
52
|
+
utc_day,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__version__ = VERSION
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"VERSION",
|
|
59
|
+
"__version__",
|
|
60
|
+
"DEFAULT_BASE_URL",
|
|
61
|
+
# entry points
|
|
62
|
+
"check",
|
|
63
|
+
"acheck",
|
|
64
|
+
"inspect",
|
|
65
|
+
"ainspect",
|
|
66
|
+
"settled_guard",
|
|
67
|
+
"guard_all",
|
|
68
|
+
"SettledAsyncTransport",
|
|
69
|
+
"SettledTransport",
|
|
70
|
+
# results and errors
|
|
71
|
+
"SettledResult",
|
|
72
|
+
"SettledBlockedError",
|
|
73
|
+
"QuoteSummary",
|
|
74
|
+
"OptionResult",
|
|
75
|
+
"SettledSummary",
|
|
76
|
+
"EndpointInfo",
|
|
77
|
+
"Answer",
|
|
78
|
+
# quotes
|
|
79
|
+
"parse_quote",
|
|
80
|
+
"quote_from_response",
|
|
81
|
+
"decode_payment_required_header",
|
|
82
|
+
"ParsedQuote",
|
|
83
|
+
"ParsedAccept",
|
|
84
|
+
"normalize_network",
|
|
85
|
+
# the pieces
|
|
86
|
+
"assess",
|
|
87
|
+
"block_message",
|
|
88
|
+
"Checker",
|
|
89
|
+
# spending limits and rarity
|
|
90
|
+
"rarity_of",
|
|
91
|
+
"RARITIES",
|
|
92
|
+
"RARITY_LEGENDARY_OVER_USD",
|
|
93
|
+
"spent_today",
|
|
94
|
+
"SpendToday",
|
|
95
|
+
"SpendInfo",
|
|
96
|
+
"SpendLimits",
|
|
97
|
+
"SpendQuestion",
|
|
98
|
+
"SpendLedger",
|
|
99
|
+
"FileLedger",
|
|
100
|
+
"MemoryLedger",
|
|
101
|
+
"LedgerInfo",
|
|
102
|
+
"default_budget_file",
|
|
103
|
+
"utc_day",
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def __getattr__(name: str) -> Any:
|
|
108
|
+
# The httpx transports import httpx, so they load on first use.
|
|
109
|
+
if name in ("SettledAsyncTransport", "SettledTransport"):
|
|
110
|
+
from . import guard
|
|
111
|
+
|
|
112
|
+
return getattr(guard, name)
|
|
113
|
+
raise AttributeError(f"module 'settled_x402' has no attribute {name!r}")
|
settled_x402/__main__.py
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Network and asset tables, copied from settled-x402 (JavaScript) so both guards read a quote the same way.
|
|
2
|
+
# x402 v1 network names as CAIP-2 ids (@x402/evm's v1 table, plus the names Settled also accepts).
|
|
3
|
+
|
|
4
|
+
V1_NETWORKS: dict[str, str] = {
|
|
5
|
+
"ethereum": "eip155:1",
|
|
6
|
+
"sepolia": "eip155:11155111",
|
|
7
|
+
"abstract": "eip155:2741",
|
|
8
|
+
"abstract-testnet": "eip155:11124",
|
|
9
|
+
"base": "eip155:8453",
|
|
10
|
+
"base-sepolia": "eip155:84532",
|
|
11
|
+
"avalanche": "eip155:43114",
|
|
12
|
+
"avalanche-fuji": "eip155:43113",
|
|
13
|
+
"iotex": "eip155:4689",
|
|
14
|
+
"sei": "eip155:1329",
|
|
15
|
+
"sei-testnet": "eip155:1328",
|
|
16
|
+
"polygon": "eip155:137",
|
|
17
|
+
"polygon-amoy": "eip155:80002",
|
|
18
|
+
"peaq": "eip155:3338",
|
|
19
|
+
"story": "eip155:1514",
|
|
20
|
+
"educhain": "eip155:41923",
|
|
21
|
+
"skale-base-sepolia": "eip155:324705682",
|
|
22
|
+
"megaeth": "eip155:4326",
|
|
23
|
+
"monad": "eip155:143",
|
|
24
|
+
"monad-testnet": "eip155:10143",
|
|
25
|
+
"stable": "eip155:988",
|
|
26
|
+
"stable-testnet": "eip155:2201",
|
|
27
|
+
"celo": "eip155:42220",
|
|
28
|
+
"flare": "eip155:14",
|
|
29
|
+
"arbitrum": "eip155:42161",
|
|
30
|
+
"optimism": "eip155:10",
|
|
31
|
+
"solana": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
|
|
32
|
+
"solana-devnet": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
|
|
33
|
+
}
|
|
34
|
+
NETWORK_NAMES: dict[str, str] = {v: k for k, v in V1_NETWORKS.items()}
|
|
35
|
+
|
|
36
|
+
# USD stablecoins and their decimals by network: @x402/evm 2.28's default assets, plus USDbC on Base, Optimism USDC and
|
|
37
|
+
# Solana USDC. EVM addresses are lowercased; Solana mints keep their case.
|
|
38
|
+
USD_ASSETS: dict[str, dict[str, int]] = {
|
|
39
|
+
"eip155:8453": {
|
|
40
|
+
"0x833589fcd6edb6e08f4c7c32d4f71b54bda02913": 6,
|
|
41
|
+
"0xd9aaec86b65d86f6a7b5b1b0c42ffa531710b6ca": 6,
|
|
42
|
+
},
|
|
43
|
+
"eip155:84532": {
|
|
44
|
+
"0x036cbd53842c5426634e7929541ec2318f3dcf7e": 6,
|
|
45
|
+
},
|
|
46
|
+
"eip155:1": {
|
|
47
|
+
"0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": 6,
|
|
48
|
+
},
|
|
49
|
+
"eip155:43114": {
|
|
50
|
+
"0xb97ef9ef8734c71904d8002f8b6bc66dd9c48a6e": 6,
|
|
51
|
+
},
|
|
52
|
+
"eip155:4326": {
|
|
53
|
+
"0xfafddbb3fc7688494971a79cc65dca3ef82079e7": 18,
|
|
54
|
+
},
|
|
55
|
+
"eip155:143": {
|
|
56
|
+
"0x754704bc059f8c67012fed69bc8a327a5aafb603": 6,
|
|
57
|
+
},
|
|
58
|
+
"eip155:10143": {
|
|
59
|
+
"0x534b2f3a21130d7a60830c2df862319e593943a3": 6,
|
|
60
|
+
},
|
|
61
|
+
"eip155:988": {
|
|
62
|
+
"0x779ded0c9e1022225f8e0630b35a9b54be713736": 6,
|
|
63
|
+
},
|
|
64
|
+
"eip155:2201": {
|
|
65
|
+
"0x78cf24370174180738c5b8e352b6d14c83a6c9a9": 6,
|
|
66
|
+
},
|
|
67
|
+
"eip155:137": {
|
|
68
|
+
"0x3c499c542cef5e3811e1192ce70d8cc03d5c3359": 6,
|
|
69
|
+
},
|
|
70
|
+
"eip155:42161": {
|
|
71
|
+
"0xaf88d065e77c8cc2239327c5edb3a432268e5831": 6,
|
|
72
|
+
},
|
|
73
|
+
"eip155:421614": {
|
|
74
|
+
"0x75faf114eafb1bdbe2f0316df893fd58ce46aa4d": 6,
|
|
75
|
+
},
|
|
76
|
+
"eip155:31612": {
|
|
77
|
+
"0xdd468a1ddc392dcdbef6db6e34e89aa338f9f186": 18,
|
|
78
|
+
},
|
|
79
|
+
"eip155:31611": {
|
|
80
|
+
"0x118917a40faf1cd7a13db0ef56c86de7973ac503": 18,
|
|
81
|
+
},
|
|
82
|
+
"eip155:723487": {
|
|
83
|
+
"0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb": 6,
|
|
84
|
+
},
|
|
85
|
+
"eip155:72344": {
|
|
86
|
+
"0x33ad9e4bd16b69b5bfded37d8b5d9ff9aba014fb": 6,
|
|
87
|
+
},
|
|
88
|
+
"eip155:36900": {
|
|
89
|
+
"0x9cb8142aebbcdc60af7c97af897a67a8f3ca71c2": 6,
|
|
90
|
+
},
|
|
91
|
+
"eip155:190415": {
|
|
92
|
+
"0x401ecb1d350407f13ba348573e5630b83638e30d": 6,
|
|
93
|
+
},
|
|
94
|
+
"eip155:181228": {
|
|
95
|
+
"0x401ecb1d350407f13ba348573e5630b83638e30d": 6,
|
|
96
|
+
},
|
|
97
|
+
"eip155:50": {
|
|
98
|
+
"0xfa2958cb79b0491cc627c1557f441ef849ca8eb1": 6,
|
|
99
|
+
},
|
|
100
|
+
"eip155:51": {
|
|
101
|
+
"0xb5ab69f7bbada22b28e79c8ffaece55ef1c771d4": 6,
|
|
102
|
+
},
|
|
103
|
+
"eip155:38833": {
|
|
104
|
+
"0xa5b8bf902b2844da17d4506cc827f7f1681735e7": 6,
|
|
105
|
+
},
|
|
106
|
+
"eip155:14": {
|
|
107
|
+
"0xe7cd86e13ac4309349f30b3435a9d337750fc82d": 6,
|
|
108
|
+
},
|
|
109
|
+
"eip155:42220": {
|
|
110
|
+
"0xceba9300f2b948710d2653dd7b07f33a8b32118c": 6,
|
|
111
|
+
"0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e": 6,
|
|
112
|
+
"0xd2ab3c9a02dbbab236bfec45d1d755df4267f771": 6,
|
|
113
|
+
},
|
|
114
|
+
"eip155:11142220": {
|
|
115
|
+
"0x01c5c0122039549ad1493b8220cabedd739bc44e": 6,
|
|
116
|
+
},
|
|
117
|
+
"eip155:1329": {
|
|
118
|
+
"0xe15fc38f6d8c56af07bbcbe3baf5708a2bf42392": 6,
|
|
119
|
+
},
|
|
120
|
+
"eip155:1328": {
|
|
121
|
+
"0x4fcf1784b31630811181f670aea7a7bef803eaed": 6,
|
|
122
|
+
},
|
|
123
|
+
"eip155:5042": {
|
|
124
|
+
"0x3600000000000000000000000000000000000000": 6,
|
|
125
|
+
},
|
|
126
|
+
"eip155:5042002": {
|
|
127
|
+
"0x3600000000000000000000000000000000000000": 6,
|
|
128
|
+
},
|
|
129
|
+
"eip155:10": {
|
|
130
|
+
"0x0b2c639c533813f4aa9d7837caf62653d097ff85": 6,
|
|
131
|
+
},
|
|
132
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": {
|
|
133
|
+
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": 6,
|
|
134
|
+
},
|
|
135
|
+
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1": {
|
|
136
|
+
"4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU": 6,
|
|
137
|
+
},
|
|
138
|
+
}
|
settled_x402/cli.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""settled-x402 on the command line: `settled-x402 check <url>` and `settled-x402 spend`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .core import VERSION, inspect, normalize_network
|
|
13
|
+
from .spend import RARITIES, spent_today
|
|
14
|
+
|
|
15
|
+
HELP = f"""settled-x402 {VERSION}: check x402 endpoints against Settled before paying
|
|
16
|
+
|
|
17
|
+
Usage:
|
|
18
|
+
settled-x402 check <url> [--network <name>] [--post] [--json] [--pass <token>]
|
|
19
|
+
Read the endpoint's 402 quote (nothing is paid) and ask Settled about it.
|
|
20
|
+
--network picks the option you would pay on (default: the network Settled has on record).
|
|
21
|
+
Exit code: 0 pay, 2 caution, 3 avoid, 4 unknown, 1 usage or internal error.
|
|
22
|
+
settled-x402 spend [--budget-file <path>] [--json]
|
|
23
|
+
Today's tally from the spend ledger a guard with daily_budget_usd keeps (default: SETTLED_BUDGET_FILE,
|
|
24
|
+
else ~/.settled-x402/spend.json): what the guard let through, per call, with each call's loot rarity.
|
|
25
|
+
settled-x402 --version
|
|
26
|
+
|
|
27
|
+
Environment: SETTLED_PASS (a Settled day pass), SETTLED_PAYER_TOKEN (an x4t_ payer token), SETTLED_BASE_URL,
|
|
28
|
+
SETTLED_ALLOW_PRIVATE_HOSTS=1.
|
|
29
|
+
Docs: https://settled.tools/for-agents"""
|
|
30
|
+
|
|
31
|
+
EXIT = {"pay": 0, "skipped": 0, "caution": 2, "avoid": 3, "unknown": 4}
|
|
32
|
+
|
|
33
|
+
NETS = {
|
|
34
|
+
"eip155:8453": "base",
|
|
35
|
+
"eip155:84532": "base-sepolia",
|
|
36
|
+
"eip155:137": "polygon",
|
|
37
|
+
"eip155:42161": "arbitrum",
|
|
38
|
+
"eip155:10": "optimism",
|
|
39
|
+
"eip155:1": "ethereum",
|
|
40
|
+
"eip155:43114": "avalanche",
|
|
41
|
+
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp": "solana",
|
|
42
|
+
}
|
|
43
|
+
MATCH = {
|
|
44
|
+
"match": "payTo matches Settled",
|
|
45
|
+
"evm_match": "same address as on Settled's network",
|
|
46
|
+
"mismatch": "payTo DIFFERS from Settled",
|
|
47
|
+
"asset_mismatch": "different asset and payTo",
|
|
48
|
+
"evm_mismatch": "different address",
|
|
49
|
+
"not_compared": "not on record at Settled",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _short(a: Any) -> str:
|
|
54
|
+
return f"{a[:6]}…{a[-4:]}" if isinstance(a, str) and len(a) > 14 else str(a if a is not None else "?")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _money(n: Any) -> str:
|
|
58
|
+
if isinstance(n, bool) or not isinstance(n, (int, float)):
|
|
59
|
+
return "an unknown amount"
|
|
60
|
+
if n >= 1:
|
|
61
|
+
return f"${n:.2f}"
|
|
62
|
+
v = float(f"{n:.4g}")
|
|
63
|
+
return f"${int(v)}" if v == int(v) else f"${v!r}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _net(n: Any) -> str:
|
|
67
|
+
return NETS.get(n, n or "?") if isinstance(n, str) else "?"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def run_check(target: str, args: argparse.Namespace) -> int:
|
|
71
|
+
from urllib.parse import urlsplit
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
parsed = urlsplit(target)
|
|
75
|
+
except ValueError:
|
|
76
|
+
parsed = None
|
|
77
|
+
if parsed is None or parsed.scheme not in ("http", "https") or not parsed.hostname:
|
|
78
|
+
sys.stderr.write(f"Not an http(s) URL: {target}\n")
|
|
79
|
+
return 1
|
|
80
|
+
result = inspect(
|
|
81
|
+
target,
|
|
82
|
+
method="POST" if args.post else "GET",
|
|
83
|
+
network=normalize_network(args.network) if args.network else "auto",
|
|
84
|
+
pass_token=args.pass_token or os.environ.get("SETTLED_PASS"),
|
|
85
|
+
payer_token=os.environ.get("SETTLED_PAYER_TOKEN"),
|
|
86
|
+
base_url=os.environ.get("SETTLED_BASE_URL"),
|
|
87
|
+
allow_private_hosts=os.environ.get("SETTLED_ALLOW_PRIVATE_HOSTS") == "1",
|
|
88
|
+
)
|
|
89
|
+
live = result.endpoint
|
|
90
|
+
assert live is not None
|
|
91
|
+
if args.json:
|
|
92
|
+
d = result.to_dict()
|
|
93
|
+
d.pop("endpoint", None)
|
|
94
|
+
d["endpoint_http_status"] = live.http_status
|
|
95
|
+
d["endpoint_error"] = live.error
|
|
96
|
+
sys.stdout.write(json.dumps(d, indent=2) + "\n")
|
|
97
|
+
return EXIT[result.verdict]
|
|
98
|
+
loot = f" · {RARITIES[result.rarity]['label'].lower()} loot ({RARITIES[result.rarity]['rule']})" if result.rarity else ""
|
|
99
|
+
lines = [f"{result.verdict.upper()} · {result.url or live.url}{loot}"]
|
|
100
|
+
q = result.quote
|
|
101
|
+
if q is not None:
|
|
102
|
+
lines.append(f" quote {_money(q.price_usd)} on {_net(q.network)} to {_short(q.pay_to)}{f' ({q.accepts} options)' if q.accepts > 1 else ''}")
|
|
103
|
+
elif live.error:
|
|
104
|
+
lines.append(f" quote none (could not reach the endpoint: {live.error})")
|
|
105
|
+
elif live.http_status == 402:
|
|
106
|
+
lines.append(" quote unreadable (the endpoint answered 402, but its quote could not be parsed)")
|
|
107
|
+
else:
|
|
108
|
+
lines.append(f" quote none (the endpoint answered HTTP {live.http_status} without asking for payment{'' if args.post else '; try --post'})")
|
|
109
|
+
if len(result.options) > 1:
|
|
110
|
+
for i, o in enumerate(result.options):
|
|
111
|
+
head = " options " if i == 0 else " "
|
|
112
|
+
text = f"{_net(o.network)}: {_money(o.price_usd)} to {_short(o.pay_to)} · {MATCH.get(o.match, o.match)}"
|
|
113
|
+
lines.append(f"{head}{text if o.considered else f'({text})'}")
|
|
114
|
+
judged: list[str] = []
|
|
115
|
+
for o in result.options:
|
|
116
|
+
if o.considered and _net(o.network) not in judged:
|
|
117
|
+
judged.append(_net(o.network))
|
|
118
|
+
if judged and any(not o.considered for o in result.options):
|
|
119
|
+
lines.append(f" the verdict covers the options on {', '.join(judged)}; pick another with --network")
|
|
120
|
+
s = result.settled
|
|
121
|
+
if s is not None:
|
|
122
|
+
bits = ", ".join(x for x in (s.status, s.payability) if x)
|
|
123
|
+
scout = f" · scout's last purchase {str(s.last_delivery_at)[:10]}{f' (tx {_short(s.receipt_tx)})' if s.receipt_tx else ''}" if s.last_delivery_at else ""
|
|
124
|
+
lines.append(f" Settled {bits or 'no record'}{f' · {_money(s.price_usd)} on {_net(s.network)} to {_short(s.pay_to)}' if s.pay_to else ''}{scout}")
|
|
125
|
+
if s.recommendation:
|
|
126
|
+
lines.append(f" preflight {s.recommendation}")
|
|
127
|
+
for i, r in enumerate(result.reasons):
|
|
128
|
+
lines.append(f"{' why ' if i == 0 else ' '}{r}")
|
|
129
|
+
if result.details:
|
|
130
|
+
lines.append(f" details {result.details}")
|
|
131
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
132
|
+
return EXIT[result.verdict]
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def run_spend(args: argparse.Namespace) -> int:
|
|
136
|
+
today = spent_today(budget_file=args.budget_file or os.environ.get("SETTLED_BUDGET_FILE") or None)
|
|
137
|
+
if args.json:
|
|
138
|
+
sys.stdout.write(json.dumps(today.to_dict(), indent=2) + "\n")
|
|
139
|
+
return 0
|
|
140
|
+
lines = [f"{today.day} (UTC) · {_money(today.spent_usd)} let through in {today.calls} paid call{'' if today.calls == 1 else 's'} · ledger {today.ledger.where or today.ledger.kind}"]
|
|
141
|
+
for e in today.entries:
|
|
142
|
+
at = str(e.get("at", ""))[11:19]
|
|
143
|
+
lines.append(f" {at} {_money(e.get('priceUsd')):>8} {str(e.get('rarity') or '?'):<9} {e.get('url') or '?'}")
|
|
144
|
+
if not today.entries:
|
|
145
|
+
lines.append(" nothing yet today")
|
|
146
|
+
sys.stdout.write("\n".join(lines) + "\n")
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
151
|
+
parser = argparse.ArgumentParser(prog="settled-x402", add_help=False)
|
|
152
|
+
parser.add_argument("command", nargs="?")
|
|
153
|
+
parser.add_argument("target", nargs="?")
|
|
154
|
+
parser.add_argument("--post", action="store_true")
|
|
155
|
+
parser.add_argument("--json", action="store_true")
|
|
156
|
+
parser.add_argument("--pass", dest="pass_token")
|
|
157
|
+
parser.add_argument("--network")
|
|
158
|
+
parser.add_argument("--budget-file", dest="budget_file")
|
|
159
|
+
parser.add_argument("-v", "--version", action="store_true")
|
|
160
|
+
parser.add_argument("-h", "--help", action="store_true")
|
|
161
|
+
try:
|
|
162
|
+
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
163
|
+
except SystemExit:
|
|
164
|
+
sys.stderr.write(HELP + "\n")
|
|
165
|
+
return 1
|
|
166
|
+
if args.version:
|
|
167
|
+
sys.stdout.write(VERSION + "\n")
|
|
168
|
+
return 0
|
|
169
|
+
if args.help or not args.command:
|
|
170
|
+
sys.stdout.write(HELP + "\n")
|
|
171
|
+
return 0 if args.help else 1
|
|
172
|
+
if args.command == "check":
|
|
173
|
+
if not args.target:
|
|
174
|
+
sys.stderr.write("Usage: settled-x402 check <url>\n")
|
|
175
|
+
return 1
|
|
176
|
+
return run_check(args.target, args)
|
|
177
|
+
if args.command == "spend":
|
|
178
|
+
return run_spend(args)
|
|
179
|
+
sys.stderr.write(f"Unknown command: {args.command}\n\n{HELP}\n")
|
|
180
|
+
return 1
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__": # pragma: no cover
|
|
184
|
+
sys.exit(main())
|