feat: add x402 facilitator support and configuration
- Introduced environment variables for x402 facilitator configuration in .env.example and docker-compose.yml. - Updated main.go to include x402 facilitator settings in the server configuration. - Enhanced API responses to include x402 facilitator status and metadata. - Added documentation for x402 facilitator in SDK API and Configuration Reference. - Implemented x402 facilitator handling in the server logic and tests. - Removed unused dependency on StackExchange/wmi from go.mod.
Kim committed
May 27, 2026 at 10:36 UTC
d59b963280a0c08b7d4b75c2c1930a52be7f3b6f
15 files changed
+140
-27
.env.example
+4
-1
@@ -14,7 +14,6 @@ MAX_PORT=0
14
UDP_ENABLED=false
15
TCP_ENABLED=false
16
17
-
17
# Supported managed values: cloudflare, gcloud, hetzner, route53, vultr.
18
# Reused for ACME DNS-01, managed A records, ECH HTTPS records, and optional ENS DNS automation.
19
ACME_DNS_PROVIDER=
@@ -58,3 +57,7 @@ TRUSTED_PROXY_CIDRS=
57
# Requires the headless-shell sidecar (chromedp/headless-shell) in docker-compose.
58
# Leave empty or remove to disable. See docs/src/routes/deployment/+page.md.
59
# HEADLESS_SHELL_URL=ws://headless-shell:9222
60
+
61
+X402_FACILITATOR_ENABLED=false
62
+X402_NETWORK=eip155:84532
63
+X402_RPC_URL=https://base-sepolia-rpc.publicnode.com
\ No newline at end of file
cmd/relay-server/main.go
+2
@@ -177,6 +177,8 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
177
MaxPort: cfg.MaxPort,
178
PProfEnabled: cfg.PProfEnabled,
179
PProfListenAddr: cfg.PProfAddr,
180
+ X402Enabled: cfg.X402Enabled,
181
+ X402Network: cfg.X402Network,
182
ACME: acme.Config{
183
KeyDir: cfg.IdentityPath,
184
DNSProvider: cfg.ACMEDNSProvider,
docker-compose.yml
+5
@@ -46,6 +46,11 @@ services:
46
TRUST_PROXY_HEADERS: ${TRUST_PROXY_HEADERS:-false}
47
TRUSTED_PROXY_CIDRS: ${TRUSTED_PROXY_CIDRS:-}
48
49
+ # Optional: relay-local x402 facilitator exposed under /x402
50
+ X402_FACILITATOR_ENABLED: ${X402_FACILITATOR_ENABLED:-false}
51
+ X402_NETWORK: ${X402_NETWORK:-eip155:84532}
52
+ X402_RPC_URL: ${X402_RPC_URL:-https://base-sepolia-rpc.publicnode.com}
53
+
54
# Optional: auto-generated thumbnails (requires headless-shell sidecar above)
55
# HEADLESS_SHELL_URL: ${HEADLESS_SHELL_URL:-ws://headless-shell:9222}
56
docs/src/routes/api-reference/sdk/+page.md
+18
@@ -67,6 +67,7 @@ Get relay domain and protocol version information. Used by the SDK to verify rel
67
| `protocol_version` | `string` | Protocol version (must match SDK version) |
68
| `release_version` | `string` | Relay software release version |
69
| `ens` | `object` | ENS gasless status for this relay |
70
+| `x402` | `object` | Relay-local x402 facilitator status and public payment metadata |
71
72
`ens` fields:
73
@@ -81,6 +82,16 @@ Get relay domain and protocol version information. Used by the SDK to verify rel
82
| `message` | `string` | Provider-specific DNSSEC guidance |
83
| `last_error` | `string` | Last ENS/DNS sync error |
84
85
+`x402` fields:
86
+
87
+| Field | Type | Description |
88
+|-------|------|-------------|
89
+| `enabled` | `bool` | Relay-local facilitator is exposed under `/x402` |
90
+| `url` | `string` | Facilitator base URL to use in tunnel x402 config |
91
+| `network` | `string` | CAIP-2 payment network served by the facilitator |
92
+| `network_name` | `string` | Human-readable network name when known |
93
+| `supported_url` | `string` | URL for the facilitator `/supported` endpoint |
94
+
95
**Example:**
96
97
```bash
@@ -101,6 +112,13 @@ curl https://relay.example.com/sdk/domain
112
"provider": "cloudflare",
113
"address": "0x1234567890abcdef1234567890abcdef12345678",
114
"dnssec_state": "active"
115
+ },
116
+ "x402": {
117
+ "enabled": true,
118
+ "url": "https://relay.example.com/x402",
119
+ "network": "eip155:84532",
120
+ "network_name": "Base Sepolia",
121
+ "supported_url": "https://relay.example.com/x402/supported"
122
}
123
}
124
}
docs/src/routes/configuration/+page.md
+2
-1
@@ -74,7 +74,8 @@ The relay server (`relay-server`) reads configuration from environment variables
74
| `X402_RPC_URL` | | string | RPC URL used by the facilitator; empty uses the facilitator default for supported networks |
75
76
The relay-local facilitator uses the relay identity private key from
77
-`IDENTITY_PATH/identity.json`.
77
+`IDENTITY_PATH/identity.json`. `/sdk/domain` exposes only the public facilitator
78
+URL and network; `X402_RPC_URL` is not returned to clients.
79
80
### Cloudflare
81
docs/src/routes/self-hosting/+page.md
+2
-2
@@ -135,8 +135,8 @@ See [TCP/UDP Tunneling](/tcp-udp-tunneling) for usage details.
135
## Optional: Enable x402 Facilitator
136
137
The relay can expose a relay-local x402 facilitator at `/x402`. Frontends and
138
-configuration tools can read `/x402/supported` and write the selected
139
-facilitator URL into tunnel x402 route config.
138
+configuration tools can read `/sdk/domain` for the current relay's facilitator
139
+URL and network, then call `/x402/supported` for mechanism details when needed.
140
141
```yaml
142
environment:
frontend/src/components/Header.tsx
+31
@@ -24,6 +24,15 @@ interface DomainStatusResponse {
24
ens?: {
25
verified?: boolean;
26
};
27
+ x402?: X402FacilitatorInfo;
28
+}
29
+
30
+interface X402FacilitatorInfo {
31
+ enabled?: boolean;
32
+ url?: string;
33
+ network?: string;
34
+ network_name?: string;
35
+ supported_url?: string;
36
}
37
38
const repoURL = "https://github.com/gosuda/portal-tunnel";
@@ -36,6 +45,14 @@ function formatWalletAddress(address: string): string {
45
return `${trimmed.slice(0, 6)}...${trimmed.slice(-4)}`;
46
}
47
48
+function facilitatorURL(x402: X402FacilitatorInfo): string {
49
+ return x402.url?.trim() || "/x402";
50
+}
51
+
52
+function facilitatorNetworkLabel(x402: X402FacilitatorInfo): string {
53
+ return x402.network_name?.trim() || x402.network?.trim() || "enabled";
54
+}
55
+
56
export function Header({
57
title = "PORTAL",
58
isAdmin,
@@ -44,6 +61,7 @@ export function Header({
61
}: HeaderProps) {
62
const releaseVersion = getReleaseVersion();
63
const [ensVerified, setENSVerified] = useState(false);
64
+ const [x402, setX402] = useState<X402FacilitatorInfo | null>(null);
65
const {
66
isAuthenticated,
67
isLoading,
@@ -86,10 +104,12 @@ export function Header({
104
);
105
if (!cancelled) {
106
setENSVerified(status?.ens?.verified === true);
107
+ setX402(status?.x402?.enabled === true ? status.x402 : null);
108
}
109
} catch {
110
if (!cancelled) {
111
setENSVerified(false);
112
+ setX402(null);
113
}
114
}
115
})();
@@ -133,6 +153,17 @@ export function Header({
153
ENS verified
154
</span>
155
)}
156
+ {x402 && (
157
+ <a
158
+ href={facilitatorURL(x402)}
159
+ target="_blank"
160
+ rel="noopener noreferrer"
161
+ className="inline-flex h-6 max-w-40 items-center overflow-hidden text-ellipsis whitespace-nowrap rounded-full bg-emerald-500/10 px-2.5 text-xs font-semibold text-emerald-700 ring-1 ring-emerald-500/20 transition-colors hover:bg-emerald-500/15 dark:text-emerald-300"
162
+ title={`x402 facilitator: ${facilitatorURL(x402)}`}
163
+ >
164
+ x402 {facilitatorNetworkLabel(x402)}
165
+ </a>
166
+ )}
167
</div>
168
</div>
169
</div>
go.mod
-1
@@ -50,7 +50,6 @@ require (
50
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
51
github.com/PuerkitoBio/purell v1.1.1 // indirect
52
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
53
- github.com/StackExchange/wmi v1.2.1 // indirect
53
github.com/atotto/clipboard v0.1.4 // indirect
54
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
55
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
go.sum
+19
-19
@@ -18,8 +18,6 @@ github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tN
18
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
19
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
20
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
21
-github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
22
-github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
21
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
22
github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
23
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
@@ -66,6 +64,8 @@ github.com/blocto/solana-go-sdk v1.30.0 h1:GEh4GDjYk1lMhV/hqJDCyuDeCuc5dianbN33y
64
github.com/blocto/solana-go-sdk v1.30.0/go.mod h1:Xoyhhb3hrGpEQ5rJps5a3OgMwDpmEhrd9bgzFKkkwMs=
65
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
66
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
67
+github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
68
+github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
69
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
70
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
71
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
@@ -116,17 +116,14 @@ github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
116
github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc=
117
github.com/dchest/uniuri v1.2.0 h1:koIcOUdrTIivZgSLhHQvKgqdWZq5d7KdMEWF1Ud6+5g=
118
github.com/dchest/uniuri v1.2.0/go.mod h1:fSzm4SLHzNZvWLvWJew423PhAzkpNQYq+uNLq4kxhkY=
119
-github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
120
-github.com/deckarep/golang-set/v2 v2.6.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
119
github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
120
github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
123
-github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
124
-github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
121
github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8=
126
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4=
127
-github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc=
122
+github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo=
123
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
124
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
125
+github.com/deepmap/oapi-codegen v1.6.0 h1:w/d1ntwh91XI0b/8ja7+u5SvA4IFfM0UNNLmiDR1gg0=
126
+github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M=
127
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
128
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
129
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -145,6 +142,8 @@ github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeD
142
github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
143
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
144
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
145
+github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff h1:tY80oXqGNY4FhTFhk+o9oFHGINQ/+vhlm8HFzi6znCI=
146
+github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff/go.mod h1:x7DCsMOv1taUwEWCzT4cmDeAkigA5/QCwUodaVOe8Ww=
147
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
148
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
149
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
@@ -158,7 +157,6 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
157
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
158
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
159
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
161
-github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
160
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
161
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
162
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
@@ -214,6 +212,8 @@ github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb
212
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
213
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
214
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
215
+github.com/graph-gophers/graphql-go v1.3.0 h1:Eb9x/q6MFpCLz7jBCiP/WTxjSDrYLR1QY41SORZyNJ0=
216
+github.com/graph-gophers/graphql-go v1.3.0/go.mod h1:9CQHMSxwO4MprSdzoIEobiHpoLtHm77vfxsvsIN5Vuc=
217
github.com/hashicorp/go-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE=
218
github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
219
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
@@ -234,6 +234,12 @@ github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA
234
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
235
github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc=
236
github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
237
+github.com/influxdata/influxdb-client-go/v2 v2.4.0 h1:HGBfZYStlx3Kqvsv1h2pJixbCl/jhnFtxpKFAv9Tu5k=
238
+github.com/influxdata/influxdb-client-go/v2 v2.4.0/go.mod h1:vLNHdxTJkIf2mSLvGrpj8TCcISApPoXkaxP8g9uRlW8=
239
+github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c h1:qSHzRbhzK8RdXOsAdfDgO49TtqC1oZ+acxPrkfTxcCs=
240
+github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo=
241
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839 h1:W9WBk7wlPfJLvMCdtV4zPulc4uCPrlywQOmbFOhgQNU=
242
+github.com/influxdata/line-protocol v0.0.0-20200327222509-2487e7298839/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo=
243
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
244
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
245
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -308,8 +314,12 @@ github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3
314
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
315
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
316
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
317
+github.com/opentracing/opentracing-go v1.1.0 h1:pWlfV3Bxv7k65HYwkikxat0+s3pV4bsqf19k25Ur8rU=
318
+github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
319
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
320
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
321
+github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 h1:oYW+YCJ1pachXTQmzR3rNLYGGz4g/UgFcjb28p/viDM=
322
+github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
323
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
324
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
325
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
@@ -348,8 +358,6 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
358
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
359
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
360
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
351
-github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
352
-github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
361
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
362
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
363
github.com/spruceid/siwe-go v0.2.1 h1:BroySys6CyUzeyNppTseEOT/w56xTdOfcmECTI7rnuc=
@@ -433,8 +441,6 @@ go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
441
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
442
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
443
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
436
-golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
437
-golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
444
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
445
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
446
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
@@ -442,8 +448,6 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aI
448
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
449
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
450
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
445
-golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
446
-golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
451
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
452
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
453
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
@@ -460,15 +464,11 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
464
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
465
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
466
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
463
-golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
464
-golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
467
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
468
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
469
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
470
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
471
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
470
-golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
471
-golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
472
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
473
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
474
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
portal/api_server.go
+13
@@ -17,6 +17,7 @@ import (
17
"github.com/gosuda/portal-tunnel/v2/portal/auth"
18
"github.com/gosuda/portal-tunnel/v2/portal/identity"
19
"github.com/gosuda/portal-tunnel/v2/portal/keyless"
20
+ portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
21
"github.com/gosuda/portal-tunnel/v2/types"
22
"github.com/gosuda/portal-tunnel/v2/utils"
23
)
@@ -248,10 +249,22 @@ func (s *Server) handleDomain(w http.ResponseWriter, r *http.Request) {
249
return
250
}
251
252
+ cfg := s.config()
253
+ x402 := types.X402FacilitatorInfo{
254
+ Enabled: cfg.X402Enabled,
255
+ }
256
+ if x402.Enabled {
257
+ x402.Network = strings.TrimSpace(cfg.X402Network)
258
+ x402.NetworkName = portalx402.NetworkDisplayName(x402.Network)
259
+ x402.URL = cfg.PortalURL + types.PathX402Facilitator
260
+ x402.SupportedURL = cfg.PortalURL + types.X402SupportedPath
261
+ }
262
+
263
utils.WriteAPIData(w, http.StatusOK, types.DomainResponse{
264
ProtocolVersion: types.SDKVersion,
265
ReleaseVersion: types.ReleaseVersion,
266
ENS: s.acmeManager.ENSStatus(),
267
+ X402: x402,
268
})
269
}
270
portal/server.go
+3
@@ -56,6 +56,8 @@ type ServerConfig struct {
56
MaxPort int
57
PProfEnabled bool
58
PProfListenAddr string
59
+ X402Enabled bool
60
+ X402Network string
61
ACME acme.Config
62
}
63
@@ -90,6 +92,7 @@ func normalizeServerConfig(cfg ServerConfig) (ServerConfig, error) {
92
if cfg.PProfEnabled {
93
cfg.PProfListenAddr = utils.StringOrDefault(strings.TrimSpace(cfg.PProfListenAddr), DefaultPProfListenAddr)
94
}
95
+ cfg.X402Network = strings.TrimSpace(cfg.X402Network)
96
97
hasPortRange := cfg.MinPort > 0 && cfg.MaxPort > 0
98
if cfg.UDPEnabled || cfg.TCPEnabled {
portal/server_test.go
+17
@@ -296,6 +296,8 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
296
SNIPort: 4443,
297
APIListenAddr: "127.0.0.1:0",
298
SNIListenAddr: "127.0.0.1:0",
299
+ X402Enabled: true,
300
+ X402Network: " eip155:84532 ",
301
})
302
if err != nil {
303
t.Fatalf("NewServer() error = %v", err)
@@ -338,6 +340,21 @@ func TestServerStartDomainReportsCompatibilityInfo(t *testing.T) {
340
if envelope.Data.ReleaseVersion != types.ReleaseVersion {
341
t.Fatalf("DomainResponse.ReleaseVersion = %q, want %q", envelope.Data.ReleaseVersion, types.ReleaseVersion)
342
}
343
+ if !envelope.Data.X402.Enabled {
344
+ t.Fatal("DomainResponse.X402.Enabled = false, want true")
345
+ }
346
+ if envelope.Data.X402.Network != "eip155:84532" {
347
+ t.Fatalf("DomainResponse.X402.Network = %q, want eip155:84532", envelope.Data.X402.Network)
348
+ }
349
+ if envelope.Data.X402.NetworkName != "Base Sepolia" {
350
+ t.Fatalf("DomainResponse.X402.NetworkName = %q, want Base Sepolia", envelope.Data.X402.NetworkName)
351
+ }
352
+ if envelope.Data.X402.URL != "https://localhost:4017/x402" {
353
+ t.Fatalf("DomainResponse.X402.URL = %q, want https://localhost:4017/x402", envelope.Data.X402.URL)
354
+ }
355
+ if envelope.Data.X402.SupportedURL != "https://localhost:4017/x402/supported" {
356
+ t.Fatalf("DomainResponse.X402.SupportedURL = %q, want https://localhost:4017/x402/supported", envelope.Data.X402.SupportedURL)
357
+ }
358
}
359
360
func TestRegisterLeaseIncludesSNIPortForPublicIngress(t *testing.T) {
portal/x402/x402.go
+12
@@ -25,6 +25,18 @@ const (
25
defaultRouteDescription = "Portal protected route"
26
)
27
28
+var networkDisplayNames = map[string]string{
29
+ "eip155:1": "Ethereum Mainnet",
30
+ "eip155:8453": "Base Mainnet",
31
+ "eip155:84532": "Base Sepolia",
32
+ "eip155:42161": "Arbitrum One",
33
+ "eip155:421614": "Arbitrum Sepolia",
34
+}
35
+
36
+func NetworkDisplayName(network string) string {
37
+ return networkDisplayNames[strings.TrimSpace(strings.ToLower(network))]
38
+}
39
+
40
type FacilitatorConfig struct {
41
Network string
42
RPCURL string
types/api.go
+4
-3
@@ -184,9 +184,10 @@ func HopRouteBytes(method string, route HopRoute) ([]byte, error) {
184
}
185
186
type DomainResponse struct {
187
- ProtocolVersion string `json:"protocol_version"`
188
- ReleaseVersion string `json:"release_version"`
189
- ENS ENSStatus `json:"ens"`
187
+ ProtocolVersion string `json:"protocol_version"`
188
+ ReleaseVersion string `json:"release_version"`
189
+ ENS ENSStatus `json:"ens"`
190
+ X402 X402FacilitatorInfo `json:"x402"`
191
}
192
193
type ENSStatus struct {
types/x402.go
+8
@@ -29,6 +29,14 @@ func (c X402Config) Empty() bool {
29
c.PaymentTimeoutSecs == 0
30
}
31
32
+type X402FacilitatorInfo struct {
33
+ Enabled bool `json:"enabled"`
34
+ URL string `json:"url,omitempty"`
35
+ Network string `json:"network,omitempty"`
36
+ NetworkName string `json:"network_name,omitempty"`
37
+ SupportedURL string `json:"supported_url,omitempty"`
38
+}
39
+
40
type X402SupportedKind struct {
41
X402Version int `json:"x402Version"`
42
Scheme string `json:"scheme"`