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 +}