Skip to content

Commit 6f5eecf

Browse files
committed
added x402 payment methods
1 parent f0c0d0b commit 6f5eecf

File tree

5 files changed

+237
-4315
lines changed

5 files changed

+237
-4315
lines changed

fewsats/_modidx.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
'fewsats.core.Fewsats.pay_link': ('core.html#fewsats.pay_link', 'fewsats/core.py'),
2222
'fewsats.core.Fewsats.pay_offer': ('core.html#fewsats.pay_offer', 'fewsats/core.py'),
2323
'fewsats.core.Fewsats.pay_offer_str': ('core.html#fewsats.pay_offer_str', 'fewsats/core.py'),
24+
'fewsats.core.Fewsats.pay_x402_link': ('core.html#fewsats.pay_x402_link', 'fewsats/core.py'),
25+
'fewsats.core.Fewsats.pay_x402_offer': ('core.html#fewsats.pay_x402_offer', 'fewsats/core.py'),
2426
'fewsats.core.Fewsats.payment_info': ('core.html#fewsats.payment_info', 'fewsats/core.py'),
2527
'fewsats.core.Fewsats.payment_methods': ('core.html#fewsats.payment_methods', 'fewsats/core.py'),
2628
'fewsats.core.Fewsats.verify_webhook': ('core.html#fewsats.verify_webhook', 'fewsats/core.py'),

fewsats/core.py

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ def pay_lightning(self: Fewsats,
195195
}
196196
return self._request("POST", "v0/l402/purchases/lightning", json=data)
197197

198-
# %% ../nbs/00_core.ipynb 45
198+
# %% ../nbs/00_core.ipynb 46
199199
class Offer(BasicRepr):
200200
"Represents a single L402 offer"
201201
def __init__(self,
@@ -250,7 +250,7 @@ def from_dict(cls, d: Dict[str, Any]) -> 'L402Offers':
250250
)
251251

252252

253-
# %% ../nbs/00_core.ipynb 49
253+
# %% ../nbs/00_core.ipynb 50
254254
@patch
255255
def pay_offer(self:Fewsats,
256256
offer_id : str, # the offer id to pay for
@@ -279,7 +279,7 @@ def pay_offer(self:Fewsats,
279279
return self._request("POST", "v0/l402/purchases/from-offer", json=data)
280280

281281

282-
# %% ../nbs/00_core.ipynb 52
282+
# %% ../nbs/00_core.ipynb 53
283283
@patch
284284
def pay_offer_str(self:Fewsats,
285285
offer_id : str, # the offer id to pay for
@@ -316,7 +316,7 @@ def pay_offer_str(self:Fewsats,
316316

317317
return self._request("POST", "v0/l402/purchases/from-offer", timeout=20, json=data)
318318

319-
# %% ../nbs/00_core.ipynb 55
319+
# %% ../nbs/00_core.ipynb 56
320320
@patch
321321
def pay_link(self:Fewsats,
322322
url: str, # URL to purchase from
@@ -346,14 +346,70 @@ def pay_link(self:Fewsats,
346346
}
347347
return self._request("POST", "v0/l402/purchases/from-link", json=data)
348348

349-
# %% ../nbs/00_core.ipynb 58
349+
# %% ../nbs/00_core.ipynb 59
350350
@patch
351351
def payment_info(self:Fewsats,
352352
pid:str): # purchase id
353353
"Retrieve the details of a payment."
354354
return self._request("GET", f"v0/l402/outgoing-payments/{pid}")
355355

356-
# %% ../nbs/00_core.ipynb 61
356+
# %% ../nbs/00_core.ipynb 62
357+
@patch
358+
def pay_x402_offer(self:Fewsats,
359+
payload:Dict[str, Any], # The x402 offer payload
360+
chain:str = "base", # Blockchain chain to use
361+
) -> dict:
362+
"""Creates a payment from an x402 offer and returns a payment header to access the resource.
363+
364+
Args:
365+
payload: The x402 offer payload containing accepts, error, and x402Version
366+
chain: Blockchain chain to use (default: "base")
367+
368+
Returns:
369+
Dictionary containing payment_header to use for subsequent requests
370+
"""
371+
data = {
372+
"chain": chain,
373+
"payload": payload
374+
}
375+
return self._request("POST", "v0/x402/purchases/from-offer", json=data)
376+
377+
# %% ../nbs/00_core.ipynb 65
378+
@patch
379+
def pay_x402_link(self:Fewsats,
380+
url:str, # URL to purchase from
381+
method:str = "GET", # HTTP method to use
382+
body:Dict[str, Any] = None, # Optional request body
383+
headers:Dict[str, str] = None, # Optional request headers
384+
chain:str = "base", # Blockchain chain to use
385+
) -> dict:
386+
"""Creates a purchase from an external URL that requires x402 payment.
387+
388+
Args:
389+
url: The URL to purchase from
390+
method: HTTP method to use (default: "GET")
391+
body: Optional request body
392+
headers: Optional request headers
393+
chain: Blockchain chain to use (default: "base")
394+
395+
Returns:
396+
The response from the target URL after successful payment
397+
"""
398+
data = {
399+
"url": url,
400+
"method": method,
401+
"chain": chain
402+
}
403+
404+
if body is not None:
405+
data["body"] = body
406+
407+
if headers is not None:
408+
data["headers"] = headers
409+
410+
return self._request("POST", "v0/x402/purchases/from-link", json=data)
411+
412+
# %% ../nbs/00_core.ipynb 68
357413
def get_response(r): return r.status_code, r.text
358414

359415
def wrap_with_response(method):

nbs/00_core.ipynb

Lines changed: 173 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@
271271
{
272272
"data": {
273273
"text/plain": [
274-
"(200, [{'id': 1, 'balance': 4396, 'currency': 'usd'}])"
274+
"(200, [{'id': 15, 'balance': 87, 'currency': 'usd'}])"
275275
]
276276
},
277277
"execution_count": null,
@@ -487,9 +487,9 @@
487487
"data": {
488488
"text/plain": [
489489
"(200,\n",
490-
" {'expires_at': '2025-04-23T13:10:12.330724+00:00',\n",
490+
" {'expires_at': '2025-04-28T16:43:07.985112+00:00',\n",
491491
" 'offer_id': 'test_offer_2',\n",
492-
" 'payment_request': {'lightning_invoice': 'lnbc100n1p5q3h5ppp54neu5ukgkshem4j6yyx4gdpuscj8psy92f52jhgzxsts6az02twsdq523jhxapq2pskx6mpvajscqzpgxqrzpnrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp52p0m2rgrd8v6peyxm2dqwwyfn32wmls3x74thr890h0rx2nt3h2q9qxpqysgqu6rsscdg76jng554uj7pt7ln7kage37u9t2ut4208suzvu5e4hw5n2ssa4dhmfpgc0mgmywzvya9902sg8re846pn6f4fkl3vhpa80cp7jqjjr'},\n",
492+
" 'payment_request': {'lightning_invoice': 'lnbc100n1p5qlflfpp50zwmzn36u93u7hcnhyjtqgsa65lnrrzfymeetv6uyqrltnktpesqdq523jhxapq2pskx6mpvajscqzpgxqrzpjrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5kppjx7rg9kq0ugvgn4vkcfa4z34ytcjtylcfxhs3dwl2sr029m8s9qxpqysgqtgunz5j2wkm5845qmvk2m7yz5nnqwtpnhqteawq97wv3m9xneeusjuj7xjp88078th6rtx09ad7zt3cc3l5dd3w9hkakfpkw3vvm0ugqgm2lc4'},\n",
493493
" 'version': '0.2.2'})"
494494
]
495495
},
@@ -560,7 +560,7 @@
560560
"cell_type": "markdown",
561561
"metadata": {},
562562
"source": [
563-
"## Webhooks\n",
563+
"### Webhooks\n",
564564
"\n",
565565
"Use this as a vendor to get notified when someone pays for your offer. Currently only 1 webhook is supported per user."
566566
]
@@ -710,6 +710,8 @@
710710
"cell_type": "markdown",
711711
"metadata": {},
712712
"source": [
713+
"## Pay Methods\n",
714+
"\n",
713715
"### Pay Lightning Invoice\n",
714716
"\n",
715717
"Pay lightning invoice is a low-level method to manually pay for a lightning invoice. "
@@ -773,14 +775,25 @@
773775
"r = fs.pay_lightning(invoice=ln_invoice,\n",
774776
" description=\"fewsats webhook trial\", amount=1)\n",
775777
"lightning_payment = r.json()\n",
776-
"r.status_code, lightning_payment"
778+
"r.status_code, lightning_payment\n"
779+
]
780+
},
781+
{
782+
"cell_type": "code",
783+
"execution_count": null,
784+
"metadata": {},
785+
"outputs": [],
786+
"source": [
787+
"r = fs.pay_lightning(invoice='lnbc100n1p5qlv5dpp5r4jjrenyvlndnr59nx2fgk8azyeaaxzpeuy7putgm07wfnvt398qdqqcqzzsxqrrsssp54897npsmum0av3qqj6jyvl3sks4hlc67ugdyr8x85hla79u262uq9qxpqysgq3m9p2a8j7fy2pxeg5xuhyw4zwwp6md5egkez28afffmsch2l7clhw5922mxlr52gp48w0pdvnngytgj08kn4z8uv25pq7fc0mxhrs6qphe3c0k',\n",
788+
" description=\"fewsats webhook trial\", amount=1)\n",
789+
"r.status_code, r.text"
777790
]
778791
},
779792
{
780793
"cell_type": "markdown",
781794
"metadata": {},
782795
"source": [
783-
"### Pay Offer\n",
796+
"### Paying L402 Offers\n",
784797
"\n",
785798
"The pay method pays for a specific offer. The user is not required to fetch the payment details beforehand. It is asynchronous and returns the `payment_id` and `status`. Using the `payment_id` we can check the status of the payment.\n",
786799
"\n",
@@ -894,7 +907,7 @@
894907
"cell_type": "markdown",
895908
"metadata": {},
896909
"source": [
897-
"### Pay Offer\n"
910+
"### Pay L402 Offer\n"
898911
]
899912
},
900913
{
@@ -964,7 +977,7 @@
964977
"cell_type": "markdown",
965978
"metadata": {},
966979
"source": [
967-
"### Pay Offer with JSON string\n",
980+
"### L402 Pay Offer with JSON string\n",
968981
"\n",
969982
"This alternative method accepts a JSON string containing L402 offers. It should be used by systems that do not support custom classes in tool calling."
970983
]
@@ -1163,6 +1176,158 @@
11631176
"r.status_code, r.json()"
11641177
]
11651178
},
1179+
{
1180+
"cell_type": "markdown",
1181+
"metadata": {},
1182+
"source": [
1183+
"### Pay X402 Offer"
1184+
]
1185+
},
1186+
{
1187+
"cell_type": "code",
1188+
"execution_count": null,
1189+
"metadata": {},
1190+
"outputs": [],
1191+
"source": [
1192+
"#| export\n",
1193+
"\n",
1194+
"@patch\n",
1195+
"def pay_x402_offer(self:Fewsats,\n",
1196+
" payload:Dict[str, Any], # The x402 offer payload\n",
1197+
" chain:str = \"base\", # Blockchain chain to use\n",
1198+
" ) -> dict:\n",
1199+
" \"\"\"Creates a payment from an x402 offer and returns a payment header to access the resource.\n",
1200+
" \n",
1201+
" Args:\n",
1202+
" payload: The x402 offer payload containing accepts, error, and x402Version\n",
1203+
" chain: Blockchain chain to use (default: \"base\")\n",
1204+
" \n",
1205+
" Returns:\n",
1206+
" Dictionary containing payment_header to use for subsequent requests\n",
1207+
" \"\"\"\n",
1208+
" data = {\n",
1209+
" \"chain\": chain,\n",
1210+
" \"payload\": payload\n",
1211+
" }\n",
1212+
" return self._request(\"POST\", \"v0/x402/purchases/from-offer\", json=data)"
1213+
]
1214+
},
1215+
{
1216+
"cell_type": "code",
1217+
"execution_count": null,
1218+
"metadata": {},
1219+
"outputs": [
1220+
{
1221+
"data": {
1222+
"text/plain": [
1223+
"(200,\n",
1224+
" {'payment_header': 'eyJ4NDAyVmVyc2lvbiI6IDEsICJzY2hlbWUiOiAiZXhhY3QiLCAibmV0d29yayI6ICJiYXNlLXNlcG9saWEiLCAicGF5bG9hZCI6IHsic2lnbmF0dXJlIjogIjB4MTU4NWExZjcyZjk3ZDU3ZmM3NjliZmRlNjZjNjQ0NzNlMzAxYzYwYjg0YjJiZTk5NDFiN2ZmMTRkMTc4MzIyOTQzOGRlNGI2ZGU5YjdlMzlkMDZiMmEwY2I0YTA0ZjQ1MDE5ZTExYmJjOWI5OWEyZGQ3OTQ0YTNmYzgwOTdiNjExYyIsICJhdXRob3JpemF0aW9uIjogeyJmcm9tIjogIjB4MjNiODExMDllODFGREZFOGU0MjEzNUU4M0MzMWIxMUFhN0Q5OTlBNCIsICJ0byI6ICIweGRkYjI0QmQ4QTZDYjBmMmQzZWFCRjdhODI4QzBiNDM2NDY2OEI5NjMiLCAidmFsdWUiOiAiMSIsICJ2YWxpZEFmdGVyIjogIjE3NDc0MDAyNDEiLCAidmFsaWRCZWZvcmUiOiAiMTc0NzQwMDM2MSIsICJub25jZSI6ICIweGI0ODI1M2NiMTJlMzMyNDE3N2ZiODgwYzU4OThkYzU5NmVlOGM2OWRkNGYyN2JjZTI0MGUwNzg1MzdiMTJjMzgifX19'})"
1225+
]
1226+
},
1227+
"execution_count": 34,
1228+
"metadata": {},
1229+
"output_type": "execute_result"
1230+
}
1231+
],
1232+
"source": [
1233+
"x402_offer = {\n",
1234+
" \"accepts\": [\n",
1235+
" {\n",
1236+
" \"scheme\": \"exact\",\n",
1237+
" \"network\": \"base-sepolia\",\n",
1238+
" \"maxAmountRequired\": \"1\",\n",
1239+
" \"resource\": \"https://proxy402.com/KQzW9kfi3Z\",\n",
1240+
" \"description\": \"Payment for GET https://proxy402.com/KQzW9kfi3Z\",\n",
1241+
" \"mimeType\": \"\",\n",
1242+
" \"payTo\": \"0xddb24Bd8A6Cb0f2d3eaBF7a828C0b4364668B963\",\n",
1243+
" \"maxTimeoutSeconds\": 60,\n",
1244+
" \"asset\": \"0x036CbD53842c5426634e7929541eC2318f3dCF7e\",\n",
1245+
" \"extra\": {\n",
1246+
" \"name\": \"USDC\",\n",
1247+
" \"version\": \"2\"\n",
1248+
" }\n",
1249+
" }\n",
1250+
" ],\n",
1251+
" \"error\": \"X-PAYMENT header is required\",\n",
1252+
" \"x402Version\": 1\n",
1253+
"}\n",
1254+
"\n",
1255+
"r = fs.pay_x402_offer(x402_offer, chain=\"base-sepolia\")\n",
1256+
"r.status_code, r.json()"
1257+
]
1258+
},
1259+
{
1260+
"cell_type": "markdown",
1261+
"metadata": {},
1262+
"source": [
1263+
"### Pay X402 Link"
1264+
]
1265+
},
1266+
{
1267+
"cell_type": "code",
1268+
"execution_count": null,
1269+
"metadata": {},
1270+
"outputs": [],
1271+
"source": [
1272+
"#| export\n",
1273+
"\n",
1274+
"@patch\n",
1275+
"def pay_x402_link(self:Fewsats,\n",
1276+
" url:str, # URL to purchase from\n",
1277+
" method:str = \"GET\", # HTTP method to use\n",
1278+
" body:Dict[str, Any] = None, # Optional request body\n",
1279+
" headers:Dict[str, str] = None, # Optional request headers\n",
1280+
" chain:str = \"base\", # Blockchain chain to use\n",
1281+
" ) -> dict:\n",
1282+
" \"\"\"Creates a purchase from an external URL that requires x402 payment.\n",
1283+
" \n",
1284+
" Args:\n",
1285+
" url: The URL to purchase from\n",
1286+
" method: HTTP method to use (default: \"GET\")\n",
1287+
" body: Optional request body\n",
1288+
" headers: Optional request headers\n",
1289+
" chain: Blockchain chain to use (default: \"base\")\n",
1290+
" \n",
1291+
" Returns:\n",
1292+
" The response from the target URL after successful payment\n",
1293+
" \"\"\"\n",
1294+
" data = {\n",
1295+
" \"url\": url,\n",
1296+
" \"method\": method,\n",
1297+
" \"chain\": chain\n",
1298+
" }\n",
1299+
" \n",
1300+
" if body is not None:\n",
1301+
" data[\"body\"] = body\n",
1302+
" \n",
1303+
" if headers is not None:\n",
1304+
" data[\"headers\"] = headers\n",
1305+
" \n",
1306+
" return self._request(\"POST\", \"v0/x402/purchases/from-link\", json=data)"
1307+
]
1308+
},
1309+
{
1310+
"cell_type": "code",
1311+
"execution_count": null,
1312+
"metadata": {},
1313+
"outputs": [
1314+
{
1315+
"data": {
1316+
"text/plain": [
1317+
"(400, {'detail': 'Redirecting...\\n'})"
1318+
]
1319+
},
1320+
"execution_count": 25,
1321+
"metadata": {},
1322+
"output_type": "execute_result"
1323+
}
1324+
],
1325+
"source": [
1326+
"x402_link = \"https://proxy402.com/KQzW9kfi3Z\"\n",
1327+
"r = fs.pay_x402_link(x402_link, chain=\"base-sepolia\")\n",
1328+
"r.status_code, r.json()"
1329+
]
1330+
},
11661331
{
11671332
"cell_type": "markdown",
11681333
"metadata": {},

0 commit comments

Comments
 (0)