Skip to content

Commit ca9ee4d

Browse files
committed
initialize acp x402 server
1 parent 2daa42f commit ca9ee4d

File tree

15 files changed

+899
-102
lines changed

15 files changed

+899
-102
lines changed

.dockerignore

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Python
2+
__pycache__/
3+
*.py[cod]
4+
*$py.class
5+
*.so
6+
.Python
7+
*.egg-info/
8+
dist/
9+
build/
10+
*.egg
11+
12+
# Virtual environments
13+
venv/
14+
env/
15+
ENV/
16+
17+
# uv
18+
.venv/
19+
uv.lock
20+
21+
# IDE
22+
.vscode/
23+
.idea/
24+
*.swp
25+
*.swo
26+
*~
27+
28+
# Git
29+
.git/
30+
.gitignore
31+
.gitattributes
32+
33+
# Documentation
34+
*.md
35+
docs/
36+
37+
# Tests
38+
tests/
39+
test/
40+
*_test.py
41+
test_*.py
42+
43+
# CI/CD (except workflows we might need)
44+
.github/
45+
46+
# Node modules (for typescript examples)
47+
node_modules/
48+
pnpm-lock.yaml
49+
package-lock.json
50+
51+
# Build artifacts
52+
target/
53+
*.log
54+
*.pid
55+
56+
# OS
57+
.DS_Store
58+
Thumbs.db
59+
60+
# Environment files (will be provided at runtime)
61+
.env
62+
.env.*
63+
!.env.example
64+
65+
# TypeScript examples (not needed for Python deployment)
66+
typescript/
67+
examples/typescript/
68+
69+
# Go examples
70+
go/
71+
72+
# Java examples
73+
java/
74+
75+
# E2E tests
76+
e2e/
77+
78+
# Static assets (if not needed)
79+
static/
80+
81+
# Specs
82+
specs/
83+
84+
# Other examples we don't need
85+
examples/python/clients/
86+
examples/python/discovery/
87+
examples/python/fullstack/
88+
examples/python/servers/advanced/
89+
examples/python/servers/flask/
90+
examples/python/servers/mainnet/
91+

.github/workflows/deploy-dev.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: deploy_to_dev
2+
3+
on:
4+
workflow_dispatch:
5+
# inputs:
6+
# tags:
7+
# description: "Deploy to dev"
8+
push:
9+
branches: [dev,feat/acp-837]
10+
11+
jobs:
12+
deploy-dev:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Check out code
16+
uses: actions/checkout@v2
17+
18+
- name: Configure AWS credentials
19+
uses: aws-actions/configure-aws-credentials@v1
20+
with:
21+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
22+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
23+
aws-region: ap-southeast-1
24+
25+
- name: Login to Amazon ECR
26+
id: login-ecr
27+
uses: aws-actions/amazon-ecr-login@v1
28+
29+
- name: Build, tag, and push image to Amazon ECR
30+
env:
31+
ECR_REGISTRY: 099635904996.dkr.ecr.ap-southeast-1.amazonaws.com
32+
ECR_REPOSITORY: acp-x402-server
33+
IMAGE_TAG: dev
34+
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
35+
run: |
36+
docker build --build-arg NPM_REGISTRY_TOKEN=$NPM_REGISTRY_TOKEN -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
37+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
38+
39+
40+
# - name: Force deployment (dev-acp-x402-server)
41+
# run: |
42+
# aws ecs update-service --cluster dev-protocol-be --service dev-acp-x402-server --force-new-deployment

.github/workflows/deploy-prod.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: deploy_to_prod
2+
3+
on:
4+
workflow_dispatch:
5+
# inputs:
6+
# tags:
7+
# description: "Deploy to prod"
8+
push:
9+
branches: [main]
10+
11+
jobs:
12+
deploy-production:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- name: Check out code
16+
uses: actions/checkout@v2
17+
18+
- name: Configure AWS credentials
19+
uses: aws-actions/configure-aws-credentials@v1
20+
with:
21+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
22+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
23+
aws-region: ap-southeast-1
24+
25+
- name: Login to Amazon ECR
26+
id: login-ecr
27+
uses: aws-actions/amazon-ecr-login@v1
28+
29+
- name: Build, tag, and push image to Amazon ECR
30+
env:
31+
ECR_REGISTRY: 099635904996.dkr.ecr.ap-southeast-1.amazonaws.com
32+
ECR_REPOSITORY: acp-x402-server
33+
IMAGE_TAG: prod
34+
NPM_REGISTRY_TOKEN: ${{ secrets.NPM_REGISTRY_TOKEN }}
35+
run: |
36+
docker build --build-arg NPM_REGISTRY_TOKEN=$NPM_REGISTRY_TOKEN -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
37+
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
38+
39+
40+
# - name: Force deployment (acp-x402-server)
41+
# run: |
42+
# aws ecs update-service --cluster protocol-be --service acp-x402-server --force-new-deployment

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ proxy
77
.env
88
__pycache__/
99
**/.DS_Store
10+
*.log

Dockerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Use Python 3.11 slim image for smaller size
2+
FROM python:3.11-slim
3+
4+
# Set working directory
5+
WORKDIR /app
6+
7+
# Install uv for fast dependency management
8+
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
9+
10+
# Set environment variables
11+
ENV PYTHONUNBUFFERED=1 \
12+
PYTHONDONTWRITEBYTECODE=1 \
13+
UV_SYSTEM_PYTHON=1
14+
15+
# Copy x402 library first (for better caching)
16+
COPY python/x402 /app/python/x402
17+
18+
# Install x402 package
19+
RUN cd /app/python/x402 && uv pip install -e .
20+
21+
# Copy FastAPI example
22+
COPY examples/python/servers/fastapi /app/examples/python/servers/fastapi
23+
24+
# Install FastAPI example dependencies
25+
WORKDIR /app/examples/python/servers/fastapi
26+
RUN uv pip install fastapi uvicorn python-dotenv pyjwt cryptography cdp-sdk httpx
27+
28+
# Expose port
29+
EXPOSE 4021
30+
31+
# Health check (use curl for simplicity, or install it if needed)
32+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
33+
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:4021/premium/content').read()"
34+
35+
# Run the application
36+
CMD ["python", "main.py"]
37+

docker-compose.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
version: "3.8"
2+
3+
services:
4+
x402-server:
5+
build:
6+
context: .
7+
dockerfile: Dockerfile
8+
ports:
9+
- "4021:4021"
10+
environment:
11+
# Required: Set your Ethereum address
12+
- ADDRESS=${ADDRESS}
13+
# Optional: Coinbase CDP credentials
14+
- CDP_API_KEY_ID=${CDP_API_KEY_ID:-}
15+
- CDP_API_KEY_SECRET=${CDP_API_KEY_SECRET:-}
16+
# Optional: Network
17+
- NETWORK=${NETWORK:-base}
18+
env_file:
19+
- .env
20+
restart: unless-stopped
21+
healthcheck:
22+
test:
23+
[
24+
"CMD",
25+
"python",
26+
"-c",
27+
"import urllib.request; urllib.request.urlopen('http://localhost:4021/premium/content').read()",
28+
]
29+
interval: 30s
30+
timeout: 10s
31+
retries: 3
32+
start_period: 40s
33+
logging:
34+
driver: "json-file"
35+
options:
36+
max-size: "10m"
37+
max-file: "3"

examples/python/clients/httpx/extensible.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,24 @@ async def main():
2929
# Add payment hooks directly to client.event_hooks
3030
client.event_hooks = x402_payment_hooks(account)
3131

32-
# Make request
32+
# Make request with X-Budget header
3333
try:
34-
print(f"Making request to {endpoint_path}")
35-
response = await client.get(endpoint_path)
34+
budget = "$0.01" # ⭐ Your budget
35+
print(f"Making request to {endpoint_path} with budget: {budget}")
36+
37+
response = await client.get(
38+
endpoint_path,
39+
headers={"X-Budget": budget} # ⭐ Dynamic pricing
40+
)
3641

3742
# Read the response content
3843
content = await response.aread()
3944
print(f"Response: {content.decode()}")
4045

4146
# Check for payment response header
42-
if "X-Payment-Response" in response.headers:
47+
if "X-PAYMENT-RESPONSE" in response.headers:
4348
payment_response = decode_x_payment_response(
44-
response.headers["X-Payment-Response"]
49+
response.headers["X-PAYMENT-RESPONSE"]
4550
)
4651
print(
4752
f"Payment response transaction hash: {payment_response['transaction']}"

examples/python/clients/httpx/main.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,17 @@ def custom_payment_selector(
2626
accepts, network_filter=None, scheme_filter=None, max_value=None
2727
):
2828
"""Custom payment selector that filters by network."""
29-
# Ignore the network_filter parameter for this example - we hardcode base-sepolia
29+
# Ignore the network_filter parameter for this example - we hardcode base (mainnet)
3030
_ = network_filter
3131

3232
# NOTE: In a real application, you'd want to dynamically choose the most
3333
# appropriate payment requirement based on user preferences, available funds,
3434
# network conditions, or other business logic rather than hardcoding a network.
3535

36-
# Filter by base-sepolia network (testnet)
36+
# Filter by base network (mainnet)
3737
return x402Client.default_payment_requirements_selector(
3838
accepts,
39-
network_filter="base-sepolia",
39+
network_filter="base",
4040
scheme_filter=scheme_filter,
4141
max_value=max_value,
4242
)
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""
2+
Test /acp-budget with dynamic pricing via X-Budget header.
3+
4+
Usage:
5+
uv run python test_budget.py
6+
"""
7+
8+
import os
9+
import asyncio
10+
from dotenv import load_dotenv
11+
from eth_account import Account
12+
from x402.clients.httpx import x402HttpxClient
13+
from x402.clients.base import decode_x_payment_response
14+
15+
load_dotenv()
16+
17+
private_key = os.getenv("PRIVATE_KEY")
18+
base_url = os.getenv("RESOURCE_SERVER_URL", "http://localhost:4021")
19+
20+
if not private_key:
21+
print("Error: PRIVATE_KEY not set")
22+
exit(1)
23+
24+
account = Account.from_key(private_key)
25+
print(f"Account: {account.address}\n")
26+
27+
28+
async def test_with_budget(budget: str):
29+
"""Test endpoint with specific budget."""
30+
print(f"{'='*60}")
31+
print(f"Testing with budget: {budget}")
32+
print(f"{'='*60}")
33+
34+
# x402HttpxClient automatically handles payment flow
35+
async with x402HttpxClient(
36+
account=account,
37+
base_url=base_url,
38+
) as client:
39+
# Add X-Budget header to the request
40+
response = await client.get(
41+
"/acp-budget",
42+
headers={"X-Budget": budget} # ⭐ Dynamic pricing
43+
)
44+
45+
content = await response.aread()
46+
print(f"✅ Response: {content.decode()}")
47+
48+
if "X-PAYMENT-RESPONSE" in response.headers:
49+
payment_resp = decode_x_payment_response(
50+
response.headers["X-PAYMENT-RESPONSE"]
51+
)
52+
print(f"🧾 TX: {payment_resp.get('transaction', 'N/A')}\n")
53+
54+
55+
async def main():
56+
"""Run tests with different budgets."""
57+
# await test_with_budget("$0.001")
58+
await test_with_budget("$0.01")
59+
# await test_with_budget("$0.05")
60+
61+
62+
if __name__ == "__main__":
63+
asyncio.run(main())
64+

0 commit comments

Comments
 (0)