payment clients

Payment clients are the interfaces and implementations that allow users to pay for L402 offers.
chain = 'base-mainnet'

source

PaymentRequest

 PaymentRequest (offer_id:str, payment_context_token:str,
                 payment_method:str, chain:str='', asset:str='')

Represents a payment request to get payment details from a payment provider


source

PaymentProvider

 PaymentProvider ()

Base class for all payment providers

# from l402.utils import *

# run_l402_server(PaymentRequest, port=9000)

Coinbase Provider


source

CoinbaseProvider

 CoinbaseProvider (wallet:cdp.wallet.Wallet, asset:str='usdc')

Base class for objects needing a basic __repr__

c = CoinbaseProvider(wallet=create_test_wallet(fund=False, chain=chain))
c
CoinbaseProvider(wallet=Wallet: (id: ef60974b-2571-44ca-ac25-ff99d2e3c88f, network_id: base-mainnet, server_signer_status: None), asset='usdc', supported_methods=['onchain'], chain='base-mainnet')
r = httpx.get('https://l402-offers.replit.app')
# r = httpx.get('http://localhost:9000/offers')
r.status_code, r.text
(402,
 '{"offers":[{"offer_id":"ce23eefd-1156-4aa4-85c1-361918a24485","amount":1,"currency":"USD","description":"Purchase 1 credit for API access","title":"1 Credit Package","type":"one-time","payment_methods":["onchain","lightning"]}],"payment_context_token":"8ee0082b-97c8-4ac6-baa4-f9f544dc977c","payment_request_url":"https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request","version":"0.2.2"}')
o = r.json()
o
{'offers': [{'offer_id': 'ce23eefd-1156-4aa4-85c1-361918a24485',
   'amount': 1,
   'currency': 'USD',
   'description': 'Purchase 1 credit for API access',
   'title': '1 Credit Package',
   'type': 'one-time',
   'payment_methods': ['onchain', 'lightning']}],
 'payment_context_token': '8ee0082b-97c8-4ac6-baa4-f9f544dc977c',
 'payment_request_url': 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request',
 'version': '0.2.2'}
data = {
    "offer_id": first(o['offers'])['offer_id'],
    "payment_method": 'onchain',
    "chain": chain,
    "asset": 'usdc',
    "payment_context_token": o['payment_context_token']
    }
r = httpx.post(o['payment_request_url'], json=data)
r.status_code, r.json()
(200,
 {'expires_at': '2025-01-28T02:06:17.637705+00:00',
  'offer_id': 'ce23eefd-1156-4aa4-85c1-361918a24485',
  'payment_request': {'checkout_url': 'https://commerce.coinbase.com/pay/be427ef1-153a-495e-93eb-1b10550731d0',
   'address': '0x03059433BCdB6144624cC2443159D9445C32b7a8',
   'chain': 'base-mainnet',
   'asset': 'usdc'},
  'version': '0.2.2'})
data = {
    "offer_id": first(o['offers'])['offer_id'],
    "payment_method": 'lightning',
    "payment_context_token": o['payment_context_token']
    }
r = httpx.post(o['payment_request_url'], json=data, timeout=15)
r.status_code, r.text
(200,
 '{"expires_at": "2025-01-28T04:14:16.328280+00:00", "offer_id": "ce23eefd-1156-4aa4-85c1-361918a24485", "payment_request": {"lightning_invoice": "lnbc90n1pnes5txpp5zxhg46zr2lycmqg9km930h9mmefeyanw3jm8c2cxpqxlhlvevkgsdq6xysyxun9v35hggzsv93kkct8v5cqzpgxqrzpjrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5y9unj8j6czfjy274dj7war7k7xey8jvgegsxcjenct8ntah4eyvs9qxpqysgqq60qg26s74k4jsetcz954dlpghg63a8qrprqytr0ml0f2hxtfm0zujk4t54x2n4hxaxaz6d75tm7xjalv287mn559uq8tnmyfs92gdqpdutj9l"}, "version": "0.2.2"}')
data = {
    "offer_id": 'test-lightning-1',
    "payment_method": 'lightning',
    "payment_context_token": '550cdc77-bdae-410b-bd7a-091b14be72bb'
    }
r = httpx.post('http://localhost:8000/v0/l402/payment-request', json=data, timeout=15)
r.status_code, r.text
(200,
 '{"expires_at": "2025-01-28T02:46:35.422815+00:00", "offer_id": "test-lightning-1", "payment_request": {"lightning_invoice": "lnbc90n1pnes0xhpp5lsp46vm2j47f8z3rm3lmj2lh54gvc4jn3hql0qhp9ryxef9fz0lsdpy23jhxapqf35kw6r5de5kueeq2pshjmt9de6qcqzpgxqrzpnrzjqwghf7zxvfkxq5a6sr65g0gdkv768p83mhsnt0msszapamzx2qvuxqqqqz99gpz55yqqqqqqqqqqqqqq9qrzjq25carzepgd4vqsyn44jrk85ezrpju92xyrk9apw4cdjh6yrwt5jgqqqqz99gpz55yqqqqqqqqqqqqqq9qsp5ykrsqs0x9mjt22937a0hn5wz0z0fxd9yxv97sud2kkrswuakh2js9qxpqysgqeq8g08w8n3t0wlhj3h3zkjsnpa8gznzncf98nqk92lyd96a30gsy8fxzk5q9z47fsy59ljn548xlck84qeta88wygppq3zed8qsz9dgqxn7rtq"}, "version": "0.2.2"}')

source

get_payment_request

 get_payment_request (payment_request_url:str, payment_context_token:str,
                      offer_id:str, payment_method:str, chain:str='',
                      asset:str='')
r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'onchain', chain, 'usdc')
r
---------------------------------------------------------------------------
ReadTimeout                               Traceback (most recent call last)
File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:101, in map_httpcore_exceptions()
    100 try:
--> 101     yield
    102 except Exception as exc:

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:250, in HTTPTransport.handle_request(self, request)
    249 with map_httpcore_exceptions():
--> 250     resp = self._pool.handle_request(req)
    252 assert isinstance(resp.stream, typing.Iterable)

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:256, in ConnectionPool.handle_request(self, request)
    255     self._close_connections(closing)
--> 256     raise exc from None
    258 # Return the response. Note that in this case we still have to manage
    259 # the point at which the response is closed.

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection_pool.py:236, in ConnectionPool.handle_request(self, request)
    234 try:
    235     # Send the request on the assigned connection.
--> 236     response = connection.handle_request(
    237         pool_request.request
    238     )
    239 except ConnectionNotAvailable:
    240     # In some cases a connection may initially be available to
    241     # handle a request, but then become unavailable.
    242     #
    243     # In this case we clear the connection and try again.

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/connection.py:103, in HTTPConnection.handle_request(self, request)
    101     raise exc
--> 103 return self._connection.handle_request(request)

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:136, in HTTP11Connection.handle_request(self, request)
    135         self._response_closed()
--> 136 raise exc

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:106, in HTTP11Connection.handle_request(self, request)
     97 with Trace(
     98     "receive_response_headers", logger, request, kwargs
     99 ) as trace:
    100     (
    101         http_version,
    102         status,
    103         reason_phrase,
    104         headers,
    105         trailing_data,
--> 106     ) = self._receive_response_headers(**kwargs)
    107     trace.return_value = (
    108         http_version,
    109         status,
    110         reason_phrase,
    111         headers,
    112     )

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:177, in HTTP11Connection._receive_response_headers(self, request)
    176 while True:
--> 177     event = self._receive_event(timeout=timeout)
    178     if isinstance(event, h11.Response):

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_sync/http11.py:217, in HTTP11Connection._receive_event(self, timeout)
    216 if event is h11.NEED_DATA:
--> 217     data = self._network_stream.read(
    218         self.READ_NUM_BYTES, timeout=timeout
    219     )
    221     # If we feed this case through h11 we'll raise an exception like:
    222     #
    223     #     httpcore.RemoteProtocolError: can't handle event type
   (...)
    227     # perspective. Instead we handle this case distinctly and treat
    228     # it as a ConnectError.

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_backends/sync.py:126, in SyncStream.read(self, max_bytes, timeout)
    125 exc_map: ExceptionMapping = {socket.timeout: ReadTimeout, OSError: ReadError}
--> 126 with map_exceptions(exc_map):
    127     self._sock.settimeout(timeout)

File /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback)
    157 try:
--> 158     self.gen.throw(value)
    159 except StopIteration as exc:
    160     # Suppress StopIteration *unless* it's the same exception that
    161     # was passed to throw().  This prevents a StopIteration
    162     # raised inside the "with" statement from being suppressed.

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpcore/_exceptions.py:14, in map_exceptions(map)
     13     if isinstance(exc, from_exc):
---> 14         raise to_exc(exc) from exc
     15 raise

ReadTimeout: The read operation timed out

The above exception was the direct cause of the following exception:

ReadTimeout                               Traceback (most recent call last)
Cell In[13], line 1
----> 1 r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'onchain', chain, 'usdc')
      2 r

Cell In[12], line 16, in get_payment_request(payment_request_url, payment_context_token, offer_id, payment_method, chain, asset)
      3 def get_payment_request(payment_request_url: str,
      4                         payment_context_token: str,
      5                         offer_id: str, 
      6                         payment_method: str, 
      7                         chain: str = "", 
      8                         asset: str = ""):
      9     data = {
     10         "offer_id": offer_id,
     11         "payment_method": payment_method,
   (...)
     14         "payment_context_token": payment_context_token
     15     }
---> 16     r = httpx.post(payment_request_url, json=data)
     17     r.raise_for_status()
     18     return r.json()

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_api.py:304, in post(url, content, data, files, json, params, headers, cookies, auth, proxy, follow_redirects, verify, timeout, trust_env)
    282 def post(
    283     url: URL | str,
    284     *,
   (...)
    297     trust_env: bool = True,
    298 ) -> Response:
    299     """
    300     Sends a `POST` request.
    301 
    302     **Parameters**: See `httpx.request`.
    303     """
--> 304     return request(
    305         "POST",
    306         url,
    307         content=content,
    308         data=data,
    309         files=files,
    310         json=json,
    311         params=params,
    312         headers=headers,
    313         cookies=cookies,
    314         auth=auth,
    315         proxy=proxy,
    316         follow_redirects=follow_redirects,
    317         verify=verify,
    318         timeout=timeout,
    319         trust_env=trust_env,
    320     )

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_api.py:109, in request(method, url, params, content, data, files, json, headers, cookies, auth, proxy, timeout, follow_redirects, verify, trust_env)
     57 """
     58 Sends an HTTP request.
     59 
   (...)
    100 ```
    101 """
    102 with Client(
    103     cookies=cookies,
    104     proxy=proxy,
   (...)
    107     trust_env=trust_env,
    108 ) as client:
--> 109     return client.request(
    110         method=method,
    111         url=url,
    112         content=content,
    113         data=data,
    114         files=files,
    115         json=json,
    116         params=params,
    117         headers=headers,
    118         auth=auth,
    119         follow_redirects=follow_redirects,
    120     )

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:825, in Client.request(self, method, url, content, data, files, json, params, headers, cookies, auth, follow_redirects, timeout, extensions)
    810     warnings.warn(message, DeprecationWarning, stacklevel=2)
    812 request = self.build_request(
    813     method=method,
    814     url=url,
   (...)
    823     extensions=extensions,
    824 )
--> 825 return self.send(request, auth=auth, follow_redirects=follow_redirects)

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:914, in Client.send(self, request, stream, auth, follow_redirects)
    910 self._set_timeout(request)
    912 auth = self._build_request_auth(request, auth)
--> 914 response = self._send_handling_auth(
    915     request,
    916     auth=auth,
    917     follow_redirects=follow_redirects,
    918     history=[],
    919 )
    920 try:
    921     if not stream:

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:942, in Client._send_handling_auth(self, request, auth, follow_redirects, history)
    939 request = next(auth_flow)
    941 while True:
--> 942     response = self._send_handling_redirects(
    943         request,
    944         follow_redirects=follow_redirects,
    945         history=history,
    946     )
    947     try:
    948         try:

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:979, in Client._send_handling_redirects(self, request, follow_redirects, history)
    976 for hook in self._event_hooks["request"]:
    977     hook(request)
--> 979 response = self._send_single_request(request)
    980 try:
    981     for hook in self._event_hooks["response"]:

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_client.py:1014, in Client._send_single_request(self, request)
   1009     raise RuntimeError(
   1010         "Attempted to send an async request with a sync Client instance."
   1011     )
   1013 with request_context(request=request):
-> 1014     response = transport.handle_request(request)
   1016 assert isinstance(response.stream, SyncByteStream)
   1018 response.request = request

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:249, in HTTPTransport.handle_request(self, request)
    235 import httpcore
    237 req = httpcore.Request(
    238     method=request.method,
    239     url=httpcore.URL(
   (...)
    247     extensions=request.extensions,
    248 )
--> 249 with map_httpcore_exceptions():
    250     resp = self._pool.handle_request(req)
    252 assert isinstance(resp.stream, typing.Iterable)

File /Library/Frameworks/Python.framework/Versions/3.12/lib/python3.12/contextlib.py:158, in _GeneratorContextManager.__exit__(self, typ, value, traceback)
    156     value = typ()
    157 try:
--> 158     self.gen.throw(value)
    159 except StopIteration as exc:
    160     # Suppress StopIteration *unless* it's the same exception that
    161     # was passed to throw().  This prevents a StopIteration
    162     # raised inside the "with" statement from being suppressed.
    163     return exc is not value

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_transports/default.py:118, in map_httpcore_exceptions()
    115     raise
    117 message = str(exc)
--> 118 raise mapped_exc(message) from exc

ReadTimeout: The read operation timed out
r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'lightning')
r
---------------------------------------------------------------------------
HTTPStatusError                           Traceback (most recent call last)
Cell In[13], line 1
----> 1 r = get_payment_request(o['payment_request_url'], o['payment_context_token'], o['offers'][0]['offer_id'], 'lightning')
      2 r

Cell In[11], line 17, in get_payment_request(payment_request_url, payment_context_token, offer_id, payment_method, chain, asset)
      9 data = {
     10     "offer_id": offer_id,
     11     "payment_method": payment_method,
   (...)
     14     "payment_context_token": payment_context_token
     15 }
     16 r = httpx.post(payment_request_url, json=data)
---> 17 r.raise_for_status()
     18 return r.json()

File ~/go/github.com/Fewsats/l402-python/venv/lib/python3.12/site-packages/httpx/_models.py:829, in Response.raise_for_status(self)
    827 error_type = error_types.get(status_class, "Invalid status code")
    828 message = message.format(self, error_type=error_type)
--> 829 raise HTTPStatusError(message, request=request, response=self)

HTTPStatusError: Server error '500 Internal Server Error' for url 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500

source

Client

 Client (lightning_provider=None, credit_card_provider=None,
         onchain_provider=None, fewsats_provider=None)

Base class for objects needing a basic __repr__

w = create_test_wallet(fund=False, chain=chain)
c = Client(onchain_provider=CoinbaseProvider(wallet=w, asset='usdc'))
try:
    c.pay(o)
except Exception as e:
    print(e)
Insufficient funds: have 0, need 1.

Fewsats Client


source

Fewsats

 Fewsats (api_key:str=None,
          base_url:str='https://hub-5n97k.ondigitalocean.app')

Initialize self. See help(type(self)) for accurate signature.

f = Fewsats()
f = Fewsats(base_url='http://localhost:8000', api_key='yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58')
f.get_payment_methods()
[{'id': 1,
  'last4': '4242',
  'brand': 'visa',
  'exp_month': 12,
  'exp_year': 2034,
  'is_default': False},
 {'id': 4,
  'last4': '4242',
  'brand': 'Visa',
  'exp_month': 12,
  'exp_year': 2034,
  'is_default': True}]
c = Client(fewsats_provider=Fewsats(base_url='http://localhost:8000', api_key='yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58'))
err = None
try:
    c.pay(o)
except Exception as e:
    print(e.response.text)
{'payment_request_url': 'https://hub-5n97k.ondigitalocean.app/v0/l402/payment-request', 'payment_context_token': 'a1700b1a-8325-47a9-902a-f90e82d21427', 'payment_method': 'onchain', 'offer': {'offer_id': 'f52bcccd-1057-4204-b1ae-01707bfe29af', 'amount': 1, 'currency': 'USD', 'description': 'Purchase 1 credit for API access', 'title': '1 Credit Package', 'type': 'one-time', 'payment_methods': ['onchain']}}
http://localhost:8000
yQbJPudW-nqiTyx880t8G83oLGyoR-RlnrLDmEe0D58
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/ninja/operation.py", line 341, in run
    result = await self.view_func(request, **values)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hub_api/l402/api/common/decorators.py", line 17, in wrapper
    return await func(request, *args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hub_api/l402/api/v0/views.py", line 32, in l402_create_purchase_from_offer_v0
    return await purchase_from_offer(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/asgiref/sync.py", line 518, in thread_handler
    raise exc_info[1]
  File "/opt/hub_api/common/decorators/aatomic.py", line 48, in wrapper
    return await fun(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hub_api/l402/workflow.py", line 424, in purchase_from_offer
    return await purchase_with_onchain(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/asgiref/sync.py", line 518, in thread_handler
    raise exc_info[1]
  File "/opt/hub_api/common/decorators/aatomic.py", line 48, in wrapper
    return await fun(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/hub_api/l402/workflow.py", line 496, in purchase_with_onchain
    transfer = wallet.transfer(
               ^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/cdp/wallet.py", line 466, in transfer
    return self.default_address.transfer(amount, asset_id, destination, gasless, skip_batching)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/cdp/wallet_address.py", line 122, in transfer
    self._ensure_sufficient_balance(normalized_amount, asset_id)
  File "/usr/local/lib/python3.12/site-packages/cdp/wallet_address.py", line 463, in _ensure_sufficient_balance
    raise InsufficientFundsError(expected=amount, exact=current_balance)
cdp.errors.InsufficientFundsError: Insufficient funds: have 0.499997, need 1.
err.response.json()
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
Cell In[18], line 1
----> 1 err.response.json()

AttributeError: 'NoneType' object has no attribute 'response'
try:
    f.pay(o)
except Exception as e:
    print(e)
name 'f' is not defined
f = Fewsats()
try:
    f._pay_onchain(address=r['payment_request']['address'], amount="0.000001", chain='base-mainnet', asset='usdc')
except Exception as e:
    print(e)
{'address': '0x114113186ca748Ea629A08B762df34a93f4C7e64', 'amount': '0.000001', 'chain': 'base-mainnet', 'asset': 'usdc'}
The read operation timed out
o, r
({'offers': [{'amount': 1,
    'currency': 'USD',
    'description': 'Purchase 1 credit for API access',
    'offer_id': '33d08b4a-1589-4519-a24f-ba67e2166f0b',
    'payment_methods': ['onchain'],
    'title': '1 Credit Package',
    'type': 'one-time'}],
  'payment_context_token': 'd5607f07-3ecd-4f84-a9f9-74db7f922760',
  'payment_request_url': 'http://localhost:9000/payment_request',
  'version': '0.2.2'},
 {'expires_at': '2025-01-25T18:21:42.339161+00:00',
  'offer_id': '33d08b4a-1589-4519-a24f-ba67e2166f0b',
  'payment_request': {'address': '0xAa4b26Ca04692E6cAA310Dc05Feaf1dE75943d62',
   'chain': 'base-mainnet',
   'asset': 'usdc'},
  'version': '0.2.2'})