diff --git a/pyproject.toml b/pyproject.toml index b20d792..7962609 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "setuptools>=21.0.0", "Flask>=3.0.0", "gunicorn>=23.0.0", - "og-x402[evm]==0.0.2.dev8", + "og-x402[evm,flask,extensions]==2.15.0", "fastapi>=0.128.0", "uvicorn[standard]>=0.40.0", "pydantic>=2.12.5", diff --git a/tee_gateway/__main__.py b/tee_gateway/__main__.py index 3b317db..e313db6 100644 --- a/tee_gateway/__main__.py +++ b/tee_gateway/__main__.py @@ -24,6 +24,8 @@ from tee_gateway.llm_backend import get_provider_config, set_provider_config from tee_gateway.heartbeat import create_heartbeat_service from tee_gateway.controllers.ohttp_controller import ( + OHTTP_BATCH_SETTLEMENT_BOUNDARY, + build_ohttp_batch_settlement_frame, create_anonymous_chat_completion, get_hpke_config, ) @@ -31,15 +33,19 @@ from x402.http import FacilitatorConfig, HTTPFacilitatorClientSync, PaymentOption from x402.http.middleware.flask import payment_middleware from x402.http.types import RouteConfig -from x402.mechanisms.evm.exact import ExactEvmServerScheme -from x402.mechanisms.evm.upto import UptoEvmServerScheme +from x402.mechanisms.evm.batch_settlement.server import ( + AutoSettlementConfig, + BatchSettlementChannelManagerSync, + BatchSettlementEvmScheme, + BatchSettlementEvmSchemeServerConfig, + ChannelManagerConfigSync, + FileChannelStorage, +) from x402.extensions.erc20_approval_gas_sponsoring import ( declare_erc20_approval_gas_sponsoring_extension, ) from x402.schemas import AssetAmount from x402.server import x402ResourceServerSync -from x402.session import SessionStore -import x402.http.middleware.flask as x402_flask from .model_registry import get_model_config from .price_feed import OPGPriceFeed, set_price_feed @@ -89,6 +95,18 @@ # --------------------------------------------------------------------------- _heartbeat_service = None +# The gateway has to retain the latest signed voucher for every channel in +# order to claim it later. This directory must be mounted on persistent +# storage in production; an in-memory store would lose claimable vouchers on +# a gateway restart. +_batch_settlement_storage_dir = os.path.expanduser( + os.getenv( + "X402_BATCH_SETTLEMENT_STORAGE_DIR", + "~/.tee-gateway/x402-batch-channels", + ) +) +_batch_settlement_manager = None + def _init_heartbeat(heartbeat_config: HeartbeatConfig | None): """Create and start the heartbeat service if a HeartbeatConfig is provided.""" @@ -115,6 +133,21 @@ def _shutdown_heartbeat(): atexit.register(_shutdown_heartbeat) +def _shutdown_batch_settlement_manager(): + """Flush claimable vouchers before the gateway process exits.""" + manager = _batch_settlement_manager + if manager is None: + return + try: + logger.info("Shutting down batch-settlement manager; flushing claims") + manager.stop(flush=True) + except Exception: + logger.exception("Failed to flush batch-settlement claims during shutdown") + + +atexit.register(_shutdown_batch_settlement_manager) + + # --------------------------------------------------------------------------- # OPG price feed — start before x402 middleware so the first request can be # priced correctly. Runs as a daemon thread; no cleanup needed on exit. @@ -136,92 +169,7 @@ def _gateway_version() -> str: _active_facilitator_url: str | None = None -# --------------------------------------------------------------------------- -# x402 read-body patch -# -# Ensures that non-payment 0-length requests can bypass the middleware without -# errors. Applied at module load so it is in place before the middleware -# instance is created at injection time. -# --------------------------------------------------------------------------- -_original_read_body_bytes = x402_flask._read_body_bytes - - -def _patched_read_body_bytes(environ): - try: - content_length = int(environ.get("CONTENT_LENGTH") or 0) - except (ValueError, TypeError): - content_length = 0 - - if content_length <= 0: - return b"" - - return _original_read_body_bytes(environ) - - -x402_flask._read_body_bytes = _patched_read_body_bytes - - -def _patched_stream_session_response( - self, - environ, - start_response, - context, - session_id, - payment_payload, - payment_requirements, -): - """Expose x402's per-request cost context to Flask route handlers. - - OHTTP requests arrive as ciphertext and return ciphertext, so the x402 - middleware cannot parse request_json/response_json from the outer HTTP - bodies. The OHTTP controller decrypts the inner request and plaintext - response inside the enclave; this patch gives it a request-local dict where - it can attach those inner JSON objects for dynamic settlement. - """ - self._start_reaper() - - request_body_bytes = x402_flask._read_body_bytes(environ) - request_json = x402_flask._try_parse_json(request_body_bytes) - parsed_request_json = ( - request_json if isinstance(request_json, (dict, list)) else None - ) - - x402_flask.g.payment_payload = payment_payload - x402_flask.g.payment_requirements = payment_requirements - x402_flask.g.x402_session_id = session_id - - cost_context = { - "method": context.method, - "path": context.path, - "request_body_bytes": request_body_bytes, - "request_json": parsed_request_json, - "payment_payload": payment_payload, - "payment_requirements": payment_requirements, - } - environ["x402.cost_context"] = cost_context - - status_capture = x402_flask.StatusCapture(start_response) - status_capture.add_header(x402_flask.UPTO_SESSION_HEADER, session_id) - - upstream_iter = self._original_wsgi(environ, status_capture) - - return x402_flask.StreamingSessionResponse( - upstream_iter, - middleware=self, - session_id=session_id, - cost_context=cost_context, - status_ref=status_capture, - ) - - -setattr( - x402_flask.PaymentMiddleware, - "_stream_session_response", - _patched_stream_session_response, -) - - -def _session_cost_calculator(ctx: dict) -> int: +def _batch_settlement_cost_calculator(ctx: dict) -> int: # The chat/completions controllers compute cost in-band and embed it on # the response as a SessionCost model BEFORE returning. We parse it back # here — single source of truth for cost lives in the controller, not @@ -229,9 +177,8 @@ def _session_cost_calculator(ctx: dict) -> int: # (which charges them). # # If the controller couldn't compute cost (e.g. missing usage from the - # provider), the block is absent — SessionCost.model_validate raises and - # x402's close() swallows it so the client is not charged. The controller - # has already logged CRITICAL in that case. + # provider), the block is absent and settlement must fail rather than + # commit a voucher for an unknown amount. from .pricing import SessionCost if ctx.get("path") == "/v1/ohttp": @@ -267,18 +214,61 @@ def _init_payment_middleware(facilitator_url: str) -> None: Called once from set_provider_keys() after the facilitator URL is known. Swaps application.wsgi_app so all subsequent requests flow through it. """ + global _batch_settlement_manager + facilitator = HTTPFacilitatorClientSync(FacilitatorConfig(url=facilitator_url)) + scheme = BatchSettlementEvmScheme( + EVM_PAYMENT_ADDRESS, + BatchSettlementEvmSchemeServerConfig( + storage=FileChannelStorage(_batch_settlement_storage_dir), + ), + ) server = x402ResourceServerSync(facilitator) # type: ignore[arg-type] - store = SessionStore() - - server.register(BASE_MAINNET_NETWORK, ExactEvmServerScheme()) - server.register(BASE_MAINNET_NETWORK, UptoEvmServerScheme()) + server.register(BASE_MAINNET_NETWORK, scheme) + + # Claims are asynchronous with respect to inference. Voucher requests + # only update the durable channel record; this background manager submits + # batched claim and settle transactions through the facilitator. + _batch_settlement_manager = BatchSettlementChannelManagerSync( + ChannelManagerConfigSync( + scheme=scheme, + facilitator=facilitator, + receiver=EVM_PAYMENT_ADDRESS, + token=BASE_MAINNET_OPG_ADDRESS, + network=BASE_MAINNET_NETWORK, + ) + ) + _batch_settlement_manager.start( + AutoSettlementConfig( + claim_interval_secs=60, + settle_interval_secs=120, + max_claims_per_batch=100, + on_claim=lambda result: logger.info( + "Claimed %s batch-settlement voucher(s): %s", + result.vouchers, + result.transaction, + ), + on_settle=lambda result: logger.info( + "Settled batch-settlement claims: %s", + result.transaction, + ), + on_error=lambda error: logger.error( + "Batch-settlement channel-manager error: %s", error + ), + ) + ) + logger.info( + "Batch-settlement channel manager started " + "(storage=%s, token=%s, claim_interval=60s, settle_interval=120s)", + _batch_settlement_storage_dir, + BASE_MAINNET_OPG_ADDRESS, + ) routes = { "POST /v1/chat/completions": RouteConfig( accepts=[ PaymentOption( - scheme="upto", + scheme="batch-settlement", pay_to=EVM_PAYMENT_ADDRESS, price=AssetAmount( amount=CHAT_COMPLETIONS_OPG_SESSION_MAX_SPEND, @@ -301,7 +291,7 @@ def _init_payment_middleware(facilitator_url: str) -> None: "POST /v1/completions": RouteConfig( accepts=[ PaymentOption( - scheme="upto", + scheme="batch-settlement", pay_to=EVM_PAYMENT_ADDRESS, price=AssetAmount( amount=COMPLETIONS_OPG_SESSION_MAX_SPEND, @@ -324,7 +314,7 @@ def _init_payment_middleware(facilitator_url: str) -> None: "POST /v1/ohttp": RouteConfig( accepts=[ PaymentOption( - scheme="upto", + scheme="batch-settlement", pay_to=EVM_PAYMENT_ADDRESS, price=AssetAmount( amount=OHTTP_OPG_SESSION_MAX_SPEND, @@ -356,13 +346,14 @@ def _init_payment_middleware(facilitator_url: str) -> None: application, routes=routes, server=server, - session_store=store, - cost_per_request=100000000000000, # static precheck/fallback estimate - session_idle_timeout=100, - session_cost_calculator=_session_cost_calculator, + streaming_cost_calculator=_batch_settlement_cost_calculator, + settlement_data_enabled=True, + streaming_settlement_boundary=OHTTP_BATCH_SETTLEMENT_BOUNDARY, + streaming_receipt_encoder=build_ohttp_batch_settlement_frame, ) logger.info( - "x402 payment middleware initialized with facilitator: %s", facilitator_url + "x402 batch-settlement middleware initialized with facilitator: %s", + facilitator_url, ) diff --git a/tee_gateway/controllers/ohttp_controller.py b/tee_gateway/controllers/ohttp_controller.py index 463ca83..96b6d7b 100644 --- a/tee_gateway/controllers/ohttp_controller.py +++ b/tee_gateway/controllers/ohttp_controller.py @@ -24,11 +24,9 @@ model name, no token counts: those would be a fingerprint of the inner request and have no role in billing. * stream=true: outer response headers are flushed before any body chunk - is yielded, so cost isn't known at header-write time. The relay reads - the settled amount from x402: either by querying the facilitator with - its X-Upto-Session, or via X-Payment-Response on its next request. The - client still sees cost in the final SSE event inside the encrypted - stream (the ``opengradient`` block written by the chat controller). + is yielded, so final cost and the batch voucher receipt are carried in a + private plaintext billing frame immediately before the final sealed OHTTP + chunk. The relay strips that frame before forwarding bytes to the client. Trust / payment model: * The CLIENT encrypts only an LLM chat-completion request. It does not see, @@ -85,6 +83,10 @@ OHTTP_RESPONSE_MEDIA_TYPE = "message/ohttp-res" OHTTP_CHUNKED_RESPONSE_MEDIA_TYPE = "message/ohttp-chunked-res" OHTTP_BILLING_FRAME_MAGIC = b"\n--opengradient-ohttp-billing-v1--\n" +# This marker is never sent on the wire. The x402 Flask middleware consumes it +# after the final cost is known, settles the batch voucher, and replaces it +# with the billing frame above before the final sealed OHTTP chunk is yielded. +OHTTP_BATCH_SETTLEMENT_BOUNDARY = b"\n--opengradient-ohttp-batch-settlement-boundary-v1--\n" _SSE_CONTENT_TYPE = "text/event-stream" # Cap on the encapsulated request size. The inner payload is a chat-completion @@ -282,9 +284,12 @@ def _stream() -> Iterator[bytes]: response_json=response_json, status_code=status, ) - billing_frame = _build_billing_frame(response_json) - if billing_frame: - yield billing_frame + # The outer x402 middleware consumes this internal boundary, + # settles the batch voucher using the cost context above, and + # injects the private billing/receipt frame in its place. This + # must remain before the final OHTTP chunk so the relay can strip + # it while preserving valid chunked-OHTTP framing for the browser. + yield OHTTP_BATCH_SETTLEMENT_BOUNDARY # Always emit exactly one final chunk so the AAD=b"final" # marker is present — that's what protects clients from @@ -351,17 +356,28 @@ def _extract_cost_headers(body_bytes: bytes) -> dict[str, str]: } -def _build_billing_frame(response_json: dict[str, Any] | None) -> bytes: - """Build the private gateway-to-relay billing frame for streaming OHTTP. +def build_ohttp_batch_settlement_frame( + receipt: dict[str, Any], + streaming_cost_context: dict[str, Any] | None, +) -> bytes: + """Build the private gateway-to-relay batch settlement frame. - This plaintext frame carries only the same billing fields projected as - outer headers for non-streaming OHTTP. The relay strips it before forwarding - bytes to the browser. + This plaintext frame carries the same billing fields projected as outer + headers for non-streaming OHTTP plus the encoded x402 payment response. + The relay strips it before forwarding bytes to the browser and persists + the voucher receipt in its SDK channel storage. """ - if not isinstance(response_json, dict): - return b"" + response_json = ( + streaming_cost_context.get("inner_response_json") + if isinstance(streaming_cost_context, dict) + else None + ) + payload: dict[str, str] = {} try: + if not isinstance(response_json, dict): + raise ValueError("streaming response cost context is missing") cost = SessionCost.model_validate(response_json.get("opengradient")) + payload.update(cost.model_dump(mode="json")) except Exception as exc: logger.warning( "OHTTP billing frame skipped — cost block missing/invalid on " @@ -369,9 +385,20 @@ def _build_billing_frame(response_json: dict[str, Any] | None) -> bytes: type(exc).__name__, exc, ) + if receipt.get("success"): + payment_response = receipt.get("paymentResponse") + if isinstance(payment_response, str) and payment_response: + payload["payment_response"] = payment_response + else: + payload["payment_error"] = "batch settlement response is missing" + else: + payload["payment_error"] = str( + receipt.get("error") or "batch settlement failed" + ) + if not payload: return b"" payload = json.dumps( - cost.model_dump(mode="json"), + payload, separators=(",", ":"), ).encode("utf-8") return OHTTP_BILLING_FRAME_MAGIC + len(payload).to_bytes(4, "big") + payload diff --git a/uv.lock b/uv.lock index 48b48e1..da248fe 100644 --- a/uv.lock +++ b/uv.lock @@ -5,6 +5,15 @@ requires-python = "==3.12.*" [options] prerelease-mode = "allow" +[[package]] +name = "abnf" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/b7/a1419e559705e68bb80c9dd9c3fcfce5b0b3cbecd07dd4b3e91a50f1888e/abnf-2.5.1.tar.gz", hash = "sha256:a186898445c9ed8f5fdda0b87eab6c7d7e4c9197e1c90e7dab1069758e7c867c", size = 421588, upload-time = "2026-06-14T20:07:10.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/7e/da9a4b954e5ad9ee0d2df2c8b324de4215c939916dfdb385d18c15689738/abnf-2.5.1-py3-none-any.whl", hash = "sha256:4013a03c0ebf3f090a72b2c0dbf24c839bdba25905e69ef425fd951cfdacf99a", size = 56877, upload-time = "2026-06-14T20:07:09.299Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.7.1" @@ -426,7 +435,7 @@ wheels = [ [[package]] name = "eth-account" -version = "0.14.0b1" +version = "0.13.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bitarray" }, @@ -440,9 +449,9 @@ dependencies = [ { name = "pydantic" }, { name = "rlp" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/81/06279eeb175d209698a25c57da3d1d5b50b84a9563aecb7a1d944d118cfd/eth_account-0.14.0b1.tar.gz", hash = "sha256:4ec5f909c57d46f50a6598ea0108f2484450bd246344355d800e2200d72b6b02", size = 4085511, upload-time = "2025-12-18T21:36:36.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/cf/20f76a29be97339c969fd765f1237154286a565a1d61be98e76bb7af946a/eth_account-0.13.7.tar.gz", hash = "sha256:5853ecbcbb22e65411176f121f5f24b8afeeaf13492359d254b16d8b18c77a46", size = 935998, upload-time = "2025-04-21T21:11:21.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/30/1b58e3e634798e58cc89578c000f11afb2eceb5e0d96b16424965fb8bfc8/eth_account-0.14.0b1-py3-none-any.whl", hash = "sha256:1c4b4311246cd3c61411d074c4e267656f41e54bf21f1b4722fab6a0844c4680", size = 587120, upload-time = "2025-12-18T21:36:30.46Z" }, + { url = "https://files.pythonhosted.org/packages/46/18/088fb250018cbe665bc2111974301b2d59f294a565aff7564c4df6878da2/eth_account-0.13.7-py3-none-any.whl", hash = "sha256:39727de8c94d004ff61d10da7587509c04d2dc7eac71e04830135300bdfc6d24", size = 587452, upload-time = "2025-04-21T21:11:18.346Z" }, ] [[package]] @@ -1301,16 +1310,16 @@ wheels = [ [[package]] name = "og-x402" -version = "0.0.2.dev8" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nest-asyncio" }, { name = "pydantic" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1a/1f/2a77d526d407fdecf9b6809f7f383cf36557240c825c1dc5dc7149ee0af0/og_x402-0.0.2.dev8.tar.gz", hash = "sha256:b8d6d7ce485c1cfd40285775585b770e9f124f0bb34fb11606dee45931109d53", size = 1319747, upload-time = "2026-07-09T08:01:09.654Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/50/132a0f868e3eddb3e82d64fa5279c9353dbf8dfc379fe23bcff6ce02d070/og_x402-2.15.0.tar.gz", hash = "sha256:027c3e9f5ec9d0152e19da74df3362431157b795e32cc53c6f87ce3eda2b082b", size = 2098954, upload-time = "2026-07-15T19:29:05.11Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/87/76/c57ffcaa56a792b3f3eac98fa29f1329c0fa17833c2e7a87ab7c924edf6e/og_x402-0.0.2.dev8-py3-none-any.whl", hash = "sha256:cc9951a11a54cf29593b251f67590527c9f7d6c361623af7a9d0cbdd942101b2", size = 1400026, upload-time = "2026-07-09T08:01:07.847Z" }, + { url = "https://files.pythonhosted.org/packages/6c/89/797cf9c81d63aa51019488523b1889e4f1a44aad56688013cf9959756d1d/og_x402-2.15.0-py3-none-any.whl", hash = "sha256:dae5d35cce48f0d50fac0cda7c9b997364896dac70008833b25eb988270ecbfd", size = 2230203, upload-time = "2026-07-15T19:29:01.522Z" }, ] [package.optional-dependencies] @@ -1321,6 +1330,15 @@ evm = [ { name = "eth-utils" }, { name = "web3" }, ] +extensions = [ + { name = "idna" }, + { name = "jsonschema" }, + { name = "pynacl" }, + { name = "signinwithethereum" }, +] +flask = [ + { name = "flask" }, +] [[package]] name = "openai" @@ -1603,6 +1621,29 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/f6/ff7df9e21b38ec1c827efd90c28b3bc76eddbfdf5a44aaf2fadb59a17cb9/pyhpke-0.6.4-py3-none-any.whl", hash = "sha256:abd0b2fec1424858399ffbed0d236fb7e9740dece9907f59ca40bd567d7fef78", size = 23792, upload-time = "2025-12-21T10:38:06.172Z" }, ] +[[package]] +name = "pynacl" +version = "1.6.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/9a/4019b524b03a13438637b11538c82781a5eda427394380381af8f04f467a/pynacl-1.6.2.tar.gz", hash = "sha256:018494d6d696ae03c7e656e5e74cdfd8ea1326962cc401bcf018f1ed8436811c", size = 3511692, upload-time = "2026-01-01T17:48:10.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/7b/4845bbf88e94586ec47a432da4e9107e3fc3ce37eb412b1398630a37f7dd/pynacl-1.6.2-cp38-abi3-macosx_10_10_universal2.whl", hash = "sha256:c949ea47e4206af7c8f604b8278093b674f7c79ed0d4719cc836902bf4517465", size = 388458, upload-time = "2026-01-01T17:32:16.829Z" }, + { url = "https://files.pythonhosted.org/packages/1e/b4/e927e0653ba63b02a4ca5b4d852a8d1d678afbf69b3dbf9c4d0785ac905c/pynacl-1.6.2-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8845c0631c0be43abdd865511c41eab235e0be69c81dc66a50911594198679b0", size = 800020, upload-time = "2026-01-01T17:32:18.34Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/d60984052df5c97b1d24365bc1e30024379b42c4edcd79d2436b1b9806f2/pynacl-1.6.2-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22de65bb9010a725b0dac248f353bb072969c94fa8d6b1f34b87d7953cf7bbe4", size = 1399174, upload-time = "2026-01-01T17:32:20.239Z" }, + { url = "https://files.pythonhosted.org/packages/68/f7/322f2f9915c4ef27d140101dd0ed26b479f7e6f5f183590fd32dfc48c4d3/pynacl-1.6.2-cp38-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:46065496ab748469cdd999246d17e301b2c24ae2fdf739132e580a0e94c94a87", size = 835085, upload-time = "2026-01-01T17:32:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d0/f301f83ac8dbe53442c5a43f6a39016f94f754d7a9815a875b65e218a307/pynacl-1.6.2-cp38-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a66d6fb6ae7661c58995f9c6435bda2b1e68b54b598a6a10247bfcdadac996c", size = 1437614, upload-time = "2026-01-01T17:32:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/c4/58/fc6e649762b029315325ace1a8c6be66125e42f67416d3dbd47b69563d61/pynacl-1.6.2-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:26bfcd00dcf2cf160f122186af731ae30ab120c18e8375684ec2670dccd28130", size = 818251, upload-time = "2026-01-01T17:32:25.69Z" }, + { url = "https://files.pythonhosted.org/packages/c9/a8/b917096b1accc9acd878819a49d3d84875731a41eb665f6ebc826b1af99e/pynacl-1.6.2-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c8a231e36ec2cab018c4ad4358c386e36eede0319a0c41fed24f840b1dac59f6", size = 1402859, upload-time = "2026-01-01T17:32:27.215Z" }, + { url = "https://files.pythonhosted.org/packages/85/42/fe60b5f4473e12c72f977548e4028156f4d340b884c635ec6b063fe7e9a5/pynacl-1.6.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:68be3a09455743ff9505491220b64440ced8973fe930f270c8e07ccfa25b1f9e", size = 791926, upload-time = "2026-01-01T17:32:29.314Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f9/e40e318c604259301cc091a2a63f237d9e7b424c4851cafaea4ea7c4834e/pynacl-1.6.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b097553b380236d51ed11356c953bf8ce36a29a3e596e934ecabe76c985a577", size = 1363101, upload-time = "2026-01-01T17:32:31.263Z" }, + { url = "https://files.pythonhosted.org/packages/48/47/e761c254f410c023a469284a9bc210933e18588ca87706ae93002c05114c/pynacl-1.6.2-cp38-abi3-win32.whl", hash = "sha256:5811c72b473b2f38f7e2a3dc4f8642e3a3e9b5e7317266e4ced1fba85cae41aa", size = 227421, upload-time = "2026-01-01T17:32:33.076Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/334600e8cacc7d86587fe5f565480fde569dfb487389c8e1be56ac21d8ac/pynacl-1.6.2-cp38-abi3-win_amd64.whl", hash = "sha256:62985f233210dee6548c223301b6c25440852e13d59a8b81490203c3227c5ba0", size = 239754, upload-time = "2026-01-01T17:32:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/29/7d/5945b5af29534641820d3bd7b00962abbbdfee84ec7e19f0d5b3175f9a31/pynacl-1.6.2-cp38-abi3-win_arm64.whl", hash = "sha256:834a43af110f743a754448463e8fd61259cd4ab5bbedcf70f9dabad1d28a394c", size = 184801, upload-time = "2026-01-01T17:32:36.309Z" }, +] + [[package]] name = "pyparsing" version = "3.3.2" @@ -1842,6 +1883,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5d/40/e1e72872c6354b306daef1703549e8e83b4d43cfea356311bf722a043752/setuptools-83.0.0-py3-none-any.whl", hash = "sha256:29b23c360f22f414dc7336bb39178cc7bcbf6021ed2733cde173f09dba19abb3", size = 1008090, upload-time = "2026-07-04T15:31:20.885Z" }, ] +[[package]] +name = "signinwithethereum" +version = "5.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "abnf" }, + { name = "eth-account" }, + { name = "eth-utils" }, + { name = "pydantic" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "web3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c6/9c/5bca1085418726e08c333b4b97891c2d03f12b99973418acf7357dd76a54/signinwithethereum-5.0.1.tar.gz", hash = "sha256:9661ac2020cb659cc7c3fc1f9b8a9ff59f2018e259f7a4e1ae641f126a475fa3", size = 165980, upload-time = "2026-04-24T12:16:09.533Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/22/ac218e503a9634757c09ab66e50a6dc3c9bfaaa877163a744516d33c85ce/signinwithethereum-5.0.1-py3-none-any.whl", hash = "sha256:53551a1073209f4f011589a957efb8603650af7be24433f966bf897405c0ebd3", size = 19342, upload-time = "2026-04-24T12:16:08.072Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -1908,7 +1967,7 @@ dependencies = [ { name = "langchain-google-genai" }, { name = "langchain-openai" }, { name = "langchain-xai" }, - { name = "og-x402", extra = ["evm"] }, + { name = "og-x402", extra = ["evm", "extensions", "flask"] }, { name = "openai" }, { name = "psutil" }, { name = "pydantic" }, @@ -1955,7 +2014,7 @@ requires-dist = [ { name = "langchain-google-genai", specifier = ">=4.2.1" }, { name = "langchain-openai", specifier = ">=1.1.12" }, { name = "langchain-xai", specifier = ">=1.2.2" }, - { name = "og-x402", extras = ["evm"], specifier = "==0.0.2.dev8" }, + { name = "og-x402", extras = ["evm", "flask", "extensions"], specifier = "==2.15.0" }, { name = "openai", specifier = ">=2.15.0" }, { name = "psutil", specifier = ">=7.2.1" }, { name = "pydantic", specifier = ">=2.12.5" }, @@ -2168,7 +2227,7 @@ wheels = [ [[package]] name = "web3" -version = "8.0.0b3" +version = "7.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, @@ -2186,9 +2245,9 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4e/36/8d333075fefd4b2cf48a25ccd69dfee33cd2d0c9308ef874ea5aec894819/web3-8.0.0b3.tar.gz", hash = "sha256:1c105258cd11ab9880c9cce680bfadfc519d2e52c0bac8cf648a9bde14ee9071", size = 2256686, upload-time = "2026-04-30T18:04:58.672Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/d9/bdfa9e715804020c3f3676346065c18adbc207c9343a3458246d7430f45c/web3-7.16.0.tar.gz", hash = "sha256:b4a75a3fa94fef4d23d502eb3c2244146ef9a1ee0082cf1cb0a91586ba0510c3", size = 2211469, upload-time = "2026-05-01T21:22:20.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/f3/28df26af36bf772d0cd34142889f029680118ddc58b3b531648862a27814/web3-8.0.0b3-py3-none-any.whl", hash = "sha256:42fbbb5133f309a5ce55402c00d7d17e703fffdd3daf88550848fd34dfdc02e1", size = 1405486, upload-time = "2026-04-30T18:04:55.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f9/5345c13f8469f3ce344b4d9934c0387b83a49420192482111e9c1fa95ec2/web3-7.16.0-py3-none-any.whl", hash = "sha256:760b2718c473980d70708c3593d9d28395db4b482f45e38a63a36fa028178f51", size = 1372181, upload-time = "2026-05-01T21:22:17.015Z" }, ] [[package]]