bitcoinstratigraphy-agent-yield 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.
- bitcoinstratigraphy_agent_yield-0.1.0/.gitignore +67 -0
- bitcoinstratigraphy_agent_yield-0.1.0/LICENSE.md +12 -0
- bitcoinstratigraphy_agent_yield-0.1.0/PKG-INFO +195 -0
- bitcoinstratigraphy_agent_yield-0.1.0/README.md +174 -0
- bitcoinstratigraphy_agent_yield-0.1.0/pyproject.toml +38 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/__init__.py +14 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/adapters.py +172 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/client.py +406 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/errors.py +5 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/invoices.py +98 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/protocol.py +122 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/py.typed +1 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/receipts.py +27 -0
- bitcoinstratigraphy_agent_yield-0.1.0/src/bitcoinstratigraphy_agent_yield/state.py +138 -0
- bitcoinstratigraphy_agent_yield-0.1.0/tests/conftest.py +246 -0
- bitcoinstratigraphy_agent_yield-0.1.0/tests/test_adapters.py +102 -0
- bitcoinstratigraphy_agent_yield-0.1.0/tests/test_client.py +362 -0
- bitcoinstratigraphy_agent_yield-0.1.0/tests/test_invoices_receipts.py +64 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
|
2
|
+
|
|
3
|
+
# compiled output
|
|
4
|
+
dist
|
|
5
|
+
tmp
|
|
6
|
+
out-tsc
|
|
7
|
+
*.tsbuildinfo
|
|
8
|
+
.expo
|
|
9
|
+
.expo-shared
|
|
10
|
+
|
|
11
|
+
# dependencies
|
|
12
|
+
node_modules
|
|
13
|
+
|
|
14
|
+
# IDEs and editors
|
|
15
|
+
/.idea
|
|
16
|
+
.project
|
|
17
|
+
.classpath
|
|
18
|
+
.c9/
|
|
19
|
+
*.launch
|
|
20
|
+
.settings/
|
|
21
|
+
*.sublime-workspace
|
|
22
|
+
|
|
23
|
+
# IDE - VSCode
|
|
24
|
+
.vscode/*
|
|
25
|
+
!.vscode/settings.json
|
|
26
|
+
!.vscode/tasks.json
|
|
27
|
+
!.vscode/launch.json
|
|
28
|
+
!.vscode/extensions.json
|
|
29
|
+
|
|
30
|
+
# misc
|
|
31
|
+
/.sass-cache
|
|
32
|
+
/connect.lock
|
|
33
|
+
/coverage
|
|
34
|
+
/libpeerconnection.log
|
|
35
|
+
npm-debug.log
|
|
36
|
+
yarn-error.log
|
|
37
|
+
testem.log
|
|
38
|
+
/typings
|
|
39
|
+
|
|
40
|
+
# System Files
|
|
41
|
+
.DS_Store
|
|
42
|
+
Thumbs.db
|
|
43
|
+
|
|
44
|
+
.cursor/rules/nx-rules.mdc
|
|
45
|
+
.github/instructions/nx.instructions.md
|
|
46
|
+
|
|
47
|
+
# Replit
|
|
48
|
+
.cache/
|
|
49
|
+
.local/
|
|
50
|
+
.data/
|
|
51
|
+
|
|
52
|
+
# Private enterprise audit evidence; never commit trace data.
|
|
53
|
+
**/.agent-yield-state/
|
|
54
|
+
/audit_trail.jsonl
|
|
55
|
+
/audit_trail.jsonl.*
|
|
56
|
+
|
|
57
|
+
# Enterprise SDK build output and Python bytecode.
|
|
58
|
+
/sdk/python/build/
|
|
59
|
+
/sdk/python/*.egg-info/
|
|
60
|
+
__pycache__/
|
|
61
|
+
*.pyc
|
|
62
|
+
|
|
63
|
+
# Runtime provenance archives and generated verification artifacts.
|
|
64
|
+
/generated-artifacts/
|
|
65
|
+
|
|
66
|
+
# Private tool-integrity signing keys (standalone deployments only).
|
|
67
|
+
**/.keys/
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (c) 2026 Charles R. Strogish. All Rights Reserved.
|
|
2
|
+
|
|
3
|
+
This software is dual-licensed under GNU AGPL version 3 only
|
|
4
|
+
(SPDX: AGPL-3.0-only), or a separately executed Commercial License from
|
|
5
|
+
Charles R. Strogish (SPDX: LicenseRef-Commercial).
|
|
6
|
+
|
|
7
|
+
The AGPLv3 text is available at https://www.gnu.org/licenses/agpl-3.0.html.
|
|
8
|
+
The repository's LICENSE.md contains the full license text and licensing rider:
|
|
9
|
+
https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp/blob/main/LICENSE.md
|
|
10
|
+
|
|
11
|
+
This notice does not grant commercial licensing rights. AGPLv3 permits
|
|
12
|
+
commercial use when its conditions are met.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: bitcoinstratigraphy-agent-yield
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One-call MCP discovery, budgeted L402 settlement and fiduciary tools for Python agents.
|
|
5
|
+
Project-URL: Homepage, https://bitcoinstratigraphy.com
|
|
6
|
+
Project-URL: Repository, https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp
|
|
7
|
+
License-Expression: AGPL-3.0-only OR LicenseRef-Commercial
|
|
8
|
+
License-File: LICENSE.md
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Typing :: Typed
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: bech32==1.2.0
|
|
16
|
+
Requires-Dist: coincurve==21.0.0
|
|
17
|
+
Requires-Dist: httpx==0.28.1
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: pytest==9.2.0; extra == 'test'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# bitcoinstratigraphy-agent-yield
|
|
23
|
+
|
|
24
|
+
One-call Python onboarding for Bitcoin Stratigraphy MCP tools: HTTPX discovery,
|
|
25
|
+
automatic bounded L402 retries, durable spending reservations, and the same
|
|
26
|
+
stderr terminal receipts as `@bitcoinstratigraphy/agent-yield`.
|
|
27
|
+
|
|
28
|
+
Install from PyPI:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
pip install bitcoinstratigraphy-agent-yield
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
For local development, install from this checkout:
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
pip install ./packages/python-agent-yield
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Two-line quick starts
|
|
41
|
+
|
|
42
|
+
Start with your existing `agent_config`. Its `fiduciary` policy is the explicit
|
|
43
|
+
wallet authorization shown below. No wallet is created, connected or funded
|
|
44
|
+
by installing or importing this package.
|
|
45
|
+
|
|
46
|
+
### LangChain
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
50
|
+
|
|
51
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "langchain"})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Pass `agent["tools"]` to `langchain.agents.create_agent(model=..., tools=...)`.
|
|
55
|
+
Existing tools are preserved; discovered tools are native `StructuredTool`s
|
|
56
|
+
with sync `.invoke()` and async `.ainvoke()`.
|
|
57
|
+
|
|
58
|
+
### CrewAI
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
62
|
+
|
|
63
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "crewai"})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Pass `agent["tools"]` to `crewai.Agent(role=..., goal=..., backstory=..., tools=...)`.
|
|
67
|
+
They are native CrewAI `BaseTool`s. You can also wrap an existing public-tools
|
|
68
|
+
CrewAI agent with `with_fiduciary_yield(existing_agent, fiduciary=policy)`;
|
|
69
|
+
this returns a copy, never mutating the original agent.
|
|
70
|
+
|
|
71
|
+
### AutoGen
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
75
|
+
|
|
76
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "autogen"})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Pass `agent["tools"]` to `autogen_agentchat.agents.AssistantAgent(..., tools=...)`.
|
|
80
|
+
These are native `autogen_core.tools.BaseTool`s, not OpenAI descriptors requiring
|
|
81
|
+
a separate dispatcher. Cancellation is checked before starting execution.
|
|
82
|
+
|
|
83
|
+
Framework imports are lazy. Install your chosen framework separately; the base
|
|
84
|
+
wheel has **only three direct runtime dependencies**, all exactly pinned:
|
|
85
|
+
HTTPX, bech32 and coincurve. No LangChain, CrewAI, AutoGen, NWC or MCP framework
|
|
86
|
+
is forced on other consumers. Native adapters target the current public
|
|
87
|
+
LangChain Core 1.x, CrewAI 1.x and AutoGen Core 0.7 APIs.
|
|
88
|
+
|
|
89
|
+
## Explicit wallet and spending policy
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
policy = {
|
|
93
|
+
"wallet": authorized_wallet,
|
|
94
|
+
"max_payment_sats": 500,
|
|
95
|
+
"max_total_sats": 5000,
|
|
96
|
+
"fee_reserve_sats": 10,
|
|
97
|
+
"state_directory": ".agent-yield-state",
|
|
98
|
+
}
|
|
99
|
+
agent_config = {"tools": existing_tools, "fiduciary": policy}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`authorized_wallet.pay_invoice(bolt11)` is a **synchronous** wallet adapter
|
|
103
|
+
returning `{"preimage": "<64 hex characters>", "fees_paid": 0}`.
|
|
104
|
+
`fees_paid` is optional integer millisatoshis. Use your wallet provider's
|
|
105
|
+
authorized SDK inside the adapter; secrets never belong in prompts, source,
|
|
106
|
+
HTTP headers, terminal receipts, or tool arguments. This scaffold does **not**
|
|
107
|
+
implement NWC wallet transport.
|
|
108
|
+
|
|
109
|
+
Without a wallet and explicit positive budgets, discovery and free tools work;
|
|
110
|
+
paid execution fails closed. Optional `make_invoice(amount_msats)` returns
|
|
111
|
+
`{"invoice": "<receiving BOLT11>"}` to enable automatic yield withdrawals.
|
|
112
|
+
Wallet adapters must enforce their own operation timeouts and fee ceiling:
|
|
113
|
+
Python cannot cancel an external payment already submitted to a wallet.
|
|
114
|
+
|
|
115
|
+
`agent["fiduciary"]` exposes `.list_tools()`, `.call_tool(name, arguments)`,
|
|
116
|
+
`await .acall_tool(name, arguments)`, `.recover_claim(payment_hash)`, and
|
|
117
|
+
`.close()`. Framework tools are added to a configuration before constructing a
|
|
118
|
+
graph/AssistantAgent; this does not retrofit private internals of running agents.
|
|
119
|
+
Async execution offloads synchronous HTTP/wallet work to a worker thread.
|
|
120
|
+
Cancelling the awaiting task does not reverse an in-progress wallet operation.
|
|
121
|
+
Close the config's client at application shutdown. For public-tools object
|
|
122
|
+
wrapping, supply an application-owned `http_client` and close it yourself.
|
|
123
|
+
|
|
124
|
+
## Connection and payment guarantees
|
|
125
|
+
|
|
126
|
+
- Discovers the live paginated catalog at `https://bitcoinstratigraphy.com/mcp`.
|
|
127
|
+
Handles MCP `2026-07-28`, JSON and SSE, bounded response sizes, timeout and
|
|
128
|
+
one bounded Hashcash admission retry. Discovery never pays a wallet.
|
|
129
|
+
- Validates mainnet BOLT11 checksum, recoverable signature, explicit payee,
|
|
130
|
+
amount, expiry and payment hash against the HTTP JSON quote before spending.
|
|
131
|
+
- Reserves invoice principal plus fee allowance in private SQLite storage
|
|
132
|
+
**before** contacting the wallet. Per-invoice and cumulative limits are
|
|
133
|
+
integer satoshis. Reservations survive threads, processes and restarts.
|
|
134
|
+
Reported excessive/invalid fees block future new payments.
|
|
135
|
+
- Verifies every preimage against its invoice hash, then retries the identical
|
|
136
|
+
MCP body/ID exactly once with `Authorization: L402 macaroon:preimage`.
|
|
137
|
+
Credentials are bound to the exact invoice/request, not globally reused
|
|
138
|
+
across unrelated tools. A repeated 402 never triggers a second payment.
|
|
139
|
+
- Never follows redirects or replaces caller-supplied payment credentials.
|
|
140
|
+
Supply `identity_headers(url, method, body_bytes)` for fresh NIP-98 headers;
|
|
141
|
+
it runs again for admission and paid retries. Valid Genesis Grant calls incur
|
|
142
|
+
no payment, paid-history credit, yield allocation or earned receipt.
|
|
143
|
+
- Unknown payments and invalid returned proofs stay reserved. **Do not delete
|
|
144
|
+
the state directory or change directories to retry a payment.** Operators
|
|
145
|
+
must establish the payment outcome before recovery. Use private persistent
|
|
146
|
+
storage, exclude it from Git, and use one directory per wallet and policy.
|
|
147
|
+
|
|
148
|
+
The SQLite ledger contains sensitive payment proofs, though never wallet keys.
|
|
149
|
+
The folder is created with mode 0700 and the database with 0600. Protect and
|
|
150
|
+
back up this directory. Existing directories must already be private.
|
|
151
|
+
This is client-side accounting, not a replacement for server-side settlement
|
|
152
|
+
ledgers or a wallet-enforced spending limit.
|
|
153
|
+
|
|
154
|
+
## Terminal receipts
|
|
155
|
+
|
|
156
|
+
On a successful paid execution with a receiving wallet:
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
[Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Allocated: 21 Sats (settlement pending).
|
|
160
|
+
[Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Earned: 21 Sats routed to agent wallet.
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Allocated is yellow; earned is green. An earned receipt appears **only** after
|
|
164
|
+
the claim API confirms `SETTLED` **and** `payment_sent: true`, never on
|
|
165
|
+
allocation, free grants, pending claims, failed tools or failed payouts.
|
|
166
|
+
Millisatoshi precision is preserved. Logging goes exclusively to `sys.stderr`
|
|
167
|
+
to preserve MCP JSON-RPC stdout. `QUIET_STRATIGRAPHY=true` disables receipts at
|
|
168
|
+
event time; logging failures never change successful tool results.
|
|
169
|
+
|
|
170
|
+
Claims are best effort, not guaranteed income. `on_error({"code": ...})`
|
|
171
|
+
reports recovery needs without exposing wallet/server error text.
|
|
172
|
+
`on_yield_claim({"status": ..., "amount_msats": ...})` observes claim status.
|
|
173
|
+
`.recover_claim(payment_hash)` replays only a persisted withdrawal request and
|
|
174
|
+
never buys a new tool invoice. Recovery after a crash during receiving-invoice
|
|
175
|
+
creation needs operator intervention; it does not create a second invoice.
|
|
176
|
+
Claims run synchronously after paid execution, bounded by the HTTP timeout.
|
|
177
|
+
|
|
178
|
+
## Development / packaging (no publication)
|
|
179
|
+
|
|
180
|
+
From the repository root, using its Python environment:
|
|
181
|
+
|
|
182
|
+
```sh
|
|
183
|
+
uv run --no-sync python -m pytest packages/python-agent-yield
|
|
184
|
+
cd packages/python-agent-yield
|
|
185
|
+
uv run --no-sync python -m hatchling build -d ../../generated-artifacts/python-agent-yield
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Or install `.[test]` inside this package, then run `python -m pytest`.
|
|
189
|
+
The dedicated tests use signed synthetic invoices, `httpx.MockTransport`,
|
|
190
|
+
temporary state, and fake wallets. They do not contact mainnet or move funds.
|
|
191
|
+
Native LangChain/AutoGen integration tests run when those frameworks are
|
|
192
|
+
installed; the CrewAI adapter has a public-API contract fixture without pulling
|
|
193
|
+
CrewAI's full dependency tree into this standalone package.
|
|
194
|
+
Wheel/sdist build output belongs in `generated-artifacts/`, never source control.
|
|
195
|
+
Building never uploads to PyPI; publication is an explicit release step.
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# bitcoinstratigraphy-agent-yield
|
|
2
|
+
|
|
3
|
+
One-call Python onboarding for Bitcoin Stratigraphy MCP tools: HTTPX discovery,
|
|
4
|
+
automatic bounded L402 retries, durable spending reservations, and the same
|
|
5
|
+
stderr terminal receipts as `@bitcoinstratigraphy/agent-yield`.
|
|
6
|
+
|
|
7
|
+
Install from PyPI:
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pip install bitcoinstratigraphy-agent-yield
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For local development, install from this checkout:
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
pip install ./packages/python-agent-yield
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Two-line quick starts
|
|
20
|
+
|
|
21
|
+
Start with your existing `agent_config`. Its `fiduciary` policy is the explicit
|
|
22
|
+
wallet authorization shown below. No wallet is created, connected or funded
|
|
23
|
+
by installing or importing this package.
|
|
24
|
+
|
|
25
|
+
### LangChain
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
29
|
+
|
|
30
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "langchain"})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Pass `agent["tools"]` to `langchain.agents.create_agent(model=..., tools=...)`.
|
|
34
|
+
Existing tools are preserved; discovered tools are native `StructuredTool`s
|
|
35
|
+
with sync `.invoke()` and async `.ainvoke()`.
|
|
36
|
+
|
|
37
|
+
### CrewAI
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
41
|
+
|
|
42
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "crewai"})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Pass `agent["tools"]` to `crewai.Agent(role=..., goal=..., backstory=..., tools=...)`.
|
|
46
|
+
They are native CrewAI `BaseTool`s. You can also wrap an existing public-tools
|
|
47
|
+
CrewAI agent with `with_fiduciary_yield(existing_agent, fiduciary=policy)`;
|
|
48
|
+
this returns a copy, never mutating the original agent.
|
|
49
|
+
|
|
50
|
+
### AutoGen
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from bitcoinstratigraphy_agent_yield import with_fiduciary_yield
|
|
54
|
+
|
|
55
|
+
agent = with_fiduciary_yield({**agent_config, "framework": "autogen"})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Pass `agent["tools"]` to `autogen_agentchat.agents.AssistantAgent(..., tools=...)`.
|
|
59
|
+
These are native `autogen_core.tools.BaseTool`s, not OpenAI descriptors requiring
|
|
60
|
+
a separate dispatcher. Cancellation is checked before starting execution.
|
|
61
|
+
|
|
62
|
+
Framework imports are lazy. Install your chosen framework separately; the base
|
|
63
|
+
wheel has **only three direct runtime dependencies**, all exactly pinned:
|
|
64
|
+
HTTPX, bech32 and coincurve. No LangChain, CrewAI, AutoGen, NWC or MCP framework
|
|
65
|
+
is forced on other consumers. Native adapters target the current public
|
|
66
|
+
LangChain Core 1.x, CrewAI 1.x and AutoGen Core 0.7 APIs.
|
|
67
|
+
|
|
68
|
+
## Explicit wallet and spending policy
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
policy = {
|
|
72
|
+
"wallet": authorized_wallet,
|
|
73
|
+
"max_payment_sats": 500,
|
|
74
|
+
"max_total_sats": 5000,
|
|
75
|
+
"fee_reserve_sats": 10,
|
|
76
|
+
"state_directory": ".agent-yield-state",
|
|
77
|
+
}
|
|
78
|
+
agent_config = {"tools": existing_tools, "fiduciary": policy}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`authorized_wallet.pay_invoice(bolt11)` is a **synchronous** wallet adapter
|
|
82
|
+
returning `{"preimage": "<64 hex characters>", "fees_paid": 0}`.
|
|
83
|
+
`fees_paid` is optional integer millisatoshis. Use your wallet provider's
|
|
84
|
+
authorized SDK inside the adapter; secrets never belong in prompts, source,
|
|
85
|
+
HTTP headers, terminal receipts, or tool arguments. This scaffold does **not**
|
|
86
|
+
implement NWC wallet transport.
|
|
87
|
+
|
|
88
|
+
Without a wallet and explicit positive budgets, discovery and free tools work;
|
|
89
|
+
paid execution fails closed. Optional `make_invoice(amount_msats)` returns
|
|
90
|
+
`{"invoice": "<receiving BOLT11>"}` to enable automatic yield withdrawals.
|
|
91
|
+
Wallet adapters must enforce their own operation timeouts and fee ceiling:
|
|
92
|
+
Python cannot cancel an external payment already submitted to a wallet.
|
|
93
|
+
|
|
94
|
+
`agent["fiduciary"]` exposes `.list_tools()`, `.call_tool(name, arguments)`,
|
|
95
|
+
`await .acall_tool(name, arguments)`, `.recover_claim(payment_hash)`, and
|
|
96
|
+
`.close()`. Framework tools are added to a configuration before constructing a
|
|
97
|
+
graph/AssistantAgent; this does not retrofit private internals of running agents.
|
|
98
|
+
Async execution offloads synchronous HTTP/wallet work to a worker thread.
|
|
99
|
+
Cancelling the awaiting task does not reverse an in-progress wallet operation.
|
|
100
|
+
Close the config's client at application shutdown. For public-tools object
|
|
101
|
+
wrapping, supply an application-owned `http_client` and close it yourself.
|
|
102
|
+
|
|
103
|
+
## Connection and payment guarantees
|
|
104
|
+
|
|
105
|
+
- Discovers the live paginated catalog at `https://bitcoinstratigraphy.com/mcp`.
|
|
106
|
+
Handles MCP `2026-07-28`, JSON and SSE, bounded response sizes, timeout and
|
|
107
|
+
one bounded Hashcash admission retry. Discovery never pays a wallet.
|
|
108
|
+
- Validates mainnet BOLT11 checksum, recoverable signature, explicit payee,
|
|
109
|
+
amount, expiry and payment hash against the HTTP JSON quote before spending.
|
|
110
|
+
- Reserves invoice principal plus fee allowance in private SQLite storage
|
|
111
|
+
**before** contacting the wallet. Per-invoice and cumulative limits are
|
|
112
|
+
integer satoshis. Reservations survive threads, processes and restarts.
|
|
113
|
+
Reported excessive/invalid fees block future new payments.
|
|
114
|
+
- Verifies every preimage against its invoice hash, then retries the identical
|
|
115
|
+
MCP body/ID exactly once with `Authorization: L402 macaroon:preimage`.
|
|
116
|
+
Credentials are bound to the exact invoice/request, not globally reused
|
|
117
|
+
across unrelated tools. A repeated 402 never triggers a second payment.
|
|
118
|
+
- Never follows redirects or replaces caller-supplied payment credentials.
|
|
119
|
+
Supply `identity_headers(url, method, body_bytes)` for fresh NIP-98 headers;
|
|
120
|
+
it runs again for admission and paid retries. Valid Genesis Grant calls incur
|
|
121
|
+
no payment, paid-history credit, yield allocation or earned receipt.
|
|
122
|
+
- Unknown payments and invalid returned proofs stay reserved. **Do not delete
|
|
123
|
+
the state directory or change directories to retry a payment.** Operators
|
|
124
|
+
must establish the payment outcome before recovery. Use private persistent
|
|
125
|
+
storage, exclude it from Git, and use one directory per wallet and policy.
|
|
126
|
+
|
|
127
|
+
The SQLite ledger contains sensitive payment proofs, though never wallet keys.
|
|
128
|
+
The folder is created with mode 0700 and the database with 0600. Protect and
|
|
129
|
+
back up this directory. Existing directories must already be private.
|
|
130
|
+
This is client-side accounting, not a replacement for server-side settlement
|
|
131
|
+
ledgers or a wallet-enforced spending limit.
|
|
132
|
+
|
|
133
|
+
## Terminal receipts
|
|
134
|
+
|
|
135
|
+
On a successful paid execution with a receiving wallet:
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
[Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Allocated: 21 Sats (settlement pending).
|
|
139
|
+
[Bitcoin Stratigraphy] ⚡ Thermodynamic Yield Earned: 21 Sats routed to agent wallet.
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Allocated is yellow; earned is green. An earned receipt appears **only** after
|
|
143
|
+
the claim API confirms `SETTLED` **and** `payment_sent: true`, never on
|
|
144
|
+
allocation, free grants, pending claims, failed tools or failed payouts.
|
|
145
|
+
Millisatoshi precision is preserved. Logging goes exclusively to `sys.stderr`
|
|
146
|
+
to preserve MCP JSON-RPC stdout. `QUIET_STRATIGRAPHY=true` disables receipts at
|
|
147
|
+
event time; logging failures never change successful tool results.
|
|
148
|
+
|
|
149
|
+
Claims are best effort, not guaranteed income. `on_error({"code": ...})`
|
|
150
|
+
reports recovery needs without exposing wallet/server error text.
|
|
151
|
+
`on_yield_claim({"status": ..., "amount_msats": ...})` observes claim status.
|
|
152
|
+
`.recover_claim(payment_hash)` replays only a persisted withdrawal request and
|
|
153
|
+
never buys a new tool invoice. Recovery after a crash during receiving-invoice
|
|
154
|
+
creation needs operator intervention; it does not create a second invoice.
|
|
155
|
+
Claims run synchronously after paid execution, bounded by the HTTP timeout.
|
|
156
|
+
|
|
157
|
+
## Development / packaging (no publication)
|
|
158
|
+
|
|
159
|
+
From the repository root, using its Python environment:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
uv run --no-sync python -m pytest packages/python-agent-yield
|
|
163
|
+
cd packages/python-agent-yield
|
|
164
|
+
uv run --no-sync python -m hatchling build -d ../../generated-artifacts/python-agent-yield
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Or install `.[test]` inside this package, then run `python -m pytest`.
|
|
168
|
+
The dedicated tests use signed synthetic invoices, `httpx.MockTransport`,
|
|
169
|
+
temporary state, and fake wallets. They do not contact mainnet or move funds.
|
|
170
|
+
Native LangChain/AutoGen integration tests run when those frameworks are
|
|
171
|
+
installed; the CrewAI adapter has a public-API contract fixture without pulling
|
|
172
|
+
CrewAI's full dependency tree into this standalone package.
|
|
173
|
+
Wheel/sdist build output belongs in `generated-artifacts/`, never source control.
|
|
174
|
+
Building never uploads to PyPI; publication is an explicit release step.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling==1.33.0"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "bitcoinstratigraphy-agent-yield"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One-call MCP discovery, budgeted L402 settlement and fiduciary tools for Python agents."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "AGPL-3.0-only OR LicenseRef-Commercial"
|
|
12
|
+
license-files = ["LICENSE.md"]
|
|
13
|
+
dependencies = ["httpx==0.28.1", "bech32==1.2.0", "coincurve==21.0.0"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.optional-dependencies]
|
|
23
|
+
test = ["pytest==9.2.0"]
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://bitcoinstratigraphy.com"
|
|
27
|
+
Repository = "https://github.com/CharlesStrogish/bitcoin-stratigraphy-mcp"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.wheel]
|
|
30
|
+
packages = ["src/bitcoinstratigraphy_agent_yield"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.sdist]
|
|
33
|
+
include = ["/src", "/tests", "/README.md", "/LICENSE.md", "/pyproject.toml"]
|
|
34
|
+
|
|
35
|
+
[tool.pytest.ini_options]
|
|
36
|
+
testpaths = ["tests"]
|
|
37
|
+
pythonpath = ["src"]
|
|
38
|
+
addopts = "-ra"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Frictionless, explicitly budgeted Bitcoin Stratigraphy tools for Python agents."""
|
|
2
|
+
|
|
3
|
+
from .adapters import AgentTool, with_fiduciary_yield
|
|
4
|
+
from .client import DEFAULT_MCP_URL, FiduciaryClient
|
|
5
|
+
from .errors import FiduciaryError
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AgentTool",
|
|
9
|
+
"DEFAULT_MCP_URL",
|
|
10
|
+
"FiduciaryClient",
|
|
11
|
+
"FiduciaryError",
|
|
12
|
+
"with_fiduciary_yield",
|
|
13
|
+
]
|
|
14
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Lazy, native framework adapters. No framework dependencies in the base wheel."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import re
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .client import FiduciaryClient
|
|
10
|
+
from .errors import FiduciaryError
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class AgentTool:
|
|
15
|
+
name: str
|
|
16
|
+
canonical_name: str
|
|
17
|
+
description: str
|
|
18
|
+
input_schema: dict
|
|
19
|
+
client: FiduciaryClient
|
|
20
|
+
|
|
21
|
+
def invoke(self, arguments=None, **kwargs):
|
|
22
|
+
return self.client.call_tool(
|
|
23
|
+
self.canonical_name, arguments if arguments is not None else kwargs
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
async def ainvoke(self, arguments=None, **kwargs):
|
|
27
|
+
return await self.client.acall_tool(
|
|
28
|
+
self.canonical_name, arguments if arguments is not None else kwargs
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _argument_model(tool):
|
|
33
|
+
from pydantic import ConfigDict, create_model
|
|
34
|
+
|
|
35
|
+
# Forward the original JSON Schema to the model provider without flattening
|
|
36
|
+
# enums, nested objects, unions, or refs. MCP remains the authoritative
|
|
37
|
+
# validator; framework-side parsing must not coerce or drop arguments.
|
|
38
|
+
schema = copy.deepcopy(tool.input_schema)
|
|
39
|
+
required = schema.get("required", [])
|
|
40
|
+
fields = {
|
|
41
|
+
name: (Any, ... if name in required else None)
|
|
42
|
+
for name in schema.get("properties", {})
|
|
43
|
+
}
|
|
44
|
+
model = create_model(
|
|
45
|
+
f"{tool.name}_Args", __config__=ConfigDict(extra="allow"), **fields
|
|
46
|
+
)
|
|
47
|
+
model.model_json_schema = classmethod(
|
|
48
|
+
lambda cls, *args, **kwargs: copy.deepcopy(schema)
|
|
49
|
+
)
|
|
50
|
+
return model
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _langchain(tool):
|
|
54
|
+
from langchain_core.tools import StructuredTool
|
|
55
|
+
|
|
56
|
+
return StructuredTool.from_function(
|
|
57
|
+
func=lambda **kwargs: tool.invoke(kwargs),
|
|
58
|
+
coroutine=lambda **kwargs: tool.ainvoke(kwargs),
|
|
59
|
+
name=tool.name,
|
|
60
|
+
description=tool.description,
|
|
61
|
+
args_schema=copy.deepcopy(tool.input_schema),
|
|
62
|
+
infer_schema=False,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _crewai(tool):
|
|
67
|
+
from crewai.tools import BaseTool
|
|
68
|
+
|
|
69
|
+
class FiduciaryCrewTool(BaseTool):
|
|
70
|
+
def _run(self, **kwargs):
|
|
71
|
+
return tool.invoke(kwargs)
|
|
72
|
+
|
|
73
|
+
async def _arun(self, **kwargs):
|
|
74
|
+
return await tool.ainvoke(kwargs)
|
|
75
|
+
|
|
76
|
+
return FiduciaryCrewTool(
|
|
77
|
+
name=tool.name, description=tool.description, args_schema=_argument_model(tool)
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _autogen(tool):
|
|
82
|
+
from autogen_core.tools import BaseTool
|
|
83
|
+
|
|
84
|
+
class FiduciaryAutoGenTool(BaseTool):
|
|
85
|
+
def __init__(self):
|
|
86
|
+
super().__init__(_argument_model(tool), object, tool.name, tool.description)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def schema(self):
|
|
90
|
+
return {
|
|
91
|
+
"name": tool.name,
|
|
92
|
+
"description": tool.description,
|
|
93
|
+
"parameters": copy.deepcopy(tool.input_schema),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async def run(self, args, cancellation_token):
|
|
97
|
+
if cancellation_token.is_cancelled():
|
|
98
|
+
raise FiduciaryError("MCP_REQUEST_CANCELLED")
|
|
99
|
+
return await tool.ainvoke(args.model_dump(exclude_unset=True))
|
|
100
|
+
|
|
101
|
+
return FiduciaryAutoGenTool()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def with_fiduciary_yield(agent, *, fiduciary=None, framework=None):
|
|
105
|
+
"""Copy an agent config or a public-tools agent, discover, and append tools.
|
|
106
|
+
|
|
107
|
+
Configs return a dict with a ``fiduciary`` client for lifetime management.
|
|
108
|
+
Public-tools/Pydantic agents return a model_copy; their original is untouched.
|
|
109
|
+
Already-built graph/private-internals agents must be wrapped at config time.
|
|
110
|
+
"""
|
|
111
|
+
is_config = isinstance(agent, Mapping)
|
|
112
|
+
if is_config:
|
|
113
|
+
options = fiduciary if fiduciary is not None else agent.get("fiduciary", {})
|
|
114
|
+
selected = framework or agent.get("framework", "custom")
|
|
115
|
+
existing = list(agent.get("tools", []))
|
|
116
|
+
else:
|
|
117
|
+
if not callable(getattr(agent, "model_copy", None)) or not hasattr(
|
|
118
|
+
agent, "tools"
|
|
119
|
+
):
|
|
120
|
+
raise FiduciaryError("WRAP_AGENT_CONFIGURATION_NOT_PRIVATE_INTERNALS")
|
|
121
|
+
if fiduciary is None:
|
|
122
|
+
raise FiduciaryError("FIDUCIARY_CONFIGURATION_REQUIRED")
|
|
123
|
+
options = fiduciary
|
|
124
|
+
selected = framework or (
|
|
125
|
+
"crewai" if type(agent).__module__.startswith("crewai") else "custom"
|
|
126
|
+
)
|
|
127
|
+
existing = list(agent.tools or [])
|
|
128
|
+
if selected not in ("custom", "langchain", "crewai", "autogen"):
|
|
129
|
+
raise FiduciaryError("UNSUPPORTED_FRAMEWORK")
|
|
130
|
+
if not isinstance(options, Mapping):
|
|
131
|
+
raise FiduciaryError("FIDUCIARY_CONFIGURATION_REQUIRED")
|
|
132
|
+
client = FiduciaryClient(**options)
|
|
133
|
+
try:
|
|
134
|
+
tools = []
|
|
135
|
+
for index, entry in enumerate(client.list_tools()):
|
|
136
|
+
safe = f"fiduciary_{index}_{re.sub(r'[^a-zA-Z0-9_-]', '_', entry['name'])}"[
|
|
137
|
+
:64
|
|
138
|
+
]
|
|
139
|
+
if any(
|
|
140
|
+
(
|
|
141
|
+
item.get("name")
|
|
142
|
+
if isinstance(item, dict)
|
|
143
|
+
else getattr(item, "name", None)
|
|
144
|
+
)
|
|
145
|
+
== safe
|
|
146
|
+
for item in existing
|
|
147
|
+
):
|
|
148
|
+
raise FiduciaryError("MCP_TOOL_NAME_COLLISION")
|
|
149
|
+
tool = AgentTool(
|
|
150
|
+
safe,
|
|
151
|
+
entry["name"],
|
|
152
|
+
entry.get("description") or entry["name"],
|
|
153
|
+
copy.deepcopy(entry["inputSchema"]),
|
|
154
|
+
client,
|
|
155
|
+
)
|
|
156
|
+
adapter = {
|
|
157
|
+
"langchain": _langchain,
|
|
158
|
+
"crewai": _crewai,
|
|
159
|
+
"autogen": _autogen,
|
|
160
|
+
}.get(selected)
|
|
161
|
+
tools.append(adapter(tool) if adapter else tool)
|
|
162
|
+
if is_config:
|
|
163
|
+
return {**agent, "tools": existing + tools, "fiduciary": client}
|
|
164
|
+
# CrewAI agents expose public tools but do not accept arbitrary client
|
|
165
|
+
# fields. Tool closures retain the client; no global monkeypatching.
|
|
166
|
+
return agent.model_copy(update={"tools": existing + tools})
|
|
167
|
+
except ImportError:
|
|
168
|
+
client.close()
|
|
169
|
+
raise FiduciaryError(f"INSTALL_{selected.upper()}_FOR_NATIVE_ADAPTER") from None
|
|
170
|
+
except BaseException:
|
|
171
|
+
client.close()
|
|
172
|
+
raise
|