openclaw-algorand-plugin 0.5.0
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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/index.ts +361 -0
- package/lib/mcp-servers.ts +14 -0
- package/lib/x402-fetch.ts +213 -0
- package/memory/algorand-plugin.md +82 -0
- package/openclaw.plugin.json +30 -0
- package/package.json +38 -0
- package/setup.ts +80 -0
- package/skills/algorand-development/SKILL.md +90 -0
- package/skills/algorand-development/references/build-smart-contracts-reference.md +79 -0
- package/skills/algorand-development/references/build-smart-contracts.md +52 -0
- package/skills/algorand-development/references/create-project-reference.md +86 -0
- package/skills/algorand-development/references/create-project.md +89 -0
- package/skills/algorand-development/references/implement-arc-standards-arc32-arc56.md +396 -0
- package/skills/algorand-development/references/implement-arc-standards-arc4.md +265 -0
- package/skills/algorand-development/references/implement-arc-standards.md +92 -0
- package/skills/algorand-development/references/search-algorand-examples-reference.md +119 -0
- package/skills/algorand-development/references/search-algorand-examples.md +89 -0
- package/skills/algorand-development/references/troubleshoot-errors-contract.md +373 -0
- package/skills/algorand-development/references/troubleshoot-errors-transaction.md +599 -0
- package/skills/algorand-development/references/troubleshoot-errors.md +105 -0
- package/skills/algorand-development/references/use-algokit-cli-reference.md +228 -0
- package/skills/algorand-development/references/use-algokit-cli.md +64 -0
- package/skills/algorand-interaction/SKILL.md +223 -0
- package/skills/algorand-interaction/references/algorand-mcp.md +743 -0
- package/skills/algorand-interaction/references/examples-algorand-mcp.md +647 -0
- package/skills/algorand-python/SKILL.md +95 -0
- package/skills/algorand-python/references/build-smart-contracts-decorators.md +413 -0
- package/skills/algorand-python/references/build-smart-contracts-reference.md +55 -0
- package/skills/algorand-python/references/build-smart-contracts-storage.md +452 -0
- package/skills/algorand-python/references/build-smart-contracts-transactions.md +445 -0
- package/skills/algorand-python/references/build-smart-contracts-types.md +438 -0
- package/skills/algorand-python/references/build-smart-contracts.md +82 -0
- package/skills/algorand-python/references/create-project-reference.md +55 -0
- package/skills/algorand-python/references/create-project.md +75 -0
- package/skills/algorand-python/references/implement-arc-standards-arc32-arc56.md +101 -0
- package/skills/algorand-python/references/implement-arc-standards-arc4.md +154 -0
- package/skills/algorand-python/references/implement-arc-standards.md +39 -0
- package/skills/algorand-python/references/troubleshoot-errors-contract.md +355 -0
- package/skills/algorand-python/references/troubleshoot-errors-transaction.md +430 -0
- package/skills/algorand-python/references/troubleshoot-errors.md +46 -0
- package/skills/algorand-python/references/use-algokit-utils-reference.md +350 -0
- package/skills/algorand-python/references/use-algokit-utils.md +76 -0
- package/skills/algorand-typescript/SKILL.md +131 -0
- package/skills/algorand-typescript/references/algorand-ts-migration-from-beta.md +448 -0
- package/skills/algorand-typescript/references/algorand-ts-migration-from-tealscript.md +487 -0
- package/skills/algorand-typescript/references/algorand-ts-migration.md +102 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax-methods-and-abi.md +134 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax-reference.md +58 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax-storage.md +154 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax-transactions.md +187 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax-types-and-values.md +150 -0
- package/skills/algorand-typescript/references/algorand-typescript-syntax.md +84 -0
- package/skills/algorand-typescript/references/build-smart-contracts-reference.md +52 -0
- package/skills/algorand-typescript/references/build-smart-contracts.md +74 -0
- package/skills/algorand-typescript/references/call-smart-contracts-reference.md +237 -0
- package/skills/algorand-typescript/references/call-smart-contracts.md +183 -0
- package/skills/algorand-typescript/references/create-project-reference.md +53 -0
- package/skills/algorand-typescript/references/create-project.md +86 -0
- package/skills/algorand-typescript/references/deploy-react-frontend-examples.md +527 -0
- package/skills/algorand-typescript/references/deploy-react-frontend-reference.md +412 -0
- package/skills/algorand-typescript/references/deploy-react-frontend.md +239 -0
- package/skills/algorand-typescript/references/implement-arc-standards-arc32-arc56.md +73 -0
- package/skills/algorand-typescript/references/implement-arc-standards-arc4.md +126 -0
- package/skills/algorand-typescript/references/implement-arc-standards.md +44 -0
- package/skills/algorand-typescript/references/test-smart-contracts-examples.md +245 -0
- package/skills/algorand-typescript/references/test-smart-contracts-unit-tests.md +147 -0
- package/skills/algorand-typescript/references/test-smart-contracts.md +127 -0
- package/skills/algorand-typescript/references/troubleshoot-errors-contract.md +296 -0
- package/skills/algorand-typescript/references/troubleshoot-errors-transaction.md +438 -0
- package/skills/algorand-typescript/references/troubleshoot-errors.md +56 -0
- package/skills/algorand-typescript/references/use-algokit-utils-reference.md +342 -0
- package/skills/algorand-typescript/references/use-algokit-utils.md +74 -0
- package/skills/algorand-x402-python/SKILL.md +113 -0
- package/skills/algorand-x402-python/references/create-python-x402-client-examples.md +469 -0
- package/skills/algorand-x402-python/references/create-python-x402-client-reference.md +313 -0
- package/skills/algorand-x402-python/references/create-python-x402-client.md +207 -0
- package/skills/algorand-x402-python/references/create-python-x402-facilitator-examples.md +924 -0
- package/skills/algorand-x402-python/references/create-python-x402-facilitator-reference.md +629 -0
- package/skills/algorand-x402-python/references/create-python-x402-facilitator.md +408 -0
- package/skills/algorand-x402-python/references/create-python-x402-server-examples.md +703 -0
- package/skills/algorand-x402-python/references/create-python-x402-server-reference.md +303 -0
- package/skills/algorand-x402-python/references/create-python-x402-server.md +221 -0
- package/skills/algorand-x402-python/references/explain-algorand-x402-python-examples.md +605 -0
- package/skills/algorand-x402-python/references/explain-algorand-x402-python-reference.md +315 -0
- package/skills/algorand-x402-python/references/explain-algorand-x402-python.md +167 -0
- package/skills/algorand-x402-python/references/use-python-x402-core-avm-examples.md +554 -0
- package/skills/algorand-x402-python/references/use-python-x402-core-avm-reference.md +278 -0
- package/skills/algorand-x402-python/references/use-python-x402-core-avm.md +166 -0
- package/skills/algorand-x402-typescript/SKILL.md +129 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-client-examples.md +879 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-client-reference.md +371 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-client.md +236 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-facilitator-examples.md +875 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-facilitator-reference.md +461 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-facilitator.md +270 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-nextjs-examples.md +1181 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-nextjs-reference.md +360 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-nextjs.md +251 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-paywall-examples.md +870 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-paywall-reference.md +323 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-paywall.md +281 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-server-examples.md +1135 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-server-reference.md +382 -0
- package/skills/algorand-x402-typescript/references/create-typescript-x402-server.md +216 -0
- package/skills/algorand-x402-typescript/references/explain-algorand-x402-typescript-examples.md +616 -0
- package/skills/algorand-x402-typescript/references/explain-algorand-x402-typescript-reference.md +323 -0
- package/skills/algorand-x402-typescript/references/explain-algorand-x402-typescript.md +232 -0
- package/skills/algorand-x402-typescript/references/use-typescript-x402-core-avm-examples.md +1417 -0
- package/skills/algorand-x402-typescript/references/use-typescript-x402-core-avm-reference.md +504 -0
- package/skills/algorand-x402-typescript/references/use-typescript-x402-core-avm.md +158 -0
|
@@ -0,0 +1,924 @@
|
|
|
1
|
+
# Python x402 Facilitator and Bazaar Discovery Examples
|
|
2
|
+
|
|
3
|
+
## FacilitatorAvmSigner Protocol
|
|
4
|
+
|
|
5
|
+
```python
|
|
6
|
+
from x402.mechanisms.avm.signer import FacilitatorAvmSigner
|
|
7
|
+
|
|
8
|
+
# Protocol definition:
|
|
9
|
+
class FacilitatorAvmSigner(Protocol):
|
|
10
|
+
def get_addresses(self) -> list[str]:
|
|
11
|
+
"""Get all managed fee payer addresses."""
|
|
12
|
+
...
|
|
13
|
+
|
|
14
|
+
def sign_transaction(
|
|
15
|
+
self, txn_bytes: bytes, fee_payer: str, network: str,
|
|
16
|
+
) -> bytes:
|
|
17
|
+
"""Sign a single transaction with the fee payer's key."""
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
def sign_group(
|
|
21
|
+
self,
|
|
22
|
+
group_bytes: list[bytes],
|
|
23
|
+
fee_payer: str,
|
|
24
|
+
indexes_to_sign: list[int],
|
|
25
|
+
network: str,
|
|
26
|
+
) -> list[bytes]:
|
|
27
|
+
"""Sign specified transactions in a group."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def simulate_group(
|
|
31
|
+
self, group_bytes: list[bytes], network: str,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Simulate a transaction group (raises on failure)."""
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
def send_group(
|
|
37
|
+
self, group_bytes: list[bytes], network: str,
|
|
38
|
+
) -> str:
|
|
39
|
+
"""Send a transaction group, returns txid."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
def confirm_transaction(
|
|
43
|
+
self, txid: str, network: str, rounds: int = 4,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""Wait for transaction confirmation."""
|
|
46
|
+
...
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## AlgorandFacilitatorSigner Implementation
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import base64
|
|
55
|
+
from x402.mechanisms.avm.signer import FacilitatorAvmSigner
|
|
56
|
+
from x402.mechanisms.avm.constants import (
|
|
57
|
+
ALGORAND_TESTNET_CAIP2,
|
|
58
|
+
ALGORAND_MAINNET_CAIP2,
|
|
59
|
+
NETWORK_CONFIGS,
|
|
60
|
+
)
|
|
61
|
+
from algosdk import encoding, transaction
|
|
62
|
+
from algosdk.v2client import algod
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class AlgorandFacilitatorSigner:
|
|
66
|
+
"""
|
|
67
|
+
Production FacilitatorAvmSigner implementation.
|
|
68
|
+
|
|
69
|
+
Key encoding notes (algosdk v2.11.1):
|
|
70
|
+
- msgpack_decode(s) expects base64 string, NOT raw bytes
|
|
71
|
+
- msgpack_encode(obj) returns base64 string, NOT raw bytes
|
|
72
|
+
- Transaction.sign(pk) expects base64 string private key
|
|
73
|
+
- SDK protocol passes raw msgpack bytes between methods
|
|
74
|
+
- Boundary: msgpack_decode(base64.b64encode(raw).decode()) for decode
|
|
75
|
+
- Boundary: base64.b64decode(msgpack_encode(obj)) for encode
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
def __init__(self, private_key_b64: str, algod_url: str = "", algod_token: str = ""):
|
|
79
|
+
self._secret_key = base64.b64decode(private_key_b64)
|
|
80
|
+
self._address = encoding.encode_address(self._secret_key[32:])
|
|
81
|
+
self._signing_key = base64.b64encode(self._secret_key).decode()
|
|
82
|
+
|
|
83
|
+
# Create algod clients per network
|
|
84
|
+
self._clients: dict[str, algod.AlgodClient] = {}
|
|
85
|
+
if algod_url:
|
|
86
|
+
self._default_client = algod.AlgodClient(algod_token, algod_url)
|
|
87
|
+
else:
|
|
88
|
+
self._default_client = None
|
|
89
|
+
|
|
90
|
+
def _get_client(self, network: str) -> algod.AlgodClient:
|
|
91
|
+
if network not in self._clients:
|
|
92
|
+
if self._default_client:
|
|
93
|
+
self._clients[network] = self._default_client
|
|
94
|
+
else:
|
|
95
|
+
config = NETWORK_CONFIGS.get(network, {})
|
|
96
|
+
url = config.get("algod_url", "https://testnet-api.algonode.cloud")
|
|
97
|
+
self._clients[network] = algod.AlgodClient("", url)
|
|
98
|
+
return self._clients[network]
|
|
99
|
+
|
|
100
|
+
def get_addresses(self) -> list[str]:
|
|
101
|
+
return [self._address]
|
|
102
|
+
|
|
103
|
+
def sign_transaction(
|
|
104
|
+
self, txn_bytes: bytes, fee_payer: str, network: str,
|
|
105
|
+
) -> bytes:
|
|
106
|
+
"""Sign a single transaction."""
|
|
107
|
+
b64_txn = base64.b64encode(txn_bytes).decode("utf-8")
|
|
108
|
+
txn_obj = encoding.msgpack_decode(b64_txn)
|
|
109
|
+
signed = txn_obj.sign(self._signing_key)
|
|
110
|
+
return base64.b64decode(encoding.msgpack_encode(signed))
|
|
111
|
+
|
|
112
|
+
def sign_group(
|
|
113
|
+
self,
|
|
114
|
+
group_bytes: list[bytes],
|
|
115
|
+
fee_payer: str,
|
|
116
|
+
indexes_to_sign: list[int],
|
|
117
|
+
network: str,
|
|
118
|
+
) -> list[bytes]:
|
|
119
|
+
"""Sign specified transactions in a group."""
|
|
120
|
+
result = list(group_bytes)
|
|
121
|
+
for i in indexes_to_sign:
|
|
122
|
+
result[i] = self.sign_transaction(group_bytes[i], fee_payer, network)
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def simulate_group(self, group_bytes: list[bytes], network: str) -> None:
|
|
126
|
+
"""Simulate a transaction group.
|
|
127
|
+
|
|
128
|
+
Key pattern: wrap unsigned transactions with SignedTransaction(txn, None)
|
|
129
|
+
and use allow_empty_signatures=True.
|
|
130
|
+
"""
|
|
131
|
+
client = self._get_client(network)
|
|
132
|
+
stxns = []
|
|
133
|
+
for txn_bytes in group_bytes:
|
|
134
|
+
b64 = base64.b64encode(txn_bytes).decode("utf-8")
|
|
135
|
+
obj = encoding.msgpack_decode(b64)
|
|
136
|
+
if isinstance(obj, transaction.SignedTransaction):
|
|
137
|
+
stxns.append(obj)
|
|
138
|
+
elif isinstance(obj, transaction.Transaction):
|
|
139
|
+
stxns.append(transaction.SignedTransaction(obj, None))
|
|
140
|
+
else:
|
|
141
|
+
stxns.append(obj)
|
|
142
|
+
|
|
143
|
+
request = transaction.SimulateRequest(
|
|
144
|
+
txn_groups=[
|
|
145
|
+
transaction.SimulateRequestTransactionGroup(txns=stxns)
|
|
146
|
+
],
|
|
147
|
+
allow_empty_signatures=True,
|
|
148
|
+
)
|
|
149
|
+
result = client.simulate_raw_transactions(request)
|
|
150
|
+
|
|
151
|
+
for group in result.get("txn-groups", []):
|
|
152
|
+
if group.get("failure-message"):
|
|
153
|
+
raise Exception(
|
|
154
|
+
f"Simulation failed: {group['failure-message']}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def send_group(self, group_bytes: list[bytes], network: str) -> str:
|
|
158
|
+
"""Send a transaction group.
|
|
159
|
+
|
|
160
|
+
Key pattern: use send_raw_transaction(base64.b64encode(b''.join(group_bytes)))
|
|
161
|
+
to avoid decode/re-encode overhead.
|
|
162
|
+
"""
|
|
163
|
+
client = self._get_client(network)
|
|
164
|
+
raw = base64.b64encode(b"".join(group_bytes))
|
|
165
|
+
return client.send_raw_transaction(raw)
|
|
166
|
+
|
|
167
|
+
def confirm_transaction(
|
|
168
|
+
self, txid: str, network: str, rounds: int = 4,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""Wait for transaction confirmation."""
|
|
171
|
+
client = self._get_client(network)
|
|
172
|
+
transaction.wait_for_confirmation(client, txid, rounds)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# Usage:
|
|
176
|
+
import os
|
|
177
|
+
|
|
178
|
+
signer = AlgorandFacilitatorSigner(
|
|
179
|
+
private_key_b64=os.environ["AVM_PRIVATE_KEY"],
|
|
180
|
+
algod_url="https://testnet-api.algonode.cloud",
|
|
181
|
+
)
|
|
182
|
+
print(f"Fee payer addresses: {signer.get_addresses()}")
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Facilitator Registration
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from x402 import x402Facilitator
|
|
191
|
+
from x402.mechanisms.avm.exact import register_exact_avm_facilitator
|
|
192
|
+
from x402.mechanisms.avm import ALGORAND_TESTNET_CAIP2, ALGORAND_MAINNET_CAIP2
|
|
193
|
+
|
|
194
|
+
facilitator = x402Facilitator()
|
|
195
|
+
|
|
196
|
+
# Single network
|
|
197
|
+
register_exact_avm_facilitator(
|
|
198
|
+
facilitator, signer, networks=[ALGORAND_TESTNET_CAIP2]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# Multiple networks
|
|
202
|
+
register_exact_avm_facilitator(
|
|
203
|
+
facilitator, signer, networks=[ALGORAND_TESTNET_CAIP2, ALGORAND_MAINNET_CAIP2]
|
|
204
|
+
)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## Complete FastAPI Facilitator Service
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# facilitator_service.py
|
|
213
|
+
import os
|
|
214
|
+
import base64
|
|
215
|
+
from fastapi import FastAPI, Request
|
|
216
|
+
from fastapi.responses import JSONResponse
|
|
217
|
+
from x402 import x402Facilitator
|
|
218
|
+
from x402.mechanisms.avm.exact import register_exact_avm_facilitator
|
|
219
|
+
from x402.mechanisms.avm import ALGORAND_TESTNET_CAIP2, ALGORAND_MAINNET_CAIP2
|
|
220
|
+
from x402.mechanisms.avm.constants import NETWORK_CONFIGS
|
|
221
|
+
from algosdk import encoding, transaction
|
|
222
|
+
from algosdk.v2client import algod
|
|
223
|
+
|
|
224
|
+
app = FastAPI(title="x402-avm Facilitator Service")
|
|
225
|
+
|
|
226
|
+
# Build signer
|
|
227
|
+
SECRET_KEY = base64.b64decode(os.environ["AVM_PRIVATE_KEY"])
|
|
228
|
+
ADDRESS = encoding.encode_address(SECRET_KEY[32:])
|
|
229
|
+
SIGNING_KEY = base64.b64encode(SECRET_KEY).decode()
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class FacilitatorSigner:
|
|
233
|
+
def __init__(self):
|
|
234
|
+
self._clients: dict[str, algod.AlgodClient] = {}
|
|
235
|
+
|
|
236
|
+
def _client(self, network: str) -> algod.AlgodClient:
|
|
237
|
+
if network not in self._clients:
|
|
238
|
+
config = NETWORK_CONFIGS.get(network, {})
|
|
239
|
+
url = config.get("algod_url", "https://testnet-api.algonode.cloud")
|
|
240
|
+
self._clients[network] = algod.AlgodClient("", url)
|
|
241
|
+
return self._clients[network]
|
|
242
|
+
|
|
243
|
+
def get_addresses(self) -> list[str]:
|
|
244
|
+
return [ADDRESS]
|
|
245
|
+
|
|
246
|
+
def sign_transaction(self, txn_bytes: bytes, fee_payer: str, network: str) -> bytes:
|
|
247
|
+
b64 = base64.b64encode(txn_bytes).decode()
|
|
248
|
+
txn_obj = encoding.msgpack_decode(b64)
|
|
249
|
+
signed = txn_obj.sign(SIGNING_KEY)
|
|
250
|
+
return base64.b64decode(encoding.msgpack_encode(signed))
|
|
251
|
+
|
|
252
|
+
def sign_group(self, group_bytes, fee_payer, indexes, network):
|
|
253
|
+
result = list(group_bytes)
|
|
254
|
+
for i in indexes:
|
|
255
|
+
result[i] = self.sign_transaction(group_bytes[i], fee_payer, network)
|
|
256
|
+
return result
|
|
257
|
+
|
|
258
|
+
def simulate_group(self, group_bytes, network):
|
|
259
|
+
client = self._client(network)
|
|
260
|
+
stxns = []
|
|
261
|
+
for txn_bytes in group_bytes:
|
|
262
|
+
b64 = base64.b64encode(txn_bytes).decode()
|
|
263
|
+
obj = encoding.msgpack_decode(b64)
|
|
264
|
+
if isinstance(obj, transaction.SignedTransaction):
|
|
265
|
+
stxns.append(obj)
|
|
266
|
+
else:
|
|
267
|
+
stxns.append(transaction.SignedTransaction(obj, None))
|
|
268
|
+
req = transaction.SimulateRequest(
|
|
269
|
+
txn_groups=[transaction.SimulateRequestTransactionGroup(txns=stxns)],
|
|
270
|
+
allow_empty_signatures=True,
|
|
271
|
+
)
|
|
272
|
+
result = client.simulate_raw_transactions(req)
|
|
273
|
+
for group in result.get("txn-groups", []):
|
|
274
|
+
if group.get("failure-message"):
|
|
275
|
+
raise Exception(f"Simulation failed: {group['failure-message']}")
|
|
276
|
+
|
|
277
|
+
def send_group(self, group_bytes, network):
|
|
278
|
+
client = self._client(network)
|
|
279
|
+
return client.send_raw_transaction(
|
|
280
|
+
base64.b64encode(b"".join(group_bytes))
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
def confirm_transaction(self, txid, network, rounds=4):
|
|
284
|
+
client = self._client(network)
|
|
285
|
+
transaction.wait_for_confirmation(client, txid, rounds)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# Initialize facilitator
|
|
289
|
+
signer = FacilitatorSigner()
|
|
290
|
+
facilitator = x402Facilitator()
|
|
291
|
+
register_exact_avm_facilitator(
|
|
292
|
+
facilitator,
|
|
293
|
+
signer,
|
|
294
|
+
networks=[ALGORAND_TESTNET_CAIP2, ALGORAND_MAINNET_CAIP2],
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
@app.get("/supported")
|
|
299
|
+
async def supported():
|
|
300
|
+
return facilitator.get_supported_networks()
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
@app.post("/verify")
|
|
304
|
+
async def verify(request: Request):
|
|
305
|
+
body = await request.json()
|
|
306
|
+
try:
|
|
307
|
+
result = await facilitator.verify(
|
|
308
|
+
body["paymentPayload"], body["paymentRequirements"]
|
|
309
|
+
)
|
|
310
|
+
return result
|
|
311
|
+
except Exception as e:
|
|
312
|
+
return JSONResponse(
|
|
313
|
+
status_code=400, content={"error": str(e)}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@app.post("/settle")
|
|
318
|
+
async def settle(request: Request):
|
|
319
|
+
body = await request.json()
|
|
320
|
+
try:
|
|
321
|
+
result = await facilitator.settle(
|
|
322
|
+
body["paymentPayload"], body["paymentRequirements"]
|
|
323
|
+
)
|
|
324
|
+
return result
|
|
325
|
+
except Exception as e:
|
|
326
|
+
return JSONResponse(
|
|
327
|
+
status_code=400, content={"error": str(e)}
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.on_event("startup")
|
|
332
|
+
async def startup():
|
|
333
|
+
print(f"Facilitator service started")
|
|
334
|
+
print(f"Fee payer address: {ADDRESS}")
|
|
335
|
+
print(f"Networks: Testnet + Mainnet")
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
# Run: uvicorn facilitator_service:app --port 4000
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## algosdk Encoding Boundary Patterns
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
import base64
|
|
347
|
+
from algosdk import encoding
|
|
348
|
+
|
|
349
|
+
# Raw bytes -> algosdk object (DECODE)
|
|
350
|
+
raw_bytes: bytes = ...
|
|
351
|
+
b64_string = base64.b64encode(raw_bytes).decode("utf-8")
|
|
352
|
+
txn_obj = encoding.msgpack_decode(b64_string)
|
|
353
|
+
|
|
354
|
+
# algosdk object -> raw bytes (ENCODE)
|
|
355
|
+
b64_string = encoding.msgpack_encode(txn_obj)
|
|
356
|
+
raw_bytes = base64.b64decode(b64_string)
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## Inline FacilitatorSigner (Minimal)
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
import os
|
|
365
|
+
import base64
|
|
366
|
+
from algosdk import encoding, transaction
|
|
367
|
+
from algosdk.v2client import algod
|
|
368
|
+
|
|
369
|
+
SECRET = base64.b64decode(os.environ["AVM_PRIVATE_KEY"])
|
|
370
|
+
ADDR = encoding.encode_address(SECRET[32:])
|
|
371
|
+
KEY = base64.b64encode(SECRET).decode()
|
|
372
|
+
CLIENT = algod.AlgodClient("", "https://testnet-api.algonode.cloud")
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
class MinimalSigner:
|
|
376
|
+
def get_addresses(self):
|
|
377
|
+
return [ADDR]
|
|
378
|
+
|
|
379
|
+
def sign_transaction(self, txn_bytes, fee_payer, network):
|
|
380
|
+
obj = encoding.msgpack_decode(base64.b64encode(txn_bytes).decode())
|
|
381
|
+
return base64.b64decode(encoding.msgpack_encode(obj.sign(KEY)))
|
|
382
|
+
|
|
383
|
+
def sign_group(self, group_bytes, fee_payer, indexes, network):
|
|
384
|
+
r = list(group_bytes)
|
|
385
|
+
for i in indexes:
|
|
386
|
+
r[i] = self.sign_transaction(group_bytes[i], fee_payer, network)
|
|
387
|
+
return r
|
|
388
|
+
|
|
389
|
+
def simulate_group(self, group_bytes, network):
|
|
390
|
+
stxns = []
|
|
391
|
+
for b in group_bytes:
|
|
392
|
+
obj = encoding.msgpack_decode(base64.b64encode(b).decode())
|
|
393
|
+
stxns.append(
|
|
394
|
+
obj if isinstance(obj, transaction.SignedTransaction)
|
|
395
|
+
else transaction.SignedTransaction(obj, None)
|
|
396
|
+
)
|
|
397
|
+
req = transaction.SimulateRequest(
|
|
398
|
+
txn_groups=[transaction.SimulateRequestTransactionGroup(txns=stxns)],
|
|
399
|
+
allow_empty_signatures=True,
|
|
400
|
+
)
|
|
401
|
+
res = CLIENT.simulate_raw_transactions(req)
|
|
402
|
+
for g in res.get("txn-groups", []):
|
|
403
|
+
if g.get("failure-message"):
|
|
404
|
+
raise Exception(g["failure-message"])
|
|
405
|
+
|
|
406
|
+
def send_group(self, group_bytes, network):
|
|
407
|
+
return CLIENT.send_raw_transaction(base64.b64encode(b"".join(group_bytes)))
|
|
408
|
+
|
|
409
|
+
def confirm_transaction(self, txid, network, rounds=4):
|
|
410
|
+
transaction.wait_for_confirmation(CLIENT, txid, rounds)
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
---
|
|
414
|
+
|
|
415
|
+
## Declaring Discovery Extension (GET with Query Parameters)
|
|
416
|
+
|
|
417
|
+
```python
|
|
418
|
+
from x402.extensions.bazaar import declare_discovery_extension, OutputConfig
|
|
419
|
+
|
|
420
|
+
discovery = declare_discovery_extension(
|
|
421
|
+
input={"city": "San Francisco"},
|
|
422
|
+
input_schema={
|
|
423
|
+
"properties": {
|
|
424
|
+
"city": {"type": "string", "description": "City name"},
|
|
425
|
+
},
|
|
426
|
+
"required": ["city"],
|
|
427
|
+
},
|
|
428
|
+
output=OutputConfig(
|
|
429
|
+
example={"weather": "sunny", "temperature": 70},
|
|
430
|
+
schema={
|
|
431
|
+
"properties": {
|
|
432
|
+
"weather": {"type": "string"},
|
|
433
|
+
"temperature": {"type": "number"},
|
|
434
|
+
},
|
|
435
|
+
"required": ["weather", "temperature"],
|
|
436
|
+
},
|
|
437
|
+
),
|
|
438
|
+
)
|
|
439
|
+
# discovery is: {"bazaar": {"info": {...}, "schema": {...}}}
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Declaring Discovery Extension (POST with JSON Body)
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
from x402.extensions.bazaar import declare_discovery_extension, OutputConfig
|
|
448
|
+
|
|
449
|
+
discovery = declare_discovery_extension(
|
|
450
|
+
input={"prompt": "Tell me about Algorand", "max_tokens": 100},
|
|
451
|
+
input_schema={
|
|
452
|
+
"properties": {
|
|
453
|
+
"prompt": {"type": "string", "description": "The text prompt"},
|
|
454
|
+
"max_tokens": {"type": "integer", "description": "Maximum tokens"},
|
|
455
|
+
},
|
|
456
|
+
"required": ["prompt"],
|
|
457
|
+
},
|
|
458
|
+
body_type="json",
|
|
459
|
+
output=OutputConfig(
|
|
460
|
+
example={"text": "Algorand is a...", "tokens_used": 42},
|
|
461
|
+
schema={
|
|
462
|
+
"properties": {
|
|
463
|
+
"text": {"type": "string"},
|
|
464
|
+
"tokens_used": {"type": "integer"},
|
|
465
|
+
},
|
|
466
|
+
"required": ["text"],
|
|
467
|
+
},
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
---
|
|
473
|
+
|
|
474
|
+
## Minimal Declaration (No Output)
|
|
475
|
+
|
|
476
|
+
```python
|
|
477
|
+
from x402.extensions.bazaar import declare_discovery_extension
|
|
478
|
+
|
|
479
|
+
discovery = declare_discovery_extension(
|
|
480
|
+
input={"query": "example search term"},
|
|
481
|
+
input_schema={
|
|
482
|
+
"properties": {"query": {"type": "string"}},
|
|
483
|
+
"required": ["query"],
|
|
484
|
+
},
|
|
485
|
+
)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
---
|
|
489
|
+
|
|
490
|
+
## Route Configuration with Bazaar Discovery (FastAPI)
|
|
491
|
+
|
|
492
|
+
```python
|
|
493
|
+
from x402.extensions.bazaar import declare_discovery_extension, OutputConfig
|
|
494
|
+
from x402.http import PaymentOption
|
|
495
|
+
from x402.http.types import RouteConfig
|
|
496
|
+
from x402.schemas import Network
|
|
497
|
+
|
|
498
|
+
AVM_NETWORK: Network = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
499
|
+
AVM_ADDRESS = "YOUR_ALGORAND_ADDRESS..."
|
|
500
|
+
|
|
501
|
+
routes = {
|
|
502
|
+
"GET /weather": RouteConfig(
|
|
503
|
+
accepts=[
|
|
504
|
+
PaymentOption(
|
|
505
|
+
scheme="exact",
|
|
506
|
+
pay_to=AVM_ADDRESS,
|
|
507
|
+
price="$0.001",
|
|
508
|
+
network=AVM_NETWORK,
|
|
509
|
+
),
|
|
510
|
+
],
|
|
511
|
+
description="Weather report",
|
|
512
|
+
mime_type="application/json",
|
|
513
|
+
extensions={
|
|
514
|
+
**declare_discovery_extension(
|
|
515
|
+
input={"city": "San Francisco"},
|
|
516
|
+
input_schema={
|
|
517
|
+
"properties": {"city": {"type": "string"}},
|
|
518
|
+
"required": ["city"],
|
|
519
|
+
},
|
|
520
|
+
output=OutputConfig(
|
|
521
|
+
example={"weather": "sunny", "temperature": 70},
|
|
522
|
+
schema={
|
|
523
|
+
"properties": {
|
|
524
|
+
"weather": {"type": "string"},
|
|
525
|
+
"temperature": {"type": "number"},
|
|
526
|
+
},
|
|
527
|
+
"required": ["weather", "temperature"],
|
|
528
|
+
},
|
|
529
|
+
),
|
|
530
|
+
)
|
|
531
|
+
},
|
|
532
|
+
),
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## Multi-Chain Route (Algorand + EVM)
|
|
539
|
+
|
|
540
|
+
```python
|
|
541
|
+
routes = {
|
|
542
|
+
"GET /weather": RouteConfig(
|
|
543
|
+
accepts=[
|
|
544
|
+
PaymentOption(
|
|
545
|
+
scheme="exact",
|
|
546
|
+
pay_to=AVM_ADDRESS,
|
|
547
|
+
price="$0.001",
|
|
548
|
+
network="algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=",
|
|
549
|
+
),
|
|
550
|
+
PaymentOption(
|
|
551
|
+
scheme="exact",
|
|
552
|
+
pay_to=EVM_ADDRESS,
|
|
553
|
+
price="$0.001",
|
|
554
|
+
network="eip155:84532",
|
|
555
|
+
),
|
|
556
|
+
],
|
|
557
|
+
extensions={
|
|
558
|
+
**declare_discovery_extension(
|
|
559
|
+
input={"city": "San Francisco"},
|
|
560
|
+
input_schema={
|
|
561
|
+
"properties": {"city": {"type": "string"}},
|
|
562
|
+
"required": ["city"],
|
|
563
|
+
},
|
|
564
|
+
output=OutputConfig(
|
|
565
|
+
example={"weather": "sunny", "temperature": 70},
|
|
566
|
+
),
|
|
567
|
+
)
|
|
568
|
+
},
|
|
569
|
+
),
|
|
570
|
+
}
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Registering Bazaar Extension on Async Server (FastAPI)
|
|
576
|
+
|
|
577
|
+
```python
|
|
578
|
+
from x402.server import x402ResourceServer
|
|
579
|
+
from x402.http import FacilitatorConfig, HTTPFacilitatorClient
|
|
580
|
+
from x402.extensions.bazaar import bazaar_resource_server_extension
|
|
581
|
+
from x402.mechanisms.avm.exact import ExactAvmServerScheme
|
|
582
|
+
from x402.schemas import Network
|
|
583
|
+
|
|
584
|
+
AVM_NETWORK: Network = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
585
|
+
|
|
586
|
+
facilitator = HTTPFacilitatorClient(
|
|
587
|
+
FacilitatorConfig(url="https://x402.org/facilitator")
|
|
588
|
+
)
|
|
589
|
+
server = x402ResourceServer(facilitator)
|
|
590
|
+
server.register(AVM_NETWORK, ExactAvmServerScheme())
|
|
591
|
+
server.register_extension(bazaar_resource_server_extension)
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Registering Bazaar Extension on Sync Server (Flask)
|
|
597
|
+
|
|
598
|
+
```python
|
|
599
|
+
from x402.server import x402ResourceServerSync
|
|
600
|
+
from x402.http import FacilitatorConfig, HTTPFacilitatorClientSync
|
|
601
|
+
from x402.extensions.bazaar import bazaar_resource_server_extension
|
|
602
|
+
from x402.mechanisms.avm.exact import ExactAvmServerScheme
|
|
603
|
+
from x402.schemas import Network
|
|
604
|
+
|
|
605
|
+
AVM_NETWORK: Network = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
606
|
+
|
|
607
|
+
facilitator = HTTPFacilitatorClientSync(
|
|
608
|
+
FacilitatorConfig(url="https://x402.org/facilitator")
|
|
609
|
+
)
|
|
610
|
+
server = x402ResourceServerSync(facilitator)
|
|
611
|
+
server.register(AVM_NETWORK, ExactAvmServerScheme())
|
|
612
|
+
server.register_extension(bazaar_resource_server_extension)
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
---
|
|
616
|
+
|
|
617
|
+
## Extracting Discovery Info (Facilitator Side)
|
|
618
|
+
|
|
619
|
+
```python
|
|
620
|
+
from x402.extensions.bazaar import extract_discovery_info
|
|
621
|
+
|
|
622
|
+
discovered = extract_discovery_info(
|
|
623
|
+
payment_payload=payment_payload,
|
|
624
|
+
payment_requirements=payment_requirements,
|
|
625
|
+
validate=True,
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
if discovered:
|
|
629
|
+
print(f"Resource URL: {discovered.resource_url}")
|
|
630
|
+
print(f"HTTP Method: {discovered.method}")
|
|
631
|
+
print(f"x402 Version: {discovered.x402_version}")
|
|
632
|
+
print(f"Description: {discovered.description}")
|
|
633
|
+
print(f"MIME Type: {discovered.mime_type}")
|
|
634
|
+
|
|
635
|
+
info = discovered.discovery_info
|
|
636
|
+
if hasattr(info.input, "query_params"):
|
|
637
|
+
print(f"Query params: {info.input.query_params}")
|
|
638
|
+
elif hasattr(info.input, "body"):
|
|
639
|
+
print(f"Body: {info.input.body}")
|
|
640
|
+
|
|
641
|
+
if info.output:
|
|
642
|
+
print(f"Output example: {info.output.example}")
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## Validating Discovery Extensions
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
from x402.extensions.bazaar import (
|
|
651
|
+
declare_discovery_extension,
|
|
652
|
+
validate_discovery_extension,
|
|
653
|
+
OutputConfig,
|
|
654
|
+
)
|
|
655
|
+
from x402.extensions.bazaar.types import parse_discovery_extension
|
|
656
|
+
|
|
657
|
+
ext_dict = declare_discovery_extension(
|
|
658
|
+
input={"city": "San Francisco"},
|
|
659
|
+
input_schema={
|
|
660
|
+
"properties": {"city": {"type": "string"}},
|
|
661
|
+
"required": ["city"],
|
|
662
|
+
},
|
|
663
|
+
output=OutputConfig(example={"weather": "sunny", "temperature": 70}),
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
extension = parse_discovery_extension(ext_dict["bazaar"])
|
|
667
|
+
|
|
668
|
+
result = validate_discovery_extension(extension)
|
|
669
|
+
|
|
670
|
+
if result.valid:
|
|
671
|
+
print("Extension is valid")
|
|
672
|
+
else:
|
|
673
|
+
print("Validation errors:", result.errors)
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
---
|
|
677
|
+
|
|
678
|
+
## Validate and Extract in One Step
|
|
679
|
+
|
|
680
|
+
```python
|
|
681
|
+
from x402.extensions.bazaar import validate_and_extract
|
|
682
|
+
|
|
683
|
+
result = validate_and_extract(extension_data)
|
|
684
|
+
|
|
685
|
+
if result.valid and result.info:
|
|
686
|
+
print(f"Method: {result.info.input.method}")
|
|
687
|
+
else:
|
|
688
|
+
print("Validation errors:", result.errors)
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## Querying Discovery Resources (Client Side)
|
|
694
|
+
|
|
695
|
+
```python
|
|
696
|
+
from x402.http import HTTPFacilitatorClient, FacilitatorConfig
|
|
697
|
+
from x402.extensions.bazaar import with_bazaar, ListDiscoveryResourcesParams
|
|
698
|
+
|
|
699
|
+
facilitator = HTTPFacilitatorClient(
|
|
700
|
+
FacilitatorConfig(url="https://x402.org/facilitator")
|
|
701
|
+
)
|
|
702
|
+
client = with_bazaar(facilitator)
|
|
703
|
+
|
|
704
|
+
response = client.extensions.discovery.list_resources()
|
|
705
|
+
for resource in response.resources:
|
|
706
|
+
print(f"URL: {resource.url}")
|
|
707
|
+
print(f"Type: {resource.type}")
|
|
708
|
+
print(f"Metadata: {resource.metadata}")
|
|
709
|
+
|
|
710
|
+
# Filter and paginate
|
|
711
|
+
response = client.extensions.discovery.list_resources(
|
|
712
|
+
ListDiscoveryResourcesParams(type="http", limit=10, offset=0)
|
|
713
|
+
)
|
|
714
|
+
print(f"Total resources: {response.total}")
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Complete FastAPI Server with Bazaar Discovery
|
|
720
|
+
|
|
721
|
+
```python
|
|
722
|
+
"""Algorand-gated weather API with Bazaar discovery."""
|
|
723
|
+
|
|
724
|
+
import os
|
|
725
|
+
|
|
726
|
+
from dotenv import load_dotenv
|
|
727
|
+
from fastapi import FastAPI
|
|
728
|
+
from pydantic import BaseModel
|
|
729
|
+
|
|
730
|
+
from x402.extensions.bazaar import (
|
|
731
|
+
OutputConfig,
|
|
732
|
+
bazaar_resource_server_extension,
|
|
733
|
+
declare_discovery_extension,
|
|
734
|
+
)
|
|
735
|
+
from x402.http import FacilitatorConfig, HTTPFacilitatorClient, PaymentOption
|
|
736
|
+
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
|
|
737
|
+
from x402.http.types import RouteConfig
|
|
738
|
+
from x402.mechanisms.avm.exact import ExactAvmServerScheme
|
|
739
|
+
from x402.schemas import Network
|
|
740
|
+
from x402.server import x402ResourceServer
|
|
741
|
+
|
|
742
|
+
load_dotenv()
|
|
743
|
+
|
|
744
|
+
AVM_ADDRESS = os.environ["AVM_ADDRESS"]
|
|
745
|
+
AVM_NETWORK: Network = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
746
|
+
FACILITATOR_URL = os.getenv("FACILITATOR_URL", "https://x402.org/facilitator")
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
class WeatherReport(BaseModel):
|
|
750
|
+
weather: str
|
|
751
|
+
temperature: int
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
class WeatherResponse(BaseModel):
|
|
755
|
+
report: WeatherReport
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
app = FastAPI(title="Weather API (x402 + Algorand + Bazaar)")
|
|
759
|
+
|
|
760
|
+
facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL))
|
|
761
|
+
server = x402ResourceServer(facilitator)
|
|
762
|
+
server.register(AVM_NETWORK, ExactAvmServerScheme())
|
|
763
|
+
server.register_extension(bazaar_resource_server_extension)
|
|
764
|
+
|
|
765
|
+
routes = {
|
|
766
|
+
"GET /weather": RouteConfig(
|
|
767
|
+
accepts=[
|
|
768
|
+
PaymentOption(
|
|
769
|
+
scheme="exact",
|
|
770
|
+
pay_to=AVM_ADDRESS,
|
|
771
|
+
price="$0.001",
|
|
772
|
+
network=AVM_NETWORK,
|
|
773
|
+
),
|
|
774
|
+
],
|
|
775
|
+
description="Get weather data for a city",
|
|
776
|
+
mime_type="application/json",
|
|
777
|
+
extensions={
|
|
778
|
+
**declare_discovery_extension(
|
|
779
|
+
input={"city": "San Francisco"},
|
|
780
|
+
input_schema={
|
|
781
|
+
"properties": {
|
|
782
|
+
"city": {
|
|
783
|
+
"type": "string",
|
|
784
|
+
"description": "City name to get weather for",
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
"required": ["city"],
|
|
788
|
+
},
|
|
789
|
+
output=OutputConfig(
|
|
790
|
+
example={"weather": "sunny", "temperature": 70},
|
|
791
|
+
schema={
|
|
792
|
+
"properties": {
|
|
793
|
+
"weather": {"type": "string"},
|
|
794
|
+
"temperature": {"type": "number"},
|
|
795
|
+
},
|
|
796
|
+
"required": ["weather", "temperature"],
|
|
797
|
+
},
|
|
798
|
+
),
|
|
799
|
+
)
|
|
800
|
+
},
|
|
801
|
+
),
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
@app.get("/weather")
|
|
808
|
+
async def get_weather(city: str = "San Francisco") -> WeatherResponse:
|
|
809
|
+
return WeatherResponse(
|
|
810
|
+
report=WeatherReport(weather="sunny", temperature=70)
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
@app.get("/health")
|
|
815
|
+
async def health():
|
|
816
|
+
return {"status": "ok"}
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
if __name__ == "__main__":
|
|
820
|
+
import uvicorn
|
|
821
|
+
|
|
822
|
+
uvicorn.run(app, host="0.0.0.0", port=4021)
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Complete Flask Server with Bazaar Discovery
|
|
828
|
+
|
|
829
|
+
```python
|
|
830
|
+
"""Flask version of the Algorand-gated weather API with Bazaar discovery."""
|
|
831
|
+
|
|
832
|
+
import os
|
|
833
|
+
|
|
834
|
+
from dotenv import load_dotenv
|
|
835
|
+
from flask import Flask, jsonify
|
|
836
|
+
|
|
837
|
+
from x402.extensions.bazaar import (
|
|
838
|
+
OutputConfig,
|
|
839
|
+
bazaar_resource_server_extension,
|
|
840
|
+
declare_discovery_extension,
|
|
841
|
+
)
|
|
842
|
+
from x402.http import FacilitatorConfig, HTTPFacilitatorClientSync, PaymentOption
|
|
843
|
+
from x402.http.middleware.flask import payment_middleware
|
|
844
|
+
from x402.http.types import RouteConfig
|
|
845
|
+
from x402.mechanisms.avm.exact import ExactAvmServerScheme
|
|
846
|
+
from x402.schemas import Network
|
|
847
|
+
from x402.server import x402ResourceServerSync
|
|
848
|
+
|
|
849
|
+
load_dotenv()
|
|
850
|
+
|
|
851
|
+
AVM_ADDRESS = os.environ["AVM_ADDRESS"]
|
|
852
|
+
AVM_NETWORK: Network = "algorand:SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI="
|
|
853
|
+
|
|
854
|
+
app = Flask(__name__)
|
|
855
|
+
|
|
856
|
+
facilitator = HTTPFacilitatorClientSync(
|
|
857
|
+
FacilitatorConfig(url=os.getenv("FACILITATOR_URL", "https://x402.org/facilitator"))
|
|
858
|
+
)
|
|
859
|
+
server = x402ResourceServerSync(facilitator)
|
|
860
|
+
server.register(AVM_NETWORK, ExactAvmServerScheme())
|
|
861
|
+
server.register_extension(bazaar_resource_server_extension)
|
|
862
|
+
|
|
863
|
+
routes = {
|
|
864
|
+
"GET /weather": RouteConfig(
|
|
865
|
+
accepts=[
|
|
866
|
+
PaymentOption(
|
|
867
|
+
scheme="exact",
|
|
868
|
+
pay_to=AVM_ADDRESS,
|
|
869
|
+
price="$0.001",
|
|
870
|
+
network=AVM_NETWORK,
|
|
871
|
+
),
|
|
872
|
+
],
|
|
873
|
+
extensions={
|
|
874
|
+
**declare_discovery_extension(
|
|
875
|
+
input={"city": "San Francisco"},
|
|
876
|
+
input_schema={
|
|
877
|
+
"properties": {"city": {"type": "string"}},
|
|
878
|
+
"required": ["city"],
|
|
879
|
+
},
|
|
880
|
+
output=OutputConfig(
|
|
881
|
+
example={"weather": "sunny", "temperature": 70},
|
|
882
|
+
),
|
|
883
|
+
)
|
|
884
|
+
},
|
|
885
|
+
),
|
|
886
|
+
}
|
|
887
|
+
payment_middleware(app, routes=routes, server=server)
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
@app.route("/weather")
|
|
891
|
+
def get_weather():
|
|
892
|
+
return jsonify({"report": {"weather": "sunny", "temperature": 70}})
|
|
893
|
+
|
|
894
|
+
|
|
895
|
+
if __name__ == "__main__":
|
|
896
|
+
app.run(host="0.0.0.0", port=4021, debug=False)
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## Client Discovering and Calling the API
|
|
902
|
+
|
|
903
|
+
```python
|
|
904
|
+
"""Client that discovers and calls the Algorand-gated weather API."""
|
|
905
|
+
|
|
906
|
+
import httpx
|
|
907
|
+
|
|
908
|
+
from x402.http import FacilitatorConfig, HTTPFacilitatorClient
|
|
909
|
+
from x402.extensions.bazaar import with_bazaar, ListDiscoveryResourcesParams
|
|
910
|
+
|
|
911
|
+
FACILITATOR_URL = "https://x402.org/facilitator"
|
|
912
|
+
|
|
913
|
+
facilitator = HTTPFacilitatorClient(FacilitatorConfig(url=FACILITATOR_URL))
|
|
914
|
+
client = with_bazaar(facilitator)
|
|
915
|
+
|
|
916
|
+
resources = client.extensions.discovery.list_resources(
|
|
917
|
+
ListDiscoveryResourcesParams(type="http", limit=50)
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
for resource in resources.resources:
|
|
921
|
+
print(f"Discovered: {resource.url} ({resource.type})")
|
|
922
|
+
if resource.metadata:
|
|
923
|
+
print(f" Metadata: {resource.metadata}")
|
|
924
|
+
```
|