Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ dev = [
"build>=1.0",
]

[project.scripts]
create-x402-wallet = "x402_openai._cli:main"

[project.urls]
Homepage = "https://github.com/qntx/x402-openai-python"
Repository = "https://github.com/qntx/x402-openai-python"
Expand Down
21 changes: 20 additions & 1 deletion src/x402_openai/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@
policies=[prefer_network("eip155:8453")],
)

# Generic HTTP (any endpoint)
session = create_x402_session(wallet=EvmWallet(private_key="0x…"))
resp = session.post("https://apibase.pro/api/v1/tools/weather/call",
json={"city": "Tokyo"})

# MCP JSON-RPC
with X402MCPClient(wallet=EvmWallet(private_key="0x…"),
base_url="https://apibase.pro") as mcp:
result = mcp.call("/mcp/v1", "tools/call",
params={"name": "weather", "arguments": {"city": "Tokyo"}})

Public API:

- :class:`X402OpenAI` / :class:`AsyncX402OpenAI` — recommended client classes.
- :class:`X402OpenAI` / :class:`AsyncX402OpenAI` — OpenAI drop-in clients.
- :func:`create_x402_session` / :func:`create_async_x402_session` — generic httpx clients.
- :class:`X402MCPClient` / :class:`AsyncX402MCPClient` — MCP JSON-RPC clients.
- :class:`X402Transport` / :class:`AsyncX402Transport` — low-level transports.
- :func:`prefer_network` / :func:`prefer_scheme` / :func:`max_amount` — payment policies.
- :mod:`x402_openai.wallets` — chain-specific wallet adapters.
Expand All @@ -31,17 +44,23 @@
from __future__ import annotations

from x402_openai._client import AsyncX402OpenAI, X402OpenAI
from x402_openai._http import create_async_x402_session, create_x402_session
from x402_openai._mcp import AsyncX402MCPClient, X402MCPClient
from x402_openai._transport import AsyncX402Transport, X402Transport
from x402_openai.wallets import EvmWallet, SvmWallet, Wallet

__all__ = [
"AsyncX402MCPClient",
"AsyncX402OpenAI",
"AsyncX402Transport",
"EvmWallet",
"SvmWallet",
"Wallet",
"X402MCPClient",
"X402OpenAI",
"X402Transport",
"create_async_x402_session",
"create_x402_session",
# Lazily re-exported from x402 SDK.
"max_amount",
"prefer_network",
Expand Down
60 changes: 60 additions & 0 deletions src/x402_openai/_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""CLI: generate an x402 EVM wallet keypair.

Creates a new keypair, saves it to ``~/.x402/wallet.json``, and prints the
funding address. Permissions are set to ``0o600`` so only the owner can
read the private key.

Usage::

create-x402-wallet # via installed entry-point
python -m x402_openai # directly from the package
"""

from __future__ import annotations

import json
import pathlib
import sys


def main() -> None:
"""Entry-point for the ``create-x402-wallet`` command."""
try:
from eth_account import Account
except ImportError:
print(
"eth-account is required. Install with:\n pip install x402-openai[evm]",
file=sys.stderr,
)
sys.exit(1)

wallet_dir = pathlib.Path.home() / ".x402"
wallet_dir.mkdir(parents=True, exist_ok=True)
wallet_file = wallet_dir / "wallet.json"

if wallet_file.exists():
print(
f"Wallet already exists at {wallet_file}\n"
"Delete it manually before generating a new one.",
file=sys.stderr,
)
sys.exit(1)

account = Account.create()
data = {
"address": account.address,
"private_key": account.key.hex(),
"chain": "eip155",
}
wallet_file.write_text(json.dumps(data, indent=2))
wallet_file.chmod(0o600)

print(f"Wallet created : {wallet_file}")
print(f"Address : {account.address}")
print()
print("Fund this address with USDC on Base (chain ID 8453) to use x402.")
print("Minimum recommended balance: 1 USDC")


if __name__ == "__main__":
main()
109 changes: 109 additions & 0 deletions src/x402_openai/_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""Generic x402-aware httpx client factories.

Drop-in replacements for ``httpx.Client`` / ``httpx.AsyncClient`` that
intercept HTTP 402 responses and automatically handle payment — framework
agnostic, works with any HTTP endpoint.

Examples
--------
Synchronous::

from x402_openai import create_x402_session
from x402_openai.wallets import EvmWallet

client = create_x402_session(wallet=EvmWallet(private_key="0x…"))
resp = client.post("https://apibase.pro/api/v1/tools/weather/call",
json={"city": "Tokyo"})

Asynchronous::

from x402_openai import create_async_x402_session
from x402_openai.wallets import EvmWallet

async with create_async_x402_session(wallet=EvmWallet(private_key="0x…")) as client:
resp = await client.post("https://apibase.pro/api/v1/tools/weather/call",
json={"city": "Tokyo"})
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

import httpx

from x402_openai._transport import AsyncX402Transport, X402Transport
from x402_openai._wallet import create_x402_http_client

if TYPE_CHECKING:
from x402_openai.wallets._base import Wallet


def create_x402_session(
*,
wallet: Wallet | None = None,
wallets: list[Wallet] | None = None,
x402_client: Any = None,
policies: list[Any] | None = None,
**httpx_kwargs: Any,
) -> httpx.Client:
"""Return a synchronous ``httpx.Client`` with automatic x402 payment handling.

Any request that receives a ``402 Payment Required`` response is
automatically paid and retried — the caller sees only the final 200.

Parameters
----------
wallet:
Single chain wallet adapter.
wallets:
List of adapters for multi-chain support.
x402_client:
Pre-configured ``x402HTTPClientSync`` (takes precedence).
policies:
Optional x402 policy list (e.g. ``prefer_network``, ``max_amount``).
**httpx_kwargs:
Forwarded verbatim to ``httpx.Client()``.
"""
x402 = create_x402_http_client(
wallet=wallet,
wallets=wallets,
x402_client=x402_client,
policies=policies,
sync=True,
)
return httpx.Client(transport=X402Transport(x402), **httpx_kwargs)


def create_async_x402_session(
*,
wallet: Wallet | None = None,
wallets: list[Wallet] | None = None,
x402_client: Any = None,
policies: list[Any] | None = None,
**httpx_kwargs: Any,
) -> httpx.AsyncClient:
"""Return an asynchronous ``httpx.AsyncClient`` with automatic x402 payment handling.

Same as :func:`create_x402_session` but async.

Parameters
----------
wallet:
Single chain wallet adapter.
wallets:
List of adapters for multi-chain support.
x402_client:
Pre-configured ``x402HTTPClient`` (takes precedence).
policies:
Optional x402 policy list.
**httpx_kwargs:
Forwarded verbatim to ``httpx.AsyncClient()``.
"""
x402 = create_x402_http_client(
wallet=wallet,
wallets=wallets,
x402_client=x402_client,
policies=policies,
sync=False,
)
return httpx.AsyncClient(transport=AsyncX402Transport(x402), **httpx_kwargs)
Loading