Skip to content

Commit 043ef06

Browse files
committed
feat(hyper-x402): add docker-compose facilitator + seller demo, local mock facilitator, and CI/CD workflows
2 parents 2daa42f + bfc3144 commit 043ef06

11 files changed

Lines changed: 495 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: build-and-test
2+
on:
3+
push:
4+
branches: [ main ]
5+
pull_request:
6+
7+
jobs:
8+
docker-build:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Build seller image
14+
run: docker build -t seller-demo:ci ./Hyper-x402/seller-demo-node
15+
16+
- name: Compose up (without secrets)
17+
working-directory: Hyper-x402
18+
run: |
19+
cp .env.example .env
20+
docker compose up -d
21+
sleep 5
22+
curl -s http://localhost:4020/supported || (docker compose logs && exit 1)
23+
24+
- name: Show logs
25+
working-directory: Hyper-x402
26+
run: docker compose logs --no-color
27+

.github/workflows/deploy.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: deploy
2+
on:
3+
workflow_dispatch:
4+
push:
5+
branches: [ main ]
6+
7+
jobs:
8+
ship:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- name: Deploy over SSH
13+
uses: appleboy/ssh-action@v1.0.3
14+
with:
15+
host: ${{ secrets.SSH_HOST }}
16+
username: ${{ secrets.SSH_USER }}
17+
key: ${{ secrets.SSH_KEY }}
18+
script: |
19+
set -e
20+
REPO_URL=${{ github.server_url }}/${{ github.repository }}.git
21+
mkdir -p /opt/hyper-facilitator
22+
cd /opt/hyper-facilitator
23+
if [ ! -d .git ]; then
24+
git clone "$REPO_URL" .
25+
else
26+
git fetch origin
27+
fi
28+
git checkout -f main
29+
git reset --hard origin/main
30+
cd Hyper-x402
31+
cp .env.example .env || true
32+
docker compose up -d
33+

Hyper-x402/.env.example

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
## Facilitator
2+
FAC_BIND=0.0.0.0:4020
3+
FAC_NETWORKS=base-mainnet
4+
FAC_RPC_BASE_MAINNET=https://base-mainnet.g.alchemy.com/v2/YOUR_HYPERAGI_RPC_KEY
5+
FAC_SERVER_WALLET_KEY=0xHYPERAGI_PRIVATE_KEY
6+
RUST_LOG=info
7+
8+
## Seller
9+
FACILITATOR_URL=http://facilitator:4020
10+
MERCHANT_ADDR=0xHyperAGIMerchantWallet
11+
SCHEME=exact
12+
NETWORK=base-mainnet
13+
ASSET=USDC
14+
AMOUNT=0.01
15+

Hyper-x402/docker-compose.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
version: "3.8"
2+
3+
services:
4+
facilitator:
5+
image: ghcr.io/x402-rs/x402-rs:latest
6+
container_name: facilitator
7+
restart: unless-stopped
8+
ports:
9+
- "4020:4020"
10+
environment:
11+
- FAC_BIND=${FAC_BIND}
12+
- FAC_NETWORKS=${FAC_NETWORKS}
13+
- FAC_RPC_BASE_MAINNET=${FAC_RPC_BASE_MAINNET}
14+
- FAC_SERVER_WALLET_KEY=${FAC_SERVER_WALLET_KEY}
15+
- RUST_LOG=${RUST_LOG}
16+
17+
seller:
18+
build: ./seller-demo-node
19+
container_name: seller
20+
restart: unless-stopped
21+
environment:
22+
- FACILITATOR_URL=${FACILITATOR_URL}
23+
- MERCHANT_ADDR=${MERCHANT_ADDR}
24+
- SCHEME=${SCHEME}
25+
- NETWORK=${NETWORK}
26+
- ASSET=${ASSET}
27+
- AMOUNT=${AMOUNT}
28+
ports:
29+
- "4080:4080"
30+
depends_on:
31+
- facilitator
32+

Hyper-x402/scripts/start.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
cd "$(dirname "$0")/.."
4+
docker compose up -d
5+
docker ps
6+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
FACILITATOR_URL=http://facilitator:4020
2+
MERCHANT_ADDR=0xHyperAGIMerchantWallet
3+
SCHEME=exact
4+
NETWORK=base-mainnet
5+
ASSET=USDC
6+
AMOUNT=0.01
7+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM node:20-alpine
2+
WORKDIR /app
3+
COPY package.json .
4+
RUN npm i --omit=dev
5+
COPY . .
6+
EXPOSE 4080
7+
CMD ["npm","start"]
8+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import express from "express";
2+
import fetch from "node-fetch";
3+
import dotenv from "dotenv";
4+
dotenv.config();
5+
6+
const app = express();
7+
app.use(express.json());
8+
9+
const PORT = 4080;
10+
const FACILITATOR_URL = process.env.FACILITATOR_URL || "http://localhost:4020";
11+
const MERCHANT_ADDR = process.env.MERCHANT_ADDR || "0xHyperAGIMerchantWallet";
12+
const SCHEME = process.env.SCHEME || "exact";
13+
const NETWORK = process.env.NETWORK || "base-mainnet";
14+
const ASSET = process.env.ASSET || "USDC";
15+
const AMOUNT = process.env.AMOUNT || "0.01";
16+
17+
// Example protected endpoint
18+
app.get("/paid", async (req, res) => {
19+
const paymentHeader = req.header("X-PAYMENT");
20+
const paymentRequirements = { scheme: SCHEME, network: NETWORK, asset: ASSET, amount: AMOUNT, payTo: MERCHANT_ADDR };
21+
22+
if (!paymentHeader) {
23+
return res.status(402).json({ x402Version: "1", paymentRequirements, facilitator: FACILITATOR_URL });
24+
}
25+
26+
try {
27+
const verifyResp = await fetch(`${FACILITATOR_URL}/verify`, {
28+
method: "POST", headers: { "Content-Type": "application/json" },
29+
body: JSON.stringify({ x402Version: "1", paymentHeader, paymentRequirements })
30+
});
31+
const verifyJson = await verifyResp.json();
32+
if (!verifyJson.isValid) return res.status(402).json({ reason: verifyJson.invalidReason || "verify_failed" });
33+
34+
const settleResp = await fetch(`${FACILITATOR_URL}/settle`, {
35+
method: "POST", headers: { "Content-Type": "application/json" },
36+
body: JSON.stringify({ x402Version: "1", paymentHeader, paymentRequirements })
37+
});
38+
const settleJson = await settleResp.json();
39+
if (!settleJson.success) return res.status(402).json({ reason: settleJson.error || "settlement_failed" });
40+
41+
return res.json({ ok: true, txHash: settleJson.txHash || settleJson.transaction, data: "Paid content delivered." });
42+
} catch (e) {
43+
console.error(e);
44+
return res.status(500).json({ error: String(e) });
45+
}
46+
});
47+
48+
app.get("/health", (req, res) => res.json({ ok: true }));
49+
50+
app.listen(PORT, () => console.log(`Seller demo listening on :${PORT}`));
51+
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "seller-demo-node",
3+
"version": "0.1.0",
4+
"private": true,
5+
"type": "module",
6+
"scripts": { "start": "node index.js" },
7+
"dependencies": {
8+
"dotenv": "^16.3.1",
9+
"express": "^4.19.2",
10+
"node-fetch": "^3.3.2"
11+
}
12+
}
13+

go/bin/README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,29 @@ Example of all possible keys can be found in `example_config.json`. The Minimal
1717
"payTo": "0x<your address>"
1818
}
1919
```
20+
21+
## Local Facilitator (no-auth, mock)
22+
23+
For local development without external dependencies, a minimal facilitator server is available. It implements the x402 facilitator interface with in-memory, mock semantics (no on-chain settlement):
24+
25+
Endpoints:
26+
- `GET /supported` – returns supported `(scheme, network)` pairs
27+
- `POST /verify` – basic consistency checks; always returns `isValid: true` if request is well-formed
28+
- `POST /settle` – returns a success response with a fake transaction id
29+
- `GET /discovery/resources` – returns an empty paginated list
30+
31+
Run:
32+
```
33+
go run facilitator_local.go
34+
```
35+
36+
Environment variables:
37+
- `FAC_PORT` – listen port (default: `8787`)
38+
- `FAC_SCHEME` – scheme (default: `exact`)
39+
- `FAC_NETWORKS` – comma-separated networks (default: `base-sepolia`)
40+
41+
Wire up examples to use it by pointing facilitator URL to `http://localhost:8787`, e.g.:
42+
- TypeScript examples: set `FACILITATOR_URL=http://localhost:8787`
43+
- Go proxy config (`example_config.json`): set `"facilitatorURL": "http://localhost:8787"`
44+
45+
Note: This facilitator is suitable for interface testing only. Replace with your real Hyper-x402 facilitator once ready.

0 commit comments

Comments
 (0)