Skip to content
Draft
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
4 changes: 4 additions & 0 deletions examples/python/.env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
CDP_API_KEY_ID=your_api_key_id_here
CDP_API_KEY_SECRET=your_private_key_here
CDP_WALLET_SECRET=your_wallet_auth_key_here

# Receiver addresses for the x402 Bazaar server examples
EVM_ADDRESS=your_evm_address_here
SVM_ADDRESS=your_solana_address_here
2 changes: 2 additions & 0 deletions examples/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies = [
"base58>=2.1.1",
"pydantic==2.11.3",
"dotenv>=0.9.9",
"x402[evm,svm,fastapi,mcp]>=2.16.0",
"uvicorn[standard]>=0.30.0",
]

[tool.uv]
Expand Down
1,667 changes: 1,567 additions & 100 deletions examples/python/uv.lock

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions examples/python/x402/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# x402 examples

These examples combine the CDP SDK with the x402 Foundation SDK:

- Clients use CDP-managed wallets to sign payments.
- Servers use `cdp.x402.create_facilitator_config()` to verify and settle through the CDP
Facilitator.

Run commands from `examples/python` after completing the setup in the
[parent README](../README.md).

## Examples

- **Bazaar server:** `servers/bazaar.py` runs a paid HTTP API and declares discovery metadata
so its routes can be indexed in the CDP Bazaar.
- **MCP server:** `servers/mcp/server.py` exposes free and paid MCP tools.
- **MCP client:** `clients/mcp/simple.py` calls the MCP server and pays for its paid tool with
a CDP-managed wallet.

Use the Bazaar example when you are building a discoverable HTTP resource. Use the MCP pair
when you are building or calling paid MCP tools.

## Run the Bazaar server

Set `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, `EVM_ADDRESS`, and `SVM_ADDRESS` in `.env`, then
run:

```bash
uv run python x402/servers/bazaar.py
```

The server listens on `http://localhost:4021`.

## Run the MCP examples

Set `CDP_API_KEY_ID`, `CDP_API_KEY_SECRET`, and `CDP_WALLET_SECRET` in `.env`.

Start the server:

```bash
uv run python x402/servers/mcp/server.py
```

In another terminal, run the client:

```bash
uv run python x402/clients/mcp/simple.py
```

The MCP server provisions a CDP-managed receiver wallet. Set `PAY_TO` to use an existing EVM
address instead. To fund the client's Base Sepolia wallet automatically, set
`CDP_FUND_FROM_FAUCET=true`.
63 changes: 63 additions & 0 deletions examples/python/x402/clients/mcp/simple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Usage: uv run python x402/clients/mcp/simple.py

"""Call x402-paid MCP tools with a CDP-managed wallet.

The signer is a CDP Server Wallet exposed through eth_account's LocalAccount
interface via EvmLocalAccount, registered onto a standard x402Client -- no
private keys. create_x402_mcp_client runs the 402 -> pay -> retry loop for you.

Setup:
1. Start the server: uv run python x402/servers/mcp/server.py
2. Set CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET in examples/python/.env.
3. Fund the printed address with USDC on Base Sepolia, or set
CDP_FUND_FROM_FAUCET=true to self-fund on first run.

Run:
uv run python x402/clients/mcp/simple.py
"""

import asyncio
import os

from cdp import CdpClient
from cdp.evm_local_account import EvmLocalAccount
from dotenv import load_dotenv
from x402 import x402Client
from x402.mcp import create_x402_mcp_client
from x402.mechanisms.evm.exact import ExactEvmScheme

load_dotenv()

SERVER_URL = os.getenv("MCP_SERVER_URL", "http://localhost:4022")
NETWORK = "eip155:84532" # Base Sepolia


async def main() -> None:
async with CdpClient() as cdp:
account = await cdp.evm.get_or_create_account(name="x402-client-wallet-1")
signer = EvmLocalAccount(account)
print(f"Paying from {signer.address}")

if os.getenv("CDP_FUND_FROM_FAUCET", "").lower() == "true":
await cdp.evm.request_faucet(
address=signer.address, network="base-sepolia", token="usdc"
)

payment_client = x402Client()
payment_client.register(NETWORK, ExactEvmScheme(signer))

async with create_x402_mcp_client(payment_client, SERVER_URL) as mcp:
tools = (await mcp.list_tools()).tools
print("Tools:", ", ".join(t.name for t in tools))

ping = await mcp.call_tool("ping", {})
print(f"ping -> {ping.content[0].text}")

report = await mcp.call_tool("generate_report", {"topic": "USDC on Base"})
print(f"generate_report -> {report.content[0].text}")
if report.payment_response and report.payment_response.transaction:
print(f"Settled tx: {report.payment_response.transaction}")


if __name__ == "__main__":
asyncio.run(main())
111 changes: 111 additions & 0 deletions examples/python/x402/servers/bazaar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Usage: uv run python x402/servers/bazaar.py

"""Bazaar-discoverable x402 server, powered by the CDP SDK.

The standard x402 Foundation server stack with one CDP swap: the facilitator is
``HTTPFacilitatorClient(create_facilitator_config())`` -- the CDP hosted
facilitator. Settling through it is what indexes a route in the CDP Bazaar.

Python's CDP SDK has no ``createX402Server`` (unlike TypeScript), so discovery
metadata is declared with the Foundation ``declare_discovery_extension`` helper.

Setup: set CDP_API_KEY_ID and CDP_API_KEY_SECRET (facilitator auth) plus
EVM_ADDRESS and SVM_ADDRESS (payment receivers) in examples/python/.env.

Run: uv run python x402/servers/bazaar.py # http://localhost:4021
"""

import os

from cdp.x402 import create_facilitator_config
from dotenv import load_dotenv
from fastapi import FastAPI
from x402.extensions.bazaar import (
OutputConfig,
bazaar_resource_server_extension,
declare_discovery_extension,
)
from x402.http import HTTPFacilitatorClient, PaymentOption
from x402.http.middleware.fastapi import PaymentMiddlewareASGI
from x402.http.types import RouteConfig
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.mechanisms.svm.exact import ExactSvmServerScheme
from x402.schemas import Network
from x402.server import x402ResourceServer

load_dotenv()

EVM_ADDRESS = os.getenv("EVM_ADDRESS")
SVM_ADDRESS = os.getenv("SVM_ADDRESS")
EVM_NETWORK: Network = "eip155:84532" # Base Sepolia
SVM_NETWORK: Network = "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" # Solana Devnet

if not EVM_ADDRESS or not SVM_ADDRESS:
raise ValueError(
"Set EVM_ADDRESS and SVM_ADDRESS to the payment receiver addresses."
)

# CDP swap: create_facilitator_config() reads CDP_API_KEY_ID / CDP_API_KEY_SECRET
# and authenticates verify/settle against the CDP hosted facilitator.
server = x402ResourceServer(HTTPFacilitatorClient(create_facilitator_config()))
server.register(EVM_NETWORK, ExactEvmServerScheme())
server.register(SVM_NETWORK, ExactSvmServerScheme())
# Enriches each route's Bazaar declaration with its HTTP method and path params.
server.register_extension(bazaar_resource_server_extension)

payment_options = [
PaymentOption(
scheme="exact", pay_to=EVM_ADDRESS, price="$0.01", network=EVM_NETWORK
),
PaymentOption(
scheme="exact", pay_to=SVM_ADDRESS, price="$0.01", network=SVM_NETWORK
),
]

# declare_discovery_extension makes the route discoverable in the Bazaar. Its
# routeTemplate (":city") collapses every concrete URL into one catalog entry.
routes = {
"GET /weather/:city": RouteConfig(
accepts=payment_options,
mime_type="application/json",
description="Current weather conditions for a city",
extensions=declare_discovery_extension(
path_params_schema={
"properties": {
"city": {"type": "string", "description": "City name slug"}
},
"required": ["city"],
},
output=OutputConfig(
example={"city": "san-francisco", "weather": "foggy", "temperature": 60}
),
),
),
}

app = FastAPI()
app.add_middleware(PaymentMiddlewareASGI, routes=routes, server=server)


@app.get("/health")
async def health_check() -> dict[str, str]:
return {"status": "ok"}


@app.get("/weather/{city}")
async def get_weather(city: str) -> dict:
conditions = {
"san-francisco": {"weather": "foggy", "temperature": 60},
"new-york": {"weather": "cloudy", "temperature": 55},
"tokyo": {"weather": "rainy", "temperature": 65},
}
return {
"city": city,
**conditions.get(city, {"weather": "sunny", "temperature": 70}),
}


if __name__ == "__main__":
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=4021)
95 changes: 95 additions & 0 deletions examples/python/x402/servers/mcp/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Usage: uv run python x402/servers/mcp/server.py

"""MCP server with x402-paid tools, using the CDP facilitator and a CDP-managed
receiver wallet.

Tools (over SSE):
- generate_report (paid, $0.01)
- ping (free)

Setup:
Set CDP_API_KEY_ID, CDP_API_KEY_SECRET, CDP_WALLET_SECRET in examples/python/.env,
or set PAY_TO to an EVM address to skip provisioning a receiver wallet.

Run:
uv run python x402/servers/mcp/server.py # http://localhost:4022
"""

import asyncio
import os

from cdp import CdpClient
from cdp.x402 import create_facilitator_config
from dotenv import load_dotenv
from mcp.server.fastmcp import FastMCP
from x402.http import HTTPFacilitatorClientSync
from x402.mcp import create_payment_wrapper
from x402.mechanisms.evm.exact import ExactEvmServerScheme
from x402.schemas import ResourceConfig
from x402.server import x402ResourceServerSync

load_dotenv()

PORT = int(os.getenv("PORT", "4022"))
NETWORK = "eip155:84532" # Base Sepolia


async def resolve_pay_to() -> str:
"""Return PAY_TO, else a CDP-managed Server Wallet address."""
pay_to = os.getenv("PAY_TO")
if pay_to:
return pay_to
async with CdpClient() as cdp:
account = await cdp.evm.get_or_create_account(name="x402-mcp-receiver-wallet-1")
return account.address


def generate_report(topic: str) -> str:
"""Mock report generator. Swap for your real data source."""
return (
f'AI report on "{topic}": demand is trending up, '
"sentiment is positive, no anomalies detected."
)


def main() -> None:
pay_to = asyncio.run(resolve_pay_to())

resource_server = x402ResourceServerSync(
HTTPFacilitatorClientSync(create_facilitator_config()) # CDP hosted facilitator
)
resource_server.register(NETWORK, ExactEvmServerScheme())
resource_server.initialize()

accepts = resource_server.build_payment_requirements(
ResourceConfig(
scheme="exact",
network=NETWORK,
pay_to=pay_to,
price="$0.01",
extra={"name": "USDC", "version": "2"},
)
)
paid = create_payment_wrapper(resource_server, accepts=accepts)

mcp_server = FastMCP("x402 CDP Report Service", host="0.0.0.0", port=PORT)

@mcp_server.tool(
name="generate_report",
description="Generate an AI report on a topic. Requires payment of $0.01 USDC.",
)
@paid
async def generate_report_tool(topic: str) -> str:
return generate_report(topic)

@mcp_server.tool(name="ping", description="A free health check tool")
async def ping() -> str:
return "pong"

print(f"x402 CDP MCP server running on http://localhost:{PORT}")
print(f" Receiving payments at {pay_to}")
mcp_server.run(transport="sse")


if __name__ == "__main__":
main()
37 changes: 37 additions & 0 deletions examples/typescript/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/typescript/pnpm-workspace.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
packages:
- "../../typescript/packages/cdp-sdk"
- "./x402/servers/bazaar"
- "./x402/servers/express"
- "./x402/servers/hono"
- "./x402/servers/next"
Expand Down
Loading
Loading