feat(x402): introduce x402 facilitator and related HTTP route handling
- Added a new package `x402` with functionality to mount the x402 facilitator and handle HTTP routes. - Implemented `MountFacilitator` to set up the x402 facilitator with necessary configurations. - Created `NewHTTPRouteHandler` to manage x402 payment routes with validation and middleware setup. - Updated `sdk/http.go` to utilize the new x402 route handler, removing legacy code. - Enhanced `AgentTunnelStatus` to include x402-related fields for better tracking. - Added new paths for x402 facilitator operations in `types/paths.go`. - Introduced new constants and types in `types/x402.go` for x402 configuration and responses.
Kim committed
May 26, 2026 at 20:39 UTC
8e1ee74eac4285763ed8ae11a701c3e4139f59da
16 files changed
+486
-128
cmd/portal-tunnel/README.md
+9
-4
@@ -78,6 +78,7 @@ app receives the request:
78
```text
79
portal expose 3000 --name paid-api \
80
--description "Paid API" \
81
+ --x402-facilitator-url https://portal.example.com:4017/x402 \
82
--x402-network eip155:8453 \
83
--x402-price "$0.001" \
84
--x402-resource /
@@ -89,8 +90,10 @@ identity address; set it explicitly when payments should be received by another
90
wallet. The x402 paywall uses tunnel metadata: `--name` for the app name,
91
`--description` for the resource description, and `--thumbnail` for the app
92
logo. `--x402-resource` defaults to the matched HTTP route prefix and controls
92
-the resource path advertised in the x402 payment requirement. x402 is not
93
-available in raw TCP or UDP modes.
93
+the resource path advertised in the x402 payment requirement. The tunnel does
94
+not infer a relay facilitator URL; set `--x402-facilitator-url` explicitly, or
95
+use relay/frontend tooling that writes the desired facilitator URL into the
96
+tunnel config. x402 is not available in raw TCP or UDP modes.
97
98
Use dedicated raw TCP mode for non-HTTP services that need a public TCP port:
99
@@ -196,7 +199,7 @@ Common flags:
199
--x402-price x402 route price, such as $0.001
200
--x402-pay-to x402 recipient address; defaults to the tunnel identity address
201
--x402-facilitator-url
199
- x402 facilitator URL; defaults to the SDK default
202
+ x402 facilitator URL
203
--x402-resource x402 protected resource/root path; defaults to the HTTP route prefix
204
--x402-mime-type x402 protected resource MIME type
205
--x402-testnet Render the x402 paywall in testnet mode
@@ -261,7 +264,8 @@ Useful commands:
264
- `portal agent run --config config.toml --foreground` runs the agent in the
265
current terminal and opens the dashboard when the terminal is interactive.
266
- `portal agent dashboard` attaches to a running agent and opens the local TUI
264
- for tunnels, relays, multi-hop routes, and editable tunnel settings.
267
+ for tunnels, relays, multi-hop routes, editable tunnel settings, and x402
268
+ facilitator URLs.
269
- `portal agent stop` asks the local agent to shut down, then disables or stops
270
the OS service so intentional shutdown is not immediately restarted.
271
- `portal agent restart` stops the running agent if present, installs or updates
@@ -322,6 +326,7 @@ upstream = "http://127.0.0.1:3000"
326
network = "eip155:8453"
327
price = "$0.001"
328
pay_to = "identity"
329
+facilitator_url = "https://portal.example.com:4017/x402"
330
resource = "/"
331
```
332
cmd/portal-tunnel/agent/dashboard.go
+39
-5
@@ -62,6 +62,7 @@ const (
62
agentDashboardSettingsFieldOwner
63
agentDashboardSettingsFieldThumbnail
64
agentDashboardSettingsFieldHide
65
+ agentDashboardSettingsFieldX402Facilitator
66
agentDashboardSettingsFieldCount
67
)
68
@@ -98,6 +99,7 @@ type agentDashboardModel struct {
99
metadataOwner textinput.Model
100
metadataThumbnail textinput.Model
101
metadataHide textinput.Model
102
+ x402FacilitatorURL textinput.Model
103
}
104
105
type agentDashboardStatusMsg struct {
@@ -165,6 +167,7 @@ func RunDashboard(configPath, stateDir string) error {
167
metadataOwner: newAgentDashboardInlineInput("owner"),
168
metadataThumbnail: newAgentDashboardInlineInput("https://..."),
169
metadataHide: newAgentDashboardInlineInput("true or false"),
170
+ x402FacilitatorURL: newAgentDashboardInlineInput("https://relay.example.com/x402"),
171
}
172
model.resizeInputs(0)
173
@@ -701,10 +704,14 @@ func (m agentDashboardModel) updateSettingsKeys(msg tea.KeyMsg) (tea.Model, tea.
704
}
705
706
func (m *agentDashboardModel) focusSettingsField(field int) {
707
+ fieldCount := agentDashboardSettingsFieldCount
708
+ if tunnel, ok := m.selectedTunnelStatus(); ok && !tunnel.X402Enabled {
709
+ fieldCount = agentDashboardSettingsFieldX402Facilitator
710
+ }
711
if field < 0 {
705
- field = agentDashboardSettingsFieldCount - 1
712
+ field = fieldCount - 1
713
}
707
- if field >= agentDashboardSettingsFieldCount {
714
+ if field >= fieldCount {
715
field = 0
716
}
717
m.input.Blur()
@@ -716,6 +723,7 @@ func (m *agentDashboardModel) focusSettingsField(field int) {
723
&m.metadataOwner,
724
&m.metadataThumbnail,
725
&m.metadataHide,
726
+ &m.x402FacilitatorURL,
727
} {
728
input.Blur()
729
}
@@ -738,6 +746,8 @@ func (m *agentDashboardModel) focusedSettingsInput() *textinput.Model {
746
return &m.metadataThumbnail
747
case agentDashboardSettingsFieldHide:
748
return &m.metadataHide
749
+ case agentDashboardSettingsFieldX402Facilitator:
750
+ return &m.x402FacilitatorURL
751
default:
752
return nil
753
}
@@ -751,6 +761,7 @@ func (m *agentDashboardModel) blurSettingsInputs() {
761
&m.metadataOwner,
762
&m.metadataThumbnail,
763
&m.metadataHide,
764
+ &m.x402FacilitatorURL,
765
} {
766
input.Blur()
767
}
@@ -765,6 +776,7 @@ func (m *agentDashboardModel) clearSettingsDraft() {
776
&m.metadataOwner,
777
&m.metadataThumbnail,
778
&m.metadataHide,
779
+ &m.x402FacilitatorURL,
780
} {
781
input.Reset()
782
}
@@ -790,6 +802,7 @@ func (m *agentDashboardModel) resizeInputs(width int) {
802
&m.metadataOwner,
803
&m.metadataThumbnail,
804
&m.metadataHide,
805
+ &m.x402FacilitatorURL,
806
} {
807
input.Width = settingsWidth
808
}
@@ -813,18 +826,23 @@ func (m *agentDashboardModel) ensureSettingsDraft(tunnel types.AgentTunnelStatus
826
func (m *agentDashboardModel) loadSettingsDraft(tunnel types.AgentTunnelStatus) {
827
metadata := tunnel.Metadata
828
m.settingsEditTunnelID = tunnel.ID
829
+ if !tunnel.X402Enabled && m.settingsFocus == agentDashboardSettingsFieldX402Facilitator {
830
+ m.settingsFocus = agentDashboardSettingsFieldMaxActiveRelays
831
+ }
832
m.settingsMaxRelays.SetValue(strconv.Itoa(tunnel.MaxActiveRelays))
833
m.metadataDescription.SetValue(strings.TrimSpace(metadata.Description))
834
m.metadataTags.SetValue(strings.Join(metadata.Tags, ","))
835
m.metadataOwner.SetValue(strings.TrimSpace(metadata.Owner))
836
m.metadataThumbnail.SetValue(strings.TrimSpace(metadata.Thumbnail))
837
m.metadataHide.SetValue(strconv.FormatBool(metadata.Hide))
838
+ m.x402FacilitatorURL.SetValue(strings.TrimSpace(tunnel.X402FacilitatorURL))
839
m.settingsMaxRelays.CursorEnd()
840
m.metadataDescription.CursorEnd()
841
m.metadataTags.CursorEnd()
842
m.metadataOwner.CursorEnd()
843
m.metadataThumbnail.CursorEnd()
844
m.metadataHide.CursorEnd()
845
+ m.x402FacilitatorURL.CursorEnd()
846
}
847
848
func (m agentDashboardModel) addTunnelFromInput() (tea.Model, tea.Cmd) {
@@ -918,6 +936,12 @@ func (m agentDashboardModel) applySettingsEdit() (tea.Model, tea.Cmd) {
936
MaxActiveRelays: &maxActiveRelays,
937
Metadata: &metadata,
938
}
939
+ if tunnel.X402Enabled {
940
+ facilitatorURL := strings.TrimSpace(m.x402FacilitatorURL.Value())
941
+ if facilitatorURL != strings.TrimSpace(tunnel.X402FacilitatorURL) {
942
+ req.X402FacilitatorURL = &facilitatorURL
943
+ }
944
+ }
945
m.err = nil
946
return m, agentDashboardRun(func(ctx context.Context) error {
947
return UpdateTunnel(ctx, m.stateDir, tunnel.ID, req)
@@ -1284,10 +1308,10 @@ func (m agentDashboardModel) renderSettingsSection(pane *agentDashboardView, wid
1308
return
1309
}
1310
1287
- m.renderSettingsInputRows(pane, width, height, startLine)
1311
+ m.renderSettingsInputRows(pane, width, height, startLine, tunnel)
1312
}
1313
1290
-func (m agentDashboardModel) renderSettingsInputRows(pane *agentDashboardView, width, height, startLine int) {
1314
+func (m agentDashboardModel) renderSettingsInputRows(pane *agentDashboardView, width, height, startLine int, tunnel types.AgentTunnelStatus) {
1315
rows := []struct {
1316
label string
1317
input textinput.Model
@@ -1300,6 +1324,13 @@ func (m agentDashboardModel) renderSettingsInputRows(pane *agentDashboardView, w
1324
{label: "Thumbnail", input: m.metadataThumbnail, field: agentDashboardSettingsFieldThumbnail},
1325
{label: "Hidden", input: m.metadataHide, field: agentDashboardSettingsFieldHide},
1326
}
1327
+ if tunnel.X402Enabled {
1328
+ rows = append(rows, struct {
1329
+ label string
1330
+ input textinput.Model
1331
+ field int
1332
+ }{label: "Facilitator", input: m.x402FacilitatorURL, field: agentDashboardSettingsFieldX402Facilitator})
1333
+ }
1334
for _, row := range rows {
1335
if len(pane.lines)-startLine >= height {
1336
return
@@ -1656,12 +1687,15 @@ func (m agentDashboardModel) settingsChanged(tunnel types.AgentTunnelStatus) boo
1687
return true
1688
}
1689
metadata := tunnel.Metadata
1690
+ x402Changed := tunnel.X402Enabled &&
1691
+ strings.TrimSpace(m.x402FacilitatorURL.Value()) != strings.TrimSpace(tunnel.X402FacilitatorURL)
1692
return maxRelays != tunnel.MaxActiveRelays ||
1693
strings.TrimSpace(m.metadataDescription.Value()) != strings.TrimSpace(metadata.Description) ||
1694
!slices.Equal(utils.SplitCSV(m.metadataTags.Value()), metadata.Tags) ||
1695
strings.TrimSpace(m.metadataOwner.Value()) != strings.TrimSpace(metadata.Owner) ||
1696
strings.TrimSpace(m.metadataThumbnail.Value()) != strings.TrimSpace(metadata.Thumbnail) ||
1664
- hide != metadata.Hide
1697
+ hide != metadata.Hide ||
1698
+ x402Changed
1699
}
1700
1701
func relayDashboardFeatures(relay types.AgentRelayStatus) string {
cmd/portal-tunnel/agent/manager.go
+53
-8
@@ -161,6 +161,7 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
161
}
162
updateMetadata := req.Metadata != nil && !req.Metadata.Empty()
163
updateMaxActiveRelays := req.MaxActiveRelays != nil
164
+ restartTunnel := false
165
if err := m.updateTunnelConfig(id, func(tunnel *TunnelConfig) error {
166
if req.MaxActiveRelays != nil {
167
if *req.MaxActiveRelays <= 0 {
@@ -185,6 +186,25 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
186
tunnel.Hide = *req.Metadata.Hide
187
}
188
}
189
+ if req.X402FacilitatorURL != nil {
190
+ facilitatorURL := strings.TrimSpace(*req.X402FacilitatorURL)
191
+ x402Route := false
192
+ for i := range tunnel.HTTPRoutes {
193
+ if tunnel.HTTPRoutes[i].X402 == nil || tunnel.HTTPRoutes[i].X402.Empty() {
194
+ continue
195
+ }
196
+ x402Route = true
197
+ x402Config := *tunnel.HTTPRoutes[i].X402
198
+ if strings.TrimSpace(x402Config.FacilitatorURL) != facilitatorURL {
199
+ restartTunnel = true
200
+ }
201
+ x402Config.FacilitatorURL = facilitatorURL
202
+ tunnel.HTTPRoutes[i].X402 = &x402Config
203
+ }
204
+ if !x402Route {
205
+ return errors.New("tunnel has no x402 http routes")
206
+ }
207
+ }
208
return nil
209
}); err != nil {
210
return err
@@ -193,10 +213,21 @@ func (m *manager) UpdateTunnel(id string, req types.AgentTunnelUpdateRequest) er
213
id = strings.TrimSpace(id)
214
m.mu.RLock()
215
tunnel := m.tunnels[id]
216
+ rootCtx := m.rootCtx
217
m.mu.RUnlock()
218
if tunnel == nil {
219
return fmt.Errorf("tunnel %q not found", id)
220
}
221
+ if restartTunnel {
222
+ if err := tunnel.Stop(context.Background()); err != nil {
223
+ return err
224
+ }
225
+ if rootCtx == nil {
226
+ rootCtx = context.Background()
227
+ }
228
+ tunnel.Start(rootCtx)
229
+ return nil
230
+ }
231
return tunnel.UpdateSettings(updateMetadata, updateMaxActiveRelays)
232
}
233
@@ -566,15 +597,29 @@ func (t *managedTunnel) Snapshot() types.AgentTunnelStatus {
597
state = "starting"
598
}
599
600
+ x402Enabled := false
601
+ x402FacilitatorURL := ""
602
+ for _, route := range cfg.HTTPRoutes {
603
+ if route.X402 == nil || route.X402.Empty() {
604
+ continue
605
+ }
606
+ x402Enabled = true
607
+ if x402FacilitatorURL == "" {
608
+ x402FacilitatorURL = strings.TrimSpace(route.X402.FacilitatorURL)
609
+ }
610
+ }
611
+
612
status := types.AgentTunnelStatus{
570
- ID: cfg.ID,
571
- Name: cfg.Name,
572
- State: state,
573
- TargetAddr: cfg.TargetAddr,
574
- LastError: lastError,
575
- MaxActiveRelays: cfg.MaxActiveRelays,
576
- Metadata: metadataFromTunnelConfig(cfg),
577
- MultiHop: append([]string(nil), cfg.MultiHop...),
613
+ ID: cfg.ID,
614
+ Name: cfg.Name,
615
+ State: state,
616
+ TargetAddr: cfg.TargetAddr,
617
+ LastError: lastError,
618
+ MaxActiveRelays: cfg.MaxActiveRelays,
619
+ Metadata: metadataFromTunnelConfig(cfg),
620
+ X402Enabled: x402Enabled,
621
+ X402FacilitatorURL: x402FacilitatorURL,
622
+ MultiHop: append([]string(nil), cfg.MultiHop...),
623
}
624
if exposure == nil {
625
if strings.TrimSpace(runtime.Address) != "" {
cmd/portal-tunnel/main.go
+1
-1
@@ -214,7 +214,7 @@ func (f *exposeX402Flags) bind(fs *flag.FlagSet) {
214
utils.StringFlag(fs, &f.network, "x402-network", "", "x402 payment network, such as eip155:8453")
215
utils.StringFlag(fs, &f.price, "x402-price", "", "x402 route price, such as $0.001")
216
utils.StringFlag(fs, &f.payTo, "x402-pay-to", "", "x402 recipient address; empty uses the tunnel identity address")
217
- utils.StringFlag(fs, &f.facilitator, "x402-facilitator-url", "", "x402 facilitator URL; empty uses the SDK default")
217
+ utils.StringFlag(fs, &f.facilitator, "x402-facilitator-url", "", "x402 facilitator URL")
218
utils.StringFlag(fs, &f.resource, "x402-resource", "", "x402 protected resource/root path; defaults to the HTTP route prefix")
219
utils.StringFlag(fs, &f.mimeType, "x402-mime-type", "", "x402 protected resource MIME type")
220
utils.BoolFlag(fs, &f.testnet, "x402-testnet", false, "Render the x402 paywall in testnet mode")
cmd/relay-server/main.go
+26
-1
@@ -16,6 +16,7 @@ import (
16
"github.com/gosuda/portal-tunnel/v2/portal/acme"
17
"github.com/gosuda/portal-tunnel/v2/portal/identity"
18
"github.com/gosuda/portal-tunnel/v2/portal/overlay"
19
+ portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
20
"github.com/gosuda/portal-tunnel/v2/types"
21
"github.com/gosuda/portal-tunnel/v2/utils"
22
)
@@ -51,6 +52,9 @@ type relayServerConfig struct {
52
HeadlessShellURL string
53
PProfEnabled bool
54
PProfAddr string
55
+ X402Enabled bool
56
+ X402Network string
57
+ X402RPCURL string
58
59
ACMEDNSProvider string
60
ENSGaslessEnabled bool
@@ -93,6 +97,9 @@ func runServeCommand(args []string) error {
97
utils.StringFlagEnv(fs, &cfg.HeadlessShellURL, "headless-shell-url", "", "headless Chrome CDP WebSocket URL for thumbnail generation (e.g. ws://headless-shell:9222)", "HEADLESS_SHELL_URL")
98
utils.BoolFlagEnv(fs, &cfg.PProfEnabled, "pprof-enabled", false, "enable pprof diagnostics HTTP server", "PPROF_ENABLED")
99
utils.StringFlagEnv(fs, &cfg.PProfAddr, "pprof-addr", portal.DefaultPProfListenAddr, "pprof diagnostics listen address when enabled", "PPROF_ADDR")
100
+ utils.BoolFlagEnv(fs, &cfg.X402Enabled, "x402-facilitator-enabled", false, "enable relay-local x402 facilitator endpoints under /x402", "X402_FACILITATOR_ENABLED")
101
+ utils.StringFlagEnv(fs, &cfg.X402Network, "x402-network", "", "x402 facilitator CAIP-2 network, such as eip155:8453", "X402_NETWORK")
102
+ utils.StringFlagEnv(fs, &cfg.X402RPCURL, "x402-rpc-url", "", "x402 facilitator RPC URL; empty uses the facilitator default for the network when available", "X402_RPC_URL")
103
104
utils.StringFlagEnv(fs, &cfg.ACMEDNSProvider, "acme-dns-provider", "", "DNS provider for managed DNS-01/A-record sync, ECH HTTPS records, and ENS gasless DNSSEC/TXT automation (cloudflare|gcloud|hetzner|njalla|route53|vultr); leave empty to use manual fullchain.pem/privatekey.pem from IDENTITY_PATH", "ACME_DNS_PROVIDER")
105
utils.BoolFlagEnv(fs, &cfg.ENSGaslessEnabled, "ens-gasless-enabled", false, "enable ENS gasless DNS import automation for the managed DNS zone and lease hostnames", "ENS_GASLESS_ENABLED")
@@ -141,6 +148,8 @@ func runServeCommand(args []string) error {
148
Bool("headless_shell_enabled", strings.TrimSpace(cfg.HeadlessShellURL) != "").
149
Bool("pprof_enabled", cfg.PProfEnabled).
150
Str("pprof_addr", cfg.PProfAddr).
151
+ Bool("x402_facilitator_enabled", cfg.X402Enabled).
152
+ Str("x402_network", strings.TrimSpace(cfg.X402Network)).
153
Str("acme_dns_provider", cfg.ACMEDNSProvider).
154
Bool("ens_gasless_enabled", cfg.ENSGaslessEnabled).
155
Msg("configured relay server")
@@ -196,7 +205,23 @@ func runServer(ctx context.Context, cfg relayServerConfig) error {
205
}
206
defer frontend.Close()
207
199
- if err := server.Start(ctx, frontend.Handler()); err != nil {
208
+ apiMux := frontend.Handler()
209
+ if cfg.X402Enabled {
210
+ relayIdentity := server.RelayIdentity()
211
+ if err := portalx402.MountFacilitator(apiMux, portalx402.FacilitatorConfig{
212
+ Network: cfg.X402Network,
213
+ RPCURL: cfg.X402RPCURL,
214
+ Identity: relayIdentity.Identity,
215
+ }); err != nil {
216
+ return err
217
+ }
218
+ log.Info().
219
+ Str("path", types.PathX402Facilitator).
220
+ Str("network", strings.TrimSpace(cfg.X402Network)).
221
+ Msg("x402 facilitator enabled")
222
+ }
223
+
224
+ if err := server.Start(ctx, apiMux); err != nil {
225
return fmt.Errorf("start relay server: %w", err)
226
}
227
docs/src/routes/cli-reference/+page.md
+22
-1
@@ -75,6 +75,7 @@ not supported.
75
|------|---------|-------|
76
| Default HTTPS stream | `portal expose 3000` | Relay routes by SNI; tunnel process terminates tenant TLS |
77
| Routed HTTP | `portal expose --http-route /api=3001 --http-route /=5173` | Tunnel process runs the HTTP reverse proxy |
78
+| Routed HTTP with x402 | `portal expose 3000 --x402-facilitator-url https://portal.example.com/x402 --x402-network eip155:8453 --x402-price "$0.001"` | Tunnel process enforces payment before proxying to the upstream |
79
| Dedicated raw TCP | `portal expose localhost:25565 --tcp` | Relay allocates a public TCP port |
80
| UDP relay | `portal expose 8080 --udp --udp-addr 19132` | Relay allocates a public UDP port |
81
@@ -97,6 +98,15 @@ not supported.
98
| `--owner` | string | | Service owner metadata |
99
| `--hide` | bool | `false` | Hide service from relay listing screens |
100
| `--http-route` | string | | HTTP route mapping in `PATH=UPSTREAM` form; repeatable |
101
+| `--x402-network` | string | | x402 payment network, such as `eip155:8453` |
102
+| `--x402-price` | string | | x402 route price, such as `$0.001` |
103
+| `--x402-pay-to` | string | identity | x402 recipient address; empty uses the tunnel identity address |
104
+| `--x402-facilitator-url` | string | | x402 facilitator URL |
105
+| `--x402-resource` | string | route prefix | x402 protected resource/root path |
106
+| `--x402-mime-type` | string | | x402 protected resource MIME type |
107
+| `--x402-testnet` | bool | `false` | Render the x402 paywall in testnet mode |
108
+| `--x402-max-timeout` | int | `0` | x402 max payment timeout seconds advertised to clients |
109
+| `--x402-payment-timeout` | int | `0` | x402 middleware verify/settle timeout seconds |
110
| `--tcp` | bool | `false` | Request a dedicated raw TCP port on the relay |
111
| `--udp` | bool | `false` | Enable public UDP relay in addition to the default stream path |
112
| `--udp-addr` | string | | Local UDP target; defaults to the primary target when `--udp` is enabled |
@@ -140,6 +150,17 @@ portal expose --name myapp \
150
Route matching is longest-prefix-first. `/api` matches `/api/*` and strips the
151
`/api` prefix before proxying to the upstream.
152
153
+Require x402 payment before a local upstream receives traffic:
154
+
155
+```bash
156
+portal expose 3000 --name paid-api \
157
+ --relays https://portal.example.com \
158
+ --discovery=false \
159
+ --x402-facilitator-url https://portal.example.com/x402 \
160
+ --x402-network eip155:8453 \
161
+ --x402-price "$0.001"
162
+```
163
+
164
Expose a Minecraft server:
165
166
```bash
@@ -201,7 +222,7 @@ portal agent restart
222
|---------|-------------|
223
| `portal agent run` | Install or update and start the managed agent service |
224
| `portal agent run --config config.toml --foreground` | Run the agent in the current terminal |
204
-| `portal agent dashboard` | Open the local TUI for tunnels, relays, multi-hop routes, and settings |
225
+| `portal agent dashboard` | Open the local TUI for tunnels, relays, multi-hop routes, settings, and x402 facilitator URLs |
226
| `portal agent stop` | Gracefully stop the agent and disable or stop the OS service |
227
| `portal agent restart` | Stop the current agent if present, install or update the service, and start it again |
228
docs/src/routes/configuration/+page.md
+12
@@ -65,6 +65,17 @@ The relay server (`relay-server`) reads configuration from environment variables
65
| `PPROF_ENABLED` | `false` | bool | Enable the relay pprof diagnostics HTTP server |
66
| `PPROF_ADDR` | `127.0.0.1:6060` | string | pprof listen address when enabled; keep it on loopback unless the port is protected |
67
68
+### Payments
69
+
70
+| Variable | Default | Type | Description |
71
+|----------|---------|------|-------------|
72
+| `X402_FACILITATOR_ENABLED` | `false` | bool | Enable the relay-local x402 facilitator under `/x402` |
73
+| `X402_NETWORK` | | string | CAIP-2 network served by the facilitator, such as `eip155:8453` |
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`.
78
+
79
### Cloudflare
80
81
| Variable | Default | Type | Description |
@@ -237,6 +248,7 @@ Tunnel fields mirror `portal expose` flags:
248
| `identity_json` | string | Identity JSON payload; overrides `identity_path` contents and is persisted there when both are set |
249
| `udp`, `udp_addr`, `tcp` | bool/string | UDP and raw TCP relay options |
250
| `description`, `tags`, `owner`, `thumbnail`, `hide` | mixed | Lease metadata shown by relays |
251
+| `http_routes.x402` | table | x402 payment settings for one HTTP route; set `facilitator_url` explicitly or let frontend/configuration tooling write it |
252
253
For a task-oriented walkthrough, see [Portal Agent](/portal-agent).
254
docs/src/routes/portal-agent/+page.md
+2
-2
@@ -126,7 +126,7 @@ Dashboard panes:
126
| Pane | Purpose |
127
|------|---------|
128
| Tunnels | Add, select, and delete simple target tunnels |
129
-| Settings | Edit max active relays and public metadata |
129
+| Settings | Edit max active relays, public metadata, and x402 facilitator URLs for x402 routes |
130
| Relays | Connect or disconnect relays for the selected tunnel |
131
| Multi-hop | Build and apply an ordered multi-hop route |
132
@@ -215,7 +215,7 @@ Control endpoints:
215
| `GET` | `/v1/agent/status` | Bearer token or wallet session | Read agent and tunnel status |
216
| `POST` | `/v1/agent/shutdown` | Bearer token | Ask the agent to stop |
217
| `POST` | `/v1/agent/tunnels` | Bearer token | Add a simple target tunnel |
218
-| `PATCH` | `/v1/agent/tunnels/{id}` | Bearer token | Update metadata or max active relays |
218
+| `PATCH` | `/v1/agent/tunnels/{id}` | Bearer token | Update metadata, max active relays, or x402 facilitator URL |
219
| `DELETE` | `/v1/agent/tunnels/{id}` | Bearer token | Delete a tunnel |
220
| `POST` | `/v1/agent/tunnels/{id}/relays` | Bearer token | Connect a relay |
221
| `DELETE` | `/v1/agent/tunnels/{id}/relays` | Bearer token | Disconnect a relay |
docs/src/routes/self-hosting/+page.md
+28
@@ -132,6 +132,34 @@ ports:
132
133
See [TCP/UDP Tunneling](/tcp-udp-tunneling) for usage details.
134
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.
140
+
141
+```yaml
142
+environment:
143
+ X402_FACILITATOR_ENABLED: "true"
144
+ X402_NETWORK: eip155:8453
145
+ X402_RPC_URL: https://base-mainnet.example
146
+```
147
+
148
+The relay-local facilitator uses the relay identity private key from
149
+`IDENTITY_PATH/identity.json`. Fund that identity only as required for
150
+settlement gas.
151
+
152
+For CLI-created x402 routes, pass the selected facilitator explicitly:
153
+
154
+```bash
155
+portal expose 3000 \
156
+ --relays https://relay.example.com:4017 \
157
+ --discovery=false \
158
+ --x402-facilitator-url https://relay.example.com:4017/x402 \
159
+ --x402-network eip155:8453 \
160
+ --x402-price "$0.001"
161
+```
162
+
163
## Troubleshooting
164
165
**Port already in use**
go.mod
+26
-3
@@ -12,11 +12,12 @@ require (
12
github.com/charmbracelet/bubbles v1.0.0
13
github.com/charmbracelet/bubbletea v1.3.10
14
github.com/charmbracelet/lipgloss v1.1.0
15
- github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0
15
+ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0
16
github.com/go-acme/lego/v4 v4.34.0
17
github.com/go-jose/go-jose/v4 v4.1.4
18
github.com/go-rod/rod v0.116.2
19
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008
20
+ github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5
21
github.com/hashicorp/yamux v0.1.2
22
github.com/hetznercloud/hcloud-go/v2 v2.40.0
23
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
@@ -43,8 +44,12 @@ require (
44
require (
45
cloud.google.com/go/auth v0.20.0 // indirect
46
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
47
+ filippo.io/edwards25519 v1.0.0-rc.1 // indirect
48
+ github.com/KyleBanks/depth v1.2.1 // indirect
49
github.com/Microsoft/go-winio v0.6.2 // indirect
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
54
github.com/atotto/clipboard v0.1.4 // indirect
55
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
@@ -61,6 +66,7 @@ require (
66
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
67
github.com/beorn7/perks v1.0.1 // indirect
68
github.com/bits-and-blooms/bitset v1.24.4 // indirect
69
+ github.com/blocto/solana-go-sdk v1.30.0 // indirect
70
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
71
github.com/cespare/xxhash/v2 v2.3.0 // indirect
72
github.com/charmbracelet/colorprofile v0.4.1 // indirect
@@ -73,15 +79,20 @@ require (
79
github.com/consensys/gnark-crypto v0.18.1 // indirect
80
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
81
github.com/dchest/uniuri v1.2.0 // indirect
76
- github.com/deckarep/golang-set/v2 v2.6.0 // indirect
82
+ github.com/deckarep/golang-set/v2 v2.8.0 // indirect
83
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
84
github.com/ethereum/c-kzg-4844/v2 v2.1.6 // indirect
85
github.com/ethereum/go-ethereum v1.17.1 // indirect
86
github.com/felixge/httpsnoop v1.0.4 // indirect
87
github.com/fsnotify/fsnotify v1.9.0 // indirect
88
+ github.com/ghodss/yaml v1.0.0 // indirect
89
github.com/go-logr/logr v1.4.3 // indirect
90
github.com/go-logr/stdr v1.2.2 // indirect
91
github.com/go-ole/go-ole v1.3.0 // indirect
92
+ github.com/go-openapi/jsonpointer v0.19.5 // indirect
93
+ github.com/go-openapi/jsonreference v0.19.6 // indirect
94
+ github.com/go-openapi/spec v0.20.4 // indirect
95
+ github.com/go-openapi/swag v0.19.15 // indirect
96
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
97
github.com/google/btree v1.1.2 // indirect
98
github.com/google/go-querystring v1.2.0 // indirect
@@ -93,8 +104,12 @@ require (
104
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
105
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
106
github.com/holiman/uint256 v1.3.2 // indirect
107
+ github.com/josharian/intern v1.0.0 // indirect
108
github.com/knadh/koanf/maps v0.1.2 // indirect
109
+ github.com/labstack/echo/v4 v4.15.1 // indirect
110
+ github.com/labstack/gommon v0.4.2 // indirect
111
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
112
+ github.com/mailru/easyjson v0.7.7 // indirect
113
github.com/mattn/go-colorable v0.1.14 // indirect
114
github.com/mattn/go-isatty v0.0.21 // indirect
115
github.com/mattn/go-localereader v0.0.1 // indirect
@@ -102,6 +117,7 @@ require (
117
github.com/miekg/dns v1.1.72 // indirect
118
github.com/mitchellh/copystructure v1.2.0 // indirect
119
github.com/mitchellh/reflectwalk v1.0.2 // indirect
120
+ github.com/mr-tron/base58 v1.2.0 // indirect
121
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
122
github.com/muesli/cancelreader v0.2.2 // indirect
123
github.com/muesli/termenv v0.16.0 // indirect
@@ -110,10 +126,15 @@ require (
126
github.com/prometheus/procfs v0.16.1 // indirect
127
github.com/relvacode/iso8601 v1.1.1-0.20210511065120-b30b151cc433 // indirect
128
github.com/rivo/uniseg v0.4.7 // indirect
113
- github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
129
+ github.com/shirou/gopsutil v3.21.11+incompatible // indirect
130
github.com/supranational/blst v0.3.16 // indirect
131
+ github.com/swaggo/echo-swagger v1.4.1 // indirect
132
+ github.com/swaggo/files/v2 v2.0.0 // indirect
133
+ github.com/swaggo/swag v1.16.4 // indirect
134
github.com/tklauser/go-sysconf v0.3.12 // indirect
135
github.com/tklauser/numcpus v0.6.1 // indirect
136
+ github.com/valyala/bytebufferpool v1.0.0 // indirect
137
+ github.com/valyala/fasttemplate v1.2.2 // indirect
138
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
139
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
140
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
@@ -123,6 +144,7 @@ require (
144
github.com/ysmood/got v0.40.0 // indirect
145
github.com/ysmood/gson v0.7.3 // indirect
146
github.com/ysmood/leakless v0.9.0 // indirect
147
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
148
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
149
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
150
go.opentelemetry.io/otel v1.43.0 // indirect
@@ -137,6 +159,7 @@ require (
159
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
160
google.golang.org/grpc v1.80.0 // indirect
161
google.golang.org/protobuf v1.36.11 // indirect
162
+ gopkg.in/yaml.v2 v2.4.0 // indirect
163
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
164
)
165
go.sum
+76
@@ -4,12 +4,20 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi
4
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
5
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
6
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
7
+filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
8
+filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
9
github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
10
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
11
+github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
12
+github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
13
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
14
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
15
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
16
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
17
+github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
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=
23
github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
@@ -54,6 +62,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
62
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
63
github.com/bits-and-blooms/bitset v1.24.4 h1:95H15Og1clikBrKr/DuzMXkQzECs1M6hhoGXLwLQOZE=
64
github.com/bits-and-blooms/bitset v1.24.4/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
65
+github.com/blocto/solana-go-sdk v1.30.0 h1:GEh4GDjYk1lMhV/hqJDCyuDeCuc5dianbN33yxL88NU=
66
+github.com/blocto/solana-go-sdk v1.30.0/go.mod h1:Xoyhhb3hrGpEQ5rJps5a3OgMwDpmEhrd9bgzFKkkwMs=
67
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
68
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
69
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -97,7 +107,9 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3
107
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
108
github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
109
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
110
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
111
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
112
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
113
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
114
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
115
github.com/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA=
@@ -106,10 +118,15 @@ 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=
121
+github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ=
122
+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=
125
+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=
128
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
129
+github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
130
github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
131
github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
132
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
@@ -130,6 +147,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
147
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
148
github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps=
149
github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY=
150
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
151
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
152
github.com/go-acme/lego/v4 v4.34.0 h1:oRsIuPJ4ORX7ufviXvelUpBSez2XxeKGwo5pNG9BVeY=
153
github.com/go-acme/lego/v4 v4.34.0/go.mod h1:gsmdlx/ZS6OUeXbOj0U+VnCLLfEFj4WCYRkcGpZw+pc=
154
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
@@ -140,8 +159,19 @@ github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
159
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
160
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=
162
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
163
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
164
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
165
+github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
166
+github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
167
+github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
168
+github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
169
+github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
170
+github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
171
+github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
172
+github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
173
+github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
174
+github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
175
github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA=
176
github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg=
177
github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=
@@ -178,6 +208,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
208
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
209
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008 h1:KuP/5VlPJwqZNyAV5U60C/j8Pc5O8ENkWPTgP7mEvj0=
210
github.com/gosuda/keyless_tls v0.0.2-0.20260507061030-5128be6b5008/go.mod h1:BOhUZgiAAQzxKO3QcC4fCXgd/+lqxgIu1OyIYTqtta8=
211
+github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5 h1:lFZ+IYBiGRrFgBJl0NX2u/KAAX3Epj2y3vkJnbYWdLg=
212
+github.com/gosuda/x402-facilitator v0.0.0-20260413025142-cb6c4794b9a5/go.mod h1:rcqknkHCWwkH9EIDlGAAIvAZkYv3U6q4wPvYKwt7ynU=
213
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
214
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
215
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
@@ -204,6 +236,8 @@ 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/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
238
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
239
+github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
240
+github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
241
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
242
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
243
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
@@ -216,16 +250,28 @@ github.com/knadh/koanf/providers/file v1.2.1 h1:bEWbtQwYrA+W2DtdBrQWyXqJaJSG3KrP
250
github.com/knadh/koanf/providers/file v1.2.1/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
251
github.com/knadh/koanf/v2 v2.3.4 h1:fnynNSDlujWE+v83hAp8wKr/cdoxHLO0629SN+U8Urc=
252
github.com/knadh/koanf/v2 v2.3.4/go.mod h1:gRb40VRAbd4iJMYYD5IxZ6hfuopFcXBpc9bbQpZwo28=
253
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
254
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
255
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
256
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
257
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
258
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
259
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
260
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
261
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
262
+github.com/labstack/echo/v4 v4.15.1 h1:S9keusg26gZpjMmPqB5hOEvNKnmd1lNmcHrbbH2lnFs=
263
+github.com/labstack/echo/v4 v4.15.1/go.mod h1:xmw1clThob0BSVRX1CRQkGQ/vjwcpOMjQZSZa9fKA/c=
264
+github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
265
+github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
266
github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4=
267
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
268
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
269
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
270
+github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
271
+github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
272
+github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
273
+github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
274
+github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
275
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
276
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
277
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
@@ -251,6 +297,8 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx
297
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
298
github.com/montanaflynn/stats v0.9.0 h1:tsBJ0RXwph9BmAuFoCmqGv6e8xa0MENQ8m0ptKq29mQ=
299
github.com/montanaflynn/stats v0.9.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
300
+github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
301
+github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
302
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
303
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
304
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
@@ -259,6 +307,7 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc
307
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
308
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
309
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
310
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
311
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
312
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
313
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
@@ -301,14 +350,23 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf
350
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=
353
+github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
354
+github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
355
github.com/spruceid/siwe-go v0.2.1 h1:BroySys6CyUzeyNppTseEOT/w56xTdOfcmECTI7rnuc=
356
github.com/spruceid/siwe-go v0.2.1/go.mod h1:MHpHbptGsM3lHth2L8quhZ9ipiwST8zsJH1CjWpeO1k=
357
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
358
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
359
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
360
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
361
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
362
github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE=
363
github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
364
+github.com/swaggo/echo-swagger v1.4.1 h1:Yf0uPaJWp1uRtDloZALyLnvdBeoEL5Kc7DtnjzO/TUk=
365
+github.com/swaggo/echo-swagger v1.4.1/go.mod h1:C8bSi+9yH2FLZsnhqMZLIZddpUxZdBYuNHbtaS1Hljc=
366
+github.com/swaggo/files/v2 v2.0.0 h1:hmAt8Dkynw7Ssz46F6pn8ok6YmGZqHSVLZ+HQM7i0kw=
367
+github.com/swaggo/files/v2 v2.0.0/go.mod h1:24kk2Y9NYEJ5lHuCra6iVwkMjIekMCaFq/0JQj66kyM=
368
+github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
369
+github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
370
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY=
371
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
372
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
@@ -317,6 +375,10 @@ github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+F
375
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
376
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
377
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
378
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
379
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
380
+github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
381
+github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
382
github.com/vultr/govultr/v3 v3.30.0 h1:kTeDJ+5or6g4CQJmD6Kmz4R63B18poNZ8RP87r9LZdg=
383
github.com/vultr/govultr/v3 v3.30.0/go.mod h1:2zyUw9yADQaGwKnwDesmIOlBNLrm7edsCfWHFJpWKf8=
384
github.com/x402-foundation/x402/go v0.0.0-20260526081544-8cf020c5335e h1:0N+e1CjhzAqm6CKn9zhimQqtpjrTjwbx5+IMvcgyXHM=
@@ -347,6 +409,8 @@ github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE=
409
github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg=
410
github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU=
411
github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ=
412
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
413
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
414
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
415
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
416
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04=
@@ -375,6 +439,7 @@ golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJk
439
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
440
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
441
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
442
+golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
443
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
444
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
445
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
@@ -382,6 +447,8 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7
447
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
448
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
449
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
450
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
451
+golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
452
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
453
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
454
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -391,10 +458,14 @@ golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
458
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
459
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
460
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
461
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
462
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
463
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
464
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
465
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
466
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
467
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
468
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
469
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
470
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
471
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
@@ -416,12 +487,17 @@ google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07
487
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
488
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
489
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
490
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
491
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
492
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
493
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
494
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
495
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
496
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
497
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
498
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
499
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
500
+gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
501
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
502
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
503
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
portal/x402/x402.go
new
+143
@@ -0,0 +1,143 @@
1
+package x402
2
+
3
+import (
4
+ "errors"
5
+ "fmt"
6
+ "net/http"
7
+ "strings"
8
+ "time"
9
+
10
+ facilitatorapi "github.com/gosuda/x402-facilitator/api"
11
+ facilitatorcore "github.com/gosuda/x402-facilitator/facilitator"
12
+ facilitatortypes "github.com/gosuda/x402-facilitator/types"
13
+ foundationx402 "github.com/x402-foundation/x402/go"
14
+ x402http "github.com/x402-foundation/x402/go/http"
15
+ x402nethttp "github.com/x402-foundation/x402/go/http/nethttp"
16
+ evmserver "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server"
17
+
18
+ "github.com/gosuda/portal-tunnel/v2/portal/identity"
19
+ "github.com/gosuda/portal-tunnel/v2/types"
20
+)
21
+
22
+const (
23
+ defaultPaymentTimeout = 30 * time.Second
24
+ defaultRouteDescription = "Portal protected route"
25
+)
26
+
27
+type FacilitatorConfig struct {
28
+ Network string
29
+ RPCURL string
30
+ Identity types.Identity
31
+}
32
+
33
+func MountFacilitator(mux *http.ServeMux, cfg FacilitatorConfig) error {
34
+ if mux == nil {
35
+ return errors.New("x402 facilitator requires an api mux")
36
+ }
37
+ network := strings.TrimSpace(cfg.Network)
38
+ if network == "" {
39
+ return errors.New("--x402-network is required when --x402-facilitator-enabled is set")
40
+ }
41
+ privateKey := strings.TrimSpace(cfg.Identity.PrivateKey)
42
+ if privateKey == "" {
43
+ return errors.New("relay identity private key is required when --x402-facilitator-enabled is set")
44
+ }
45
+ facilitator, err := facilitatorcore.NewFacilitator(facilitatortypes.Exact, network, strings.TrimSpace(cfg.RPCURL), privateKey)
46
+ if err != nil {
47
+ return fmt.Errorf("create x402 facilitator: %w", err)
48
+ }
49
+ mux.Handle(types.PathX402FacilitatorPrefix, http.StripPrefix(types.PathX402Facilitator, facilitatorapi.NewServer(facilitator)))
50
+ return nil
51
+}
52
+
53
+type HTTPRouteHandlerConfig struct {
54
+ Prefix string
55
+ Next http.Handler
56
+ X402 types.X402Config
57
+ TunnelIdentity types.Identity
58
+ Metadata types.LeaseMetadata
59
+}
60
+
61
+func NewHTTPRouteHandler(cfg HTTPRouteHandlerConfig) (http.Handler, error) {
62
+ next := cfg.Next
63
+ if next == nil {
64
+ next = http.NotFoundHandler()
65
+ }
66
+ prefix := strings.TrimSpace(cfg.Prefix)
67
+ if prefix == "" {
68
+ prefix = "/"
69
+ }
70
+ network := strings.TrimSpace(cfg.X402.Network)
71
+ if network == "" {
72
+ return nil, fmt.Errorf("http route %q x402 network is required", prefix)
73
+ }
74
+ price := strings.TrimSpace(cfg.X402.Price)
75
+ if price == "" {
76
+ return nil, fmt.Errorf("http route %q x402 price is required", prefix)
77
+ }
78
+ payTo := strings.TrimSpace(cfg.X402.PayTo)
79
+ if payTo == "" || strings.EqualFold(payTo, types.X402PayToIdentity) {
80
+ payTo = strings.TrimSpace(cfg.TunnelIdentity.Address)
81
+ }
82
+ if payTo == "" {
83
+ return nil, fmt.Errorf("http route %q x402 pay_to is required", prefix)
84
+ }
85
+ payTo, err := identity.NormalizeEVMAddress(payTo)
86
+ if err != nil {
87
+ return nil, fmt.Errorf("http route %q x402 pay_to: %w", prefix, err)
88
+ }
89
+ if cfg.X402.PaymentTimeoutSecs < 0 {
90
+ return nil, errors.New("x402 payment_timeout_seconds cannot be negative")
91
+ }
92
+ if cfg.X402.MaxTimeoutSeconds < 0 {
93
+ return nil, errors.New("x402 max_timeout_seconds cannot be negative")
94
+ }
95
+
96
+ timeout := defaultPaymentTimeout
97
+ if cfg.X402.PaymentTimeoutSecs > 0 {
98
+ timeout = time.Duration(cfg.X402.PaymentTimeoutSecs) * time.Second
99
+ }
100
+ resource := strings.TrimSpace(cfg.X402.Resource)
101
+ if resource == "" {
102
+ resource = prefix
103
+ }
104
+ description := strings.TrimSpace(cfg.Metadata.Description)
105
+ if description == "" {
106
+ description = defaultRouteDescription
107
+ }
108
+ middleware := x402nethttp.X402Payment(x402nethttp.Config{
109
+ Routes: x402http.RoutesConfig{
110
+ "*": x402http.RouteConfig{
111
+ Accepts: []x402http.PaymentOption{
112
+ {
113
+ Scheme: types.X402SchemeExact,
114
+ PayTo: payTo,
115
+ Price: foundationx402.Price(price),
116
+ Network: foundationx402.Network(network),
117
+ MaxTimeoutSeconds: cfg.X402.MaxTimeoutSeconds,
118
+ },
119
+ },
120
+ Resource: resource,
121
+ Description: description,
122
+ MimeType: strings.TrimSpace(cfg.X402.MimeType),
123
+ },
124
+ },
125
+ Facilitator: x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
126
+ URL: strings.TrimSpace(cfg.X402.FacilitatorURL),
127
+ }),
128
+ Schemes: []x402nethttp.SchemeConfig{
129
+ {
130
+ Network: foundationx402.Network("eip155:*"),
131
+ Server: evmserver.NewExactEvmScheme(),
132
+ },
133
+ },
134
+ PaywallConfig: &x402http.PaywallConfig{
135
+ AppName: strings.TrimSpace(cfg.TunnelIdentity.Name),
136
+ AppLogo: strings.TrimSpace(cfg.Metadata.Thumbnail),
137
+ Testnet: cfg.X402.Testnet,
138
+ },
139
+ SyncFacilitatorOnStart: true,
140
+ Timeout: timeout,
141
+ })
142
+ return middleware(next), nil
143
+}
sdk/http.go
+8
-90
@@ -15,27 +15,15 @@ import (
15
"strconv"
16
"strings"
17
"sync"
18
- "time"
18
19
"github.com/andybalholm/brotli"
20
"github.com/rs/zerolog/log"
22
- x402 "github.com/x402-foundation/x402/go"
23
- x402http "github.com/x402-foundation/x402/go/http"
24
- x402nethttp "github.com/x402-foundation/x402/go/http/nethttp"
25
- evmserver "github.com/x402-foundation/x402/go/mechanisms/evm/exact/server"
21
27
- "github.com/gosuda/portal-tunnel/v2/portal/identity"
22
+ portalx402 "github.com/gosuda/portal-tunnel/v2/portal/x402"
23
"github.com/gosuda/portal-tunnel/v2/types"
24
"github.com/gosuda/portal-tunnel/v2/utils"
25
)
26
32
-const (
33
- defaultX402Scheme = "exact"
34
- defaultX402PaymentTimeout = 30 * time.Second
35
- defaultX402RouteDescription = "Portal protected route"
36
- defaultX402PayToIdentityValue = "identity"
37
-)
38
-
27
func RunHTTP(ctx context.Context, relayListener net.Listener, handler http.Handler, localAddr string) error {
28
if relayListener == nil && localAddr == "" {
29
return errors.New("relay listener or local address is required")
@@ -177,7 +165,13 @@ func newHTTPRouteHandler(routeConfigs []HTTPRoute, tunnelIdentity types.Identity
165
seen[route.prefix] = struct{}{}
166
var handler http.Handler = route.newReverseProxy()
167
if routeConfig.X402 != nil && !routeConfig.X402.Empty() {
180
- handler, err = newX402HTTPRouteHandler(route, handler, *routeConfig.X402, tunnelIdentity, metadata)
168
+ handler, err = portalx402.NewHTTPRouteHandler(portalx402.HTTPRouteHandlerConfig{
169
+ Prefix: route.prefix,
170
+ Next: handler,
171
+ X402: *routeConfig.X402,
172
+ TunnelIdentity: tunnelIdentity,
173
+ Metadata: metadata,
174
+ })
175
if err != nil {
176
return nil, err
177
}
@@ -421,82 +415,6 @@ func (r *httpRoute) upstreamPathToPublic(raw string) string {
415
return r.prefix + rest
416
}
417
424
-func newX402HTTPRouteHandler(route *httpRoute, next http.Handler, cfg types.X402Config, tunnelIdentity types.Identity, metadata types.LeaseMetadata) (http.Handler, error) {
425
- network := strings.TrimSpace(cfg.Network)
426
- if network == "" {
427
- return nil, fmt.Errorf("http route %q x402 network is required", route.prefix)
428
- }
429
- price := strings.TrimSpace(cfg.Price)
430
- if price == "" {
431
- return nil, fmt.Errorf("http route %q x402 price is required", route.prefix)
432
- }
433
- payTo := strings.TrimSpace(cfg.PayTo)
434
- if payTo == "" || strings.EqualFold(payTo, defaultX402PayToIdentityValue) {
435
- payTo = strings.TrimSpace(tunnelIdentity.Address)
436
- }
437
- if payTo == "" {
438
- return nil, fmt.Errorf("http route %q x402 pay_to is required", route.prefix)
439
- }
440
- payTo, err := identity.NormalizeEVMAddress(payTo)
441
- if err != nil {
442
- return nil, fmt.Errorf("http route %q x402 pay_to: %w", route.prefix, err)
443
- }
444
- if cfg.PaymentTimeoutSecs < 0 {
445
- return nil, errors.New("x402 payment_timeout_seconds cannot be negative")
446
- }
447
- if cfg.MaxTimeoutSeconds < 0 {
448
- return nil, errors.New("x402 max_timeout_seconds cannot be negative")
449
- }
450
-
451
- timeout := defaultX402PaymentTimeout
452
- if cfg.PaymentTimeoutSecs > 0 {
453
- timeout = time.Duration(cfg.PaymentTimeoutSecs) * time.Second
454
- }
455
- resource := strings.TrimSpace(cfg.Resource)
456
- if resource == "" {
457
- resource = route.prefix
458
- }
459
- description := strings.TrimSpace(metadata.Description)
460
- if description == "" {
461
- description = defaultX402RouteDescription
462
- }
463
- middleware := x402nethttp.X402Payment(x402nethttp.Config{
464
- Routes: x402http.RoutesConfig{
465
- "*": x402http.RouteConfig{
466
- Accepts: []x402http.PaymentOption{
467
- {
468
- Scheme: defaultX402Scheme,
469
- PayTo: payTo,
470
- Price: x402.Price(price),
471
- Network: x402.Network(network),
472
- MaxTimeoutSeconds: cfg.MaxTimeoutSeconds,
473
- },
474
- },
475
- Resource: resource,
476
- Description: description,
477
- MimeType: strings.TrimSpace(cfg.MimeType),
478
- },
479
- },
480
- Facilitator: x402http.NewHTTPFacilitatorClient(&x402http.FacilitatorConfig{
481
- URL: strings.TrimSpace(cfg.FacilitatorURL),
482
- }),
483
- Schemes: []x402nethttp.SchemeConfig{
484
- {
485
- Network: x402.Network("eip155:*"),
486
- Server: evmserver.NewExactEvmScheme(),
487
- },
488
- },
489
- PaywallConfig: &x402http.PaywallConfig{
490
- AppName: strings.TrimSpace(tunnelIdentity.Name),
491
- AppLogo: strings.TrimSpace(metadata.Thumbnail),
492
- Testnet: cfg.Testnet,
493
- },
494
- SyncFacilitatorOnStart: true,
495
- Timeout: timeout,
496
- })
497
- return middleware(next), nil
498
-}
499
-
418
func serveCompressedHTTP(handler http.Handler, w http.ResponseWriter, r *http.Request) {
419
if handler == nil {
420
http.NotFound(w, r)
types/agent.go
+17
-13
@@ -8,16 +8,18 @@ type AgentStatusResponse struct {
8
}
9
10
type AgentTunnelStatus struct {
11
- ID string `json:"id"`
12
- Name string `json:"name,omitempty"`
13
- Address string `json:"address,omitempty"`
14
- State string `json:"state"`
15
- TargetAddr string `json:"target_addr,omitempty"`
16
- LastError string `json:"last_error,omitempty"`
17
- MaxActiveRelays int `json:"max_active_relays,omitempty"`
18
- Metadata LeaseMetadata `json:"metadata,omitempty"`
19
- MultiHop []string `json:"multi_hop,omitempty"`
20
- Relays []AgentRelayStatus `json:"relays,omitempty"`
11
+ ID string `json:"id"`
12
+ Name string `json:"name,omitempty"`
13
+ Address string `json:"address,omitempty"`
14
+ State string `json:"state"`
15
+ TargetAddr string `json:"target_addr,omitempty"`
16
+ LastError string `json:"last_error,omitempty"`
17
+ MaxActiveRelays int `json:"max_active_relays,omitempty"`
18
+ Metadata LeaseMetadata `json:"metadata,omitempty"`
19
+ X402Enabled bool `json:"x402_enabled,omitempty"`
20
+ X402FacilitatorURL string `json:"x402_facilitator_url,omitempty"`
21
+ MultiHop []string `json:"multi_hop,omitempty"`
22
+ Relays []AgentRelayStatus `json:"relays,omitempty"`
23
}
24
25
type AgentRelayStatus struct {
@@ -49,13 +51,15 @@ type AgentMultiHopRequest struct {
51
}
52
53
type AgentTunnelUpdateRequest struct {
52
- MaxActiveRelays *int `json:"max_active_relays,omitempty"`
53
- Metadata *AgentMetadataRequest `json:"metadata,omitempty"`
54
+ MaxActiveRelays *int `json:"max_active_relays,omitempty"`
55
+ Metadata *AgentMetadataRequest `json:"metadata,omitempty"`
56
+ X402FacilitatorURL *string `json:"x402_facilitator_url,omitempty"`
57
}
58
59
func (r AgentTunnelUpdateRequest) Empty() bool {
60
return r.MaxActiveRelays == nil &&
58
- (r.Metadata == nil || r.Metadata.Empty())
61
+ (r.Metadata == nil || r.Metadata.Empty()) &&
62
+ r.X402FacilitatorURL == nil
63
}
64
65
type AgentMetadataRequest struct {
types/paths.go
+6
@@ -48,4 +48,10 @@ const (
48
PathSDKConnect = "/sdk/connect"
49
PathDiscovery = "/discovery"
50
PathDiscoveryAnnounce = "/discovery/announce"
51
+
52
+ PathX402Facilitator = "/x402"
53
+ PathX402FacilitatorPrefix = PathX402Facilitator + "/"
54
+ X402SupportedPath = PathX402Facilitator + "/supported"
55
+ X402VerifyPath = PathX402Facilitator + "/verify"
56
+ X402SettlePath = PathX402Facilitator + "/settle"
57
)
types/x402.go
+18
@@ -1,5 +1,10 @@
1
package types
2
3
+const (
4
+ X402SchemeExact = "exact"
5
+ X402PayToIdentity = "identity"
6
+)
7
+
8
type X402Config struct {
9
Network string `json:"network,omitempty" koanf:"network"`
10
Price string `json:"price,omitempty" koanf:"price"`
@@ -23,3 +28,16 @@ func (c X402Config) Empty() bool {
28
c.MaxTimeoutSeconds == 0 &&
29
c.PaymentTimeoutSecs == 0
30
}
31
+
32
+type X402SupportedKind struct {
33
+ X402Version int `json:"x402Version"`
34
+ Scheme string `json:"scheme"`
35
+ Network string `json:"network"`
36
+ Extra map[string]any `json:"extra,omitempty"`
37
+}
38
+
39
+type X402SupportedResponse struct {
40
+ Kinds []X402SupportedKind `json:"kinds"`
41
+ Extensions []string `json:"extensions"`
42
+ Signers map[string][]string `json:"signers"`
43
+}