| 1 | --- |
| 2 | title: CLI Reference |
| 3 | description: Complete reference for Portal CLI commands, flags, and usage examples. |
| 4 | --- |
| 5 | |
| 6 | # CLI Reference |
| 7 | |
| 8 | The `portal` CLI exposes local services through Portal relay servers. The relay |
| 9 | provides transport and routing. The tunnel process decides whether a connection |
| 10 | is handled as the default HTTPS stream, routed HTTP, raw TCP, or UDP. |
| 11 | |
| 12 | ## Install |
| 13 | |
| 14 | ### macOS / Linux |
| 15 | |
| 16 | ```bash |
| 17 | curl -fsSL https://github.com/gosuda/portal-tunnel/releases/latest/download/install.sh | bash |
| 18 | ``` |
| 19 | |
| 20 | ### Windows PowerShell |
| 21 | |
| 22 | ```powershell |
| 23 | $ProgressPreference = 'SilentlyContinue' |
| 24 | irm https://github.com/gosuda/portal-tunnel/releases/latest/download/install.ps1 | iex |
| 25 | ``` |
| 26 | |
| 27 | ### From A Relay |
| 28 | |
| 29 | If your relay publishes its own installer: |
| 30 | |
| 31 | ```bash |
| 32 | curl -sSL https://portal.example.com/api/install.sh | bash |
| 33 | ``` |
| 34 | |
| 35 | The installer downloads the `portal` binary and adds it to your `PATH`. It does |
| 36 | not create a config file. |
| 37 | |
| 38 | ## Command Overview |
| 39 | |
| 40 | | Command | Purpose | |
| 41 | |---------|---------| |
| 42 | | `portal expose` | Expose one local service or one routed HTTP bundle | |
| 43 | | `portal list` | Print relay URLs resolved for this invocation | |
| 44 | | `portal agent` | Run a durable local multi-tunnel agent | |
| 45 | | `portal update` | Replace the CLI with the latest release | |
| 46 | | `portal version` | Print the current version | |
| 47 | |
| 48 | ## `portal expose` |
| 49 | |
| 50 | Expose a local service: |
| 51 | |
| 52 | ```bash |
| 53 | portal expose [flags] <target> |
| 54 | ``` |
| 55 | |
| 56 | Or run routed HTTP mode: |
| 57 | |
| 58 | ```bash |
| 59 | portal expose [flags] --http-route "PATH=UPSTREAM [METHOD[,METHOD...]:USDC_AMOUNT]" [...] |
| 60 | ``` |
| 61 | |
| 62 | The payment suffix is optional; omit it for free routes. |
| 63 | |
| 64 | ### Target Formats |
| 65 | |
| 66 | | Format | Example | Resolves to | |
| 67 | |--------|---------|-------------| |
| 68 | | Bare port | `3000` | `127.0.0.1:3000` | |
| 69 | | Host and port | `localhost:8080` | `localhost:8080` | |
| 70 | | URL host | `http://127.0.0.1:3000` | `127.0.0.1:3000` | |
| 71 | |
| 72 | URL inputs are accepted for address parsing. Paths, queries, and fragments are |
| 73 | not supported. |
| 74 | |
| 75 | ### Mode Selection |
| 76 | |
| 77 | | Mode | Example | Notes | |
| 78 | |------|---------|-------| |
| 79 | | Default HTTPS stream | `portal expose 3000` | Relay routes by SNI; tunnel process terminates tenant TLS | |
| 80 | | Routed HTTP | `portal expose --http-route /api=3001 --http-route /=5173` | Tunnel process runs the HTTP reverse proxy | |
| 81 | | Dedicated raw TCP | `portal expose localhost:25565 --tcp` | Relay allocates a public TCP port | |
| 82 | | UDP relay | `portal expose 8080 --udp --udp-addr 19132` | Relay allocates a public UDP port | |
| 83 | |
| 84 | ### Flags |
| 85 | |
| 86 | | Flag | Type | Default | Description | |
| 87 | |------|------|---------|-------------| |
| 88 | | `--relays` | string | registry | Additional relay API URLs, comma-separated | |
| 89 | | `--discovery` | bool | `true` | Include registry relays and relay discovery expansion | |
| 90 | | `--max-active-relays` | int | `3` | Maximum auto-selected relays to keep connected; explicit relays are always included | |
| 91 | | `--multi-hop` | string | | Ordered multi-hop relay API URLs, comma-separated | |
| 92 | | `--multi-hop-depth` | int | `0` | Automatically select one multi-hop route with this hop count; `0` or `1` disables multi-hop | |
| 93 | | `--ban-mitm` | bool | `false` | Ban relay when the MITM self-probe detects TLS termination | |
| 94 | | `--identity-path` | string | `identity.json` | Identity JSON file path; created automatically when missing | |
| 95 | | `--identity-json` | string | | Identity JSON payload; overrides `--identity-path` contents and is persisted there when both are set | |
| 96 | | `--name` | string | auto | Public hostname prefix, one DNS label | |
| 97 | | `--description` | string | | Service description metadata | |
| 98 | | `--tags` | string | | Service tags metadata, comma-separated | |
| 99 | | `--thumbnail` | string | | Service thumbnail URL metadata | |
| 100 | | `--owner` | string | | Service owner metadata | |
| 101 | | `--hide` | bool | `false` | Hide service from relay listing screens | |
| 102 | | `--x402-pay-to` | string | | Sui USDC payment recipient address for this tunnel | |
| 103 | | `--x402-testnet` | bool | `false` | Use Sui testnet for tunnel x402 payments; default is Sui mainnet | |
| 104 | | `--http-route` | string | | HTTP route mapping in `PATH=UPSTREAM [METHOD[,METHOD...]:USDC_AMOUNT]` form; repeatable; route amounts require `--x402-pay-to` | |
| 105 | | `--tcp` | bool | `false` | Request a dedicated raw TCP port on the relay | |
| 106 | | `--udp` | bool | `false` | Enable public UDP relay in addition to the default stream path | |
| 107 | | `--udp-addr` | string | | Local UDP target; defaults to the primary target when `--udp` is enabled | |
| 108 | | `--metrics-addr` | string | | Optional `host:port` for Prometheus `/metrics` | |
| 109 | |
| 110 | ### Constraints |
| 111 | |
| 112 | - `<target>` cannot be combined with `--http-route`. |
| 113 | - `--http-route` cannot be combined with `--udp`. |
| 114 | - Explicit `--multi-hop` cannot be combined with automatic `--multi-hop-depth`. |
| 115 | - Multi-hop currently supports only the default SNI TLS stream transport. |
| 116 | - `--tcp` and `--udp` require matching transport support on the relay. |
| 117 | - Route payment amounts are part of `--http-route` and require a tunnel-owned |
| 118 | `--x402-pay-to`. |
| 119 | - Tunnel paid routes use Sui mainnet by default; add `--x402-testnet` for Sui |
| 120 | testnet. This is independent of relay-owned x402 facilitator settings. |
| 121 | |
| 122 | ### Examples |
| 123 | |
| 124 | Expose a local web app: |
| 125 | |
| 126 | ```bash |
| 127 | portal expose 3000 |
| 128 | ``` |
| 129 | |
| 130 | Use a custom name and relay: |
| 131 | |
| 132 | ```bash |
| 133 | portal expose localhost:8080 \ |
| 134 | --name myapp \ |
| 135 | --relays https://portal.example.com \ |
| 136 | --discovery=false \ |
| 137 | --description "My web application" \ |
| 138 | --tags webapp,demo |
| 139 | ``` |
| 140 | |
| 141 | Run routed HTTP mode: |
| 142 | |
| 143 | ```bash |
| 144 | portal expose --name myapp \ |
| 145 | --http-route /api=http://127.0.0.1:3001 \ |
| 146 | --http-route /=http://127.0.0.1:5173 |
| 147 | ``` |
| 148 | |
| 149 | Route matching is longest-prefix-first. `/api` matches `/api/*` and strips the |
| 150 | `/api` prefix before proxying to the upstream. |
| 151 | |
| 152 | Expose a Minecraft server: |
| 153 | |
| 154 | ```bash |
| 155 | portal expose localhost:25565 --name minecraft --tcp |
| 156 | ``` |
| 157 | |
| 158 | Enable UDP alongside the default stream target: |
| 159 | |
| 160 | ```bash |
| 161 | portal expose localhost:8080 --udp --udp-addr localhost:19132 --name game |
| 162 | ``` |
| 163 | |
| 164 | Use an explicit multi-hop route: |
| 165 | |
| 166 | ```bash |
| 167 | portal expose 3000 --multi-hop https://entry.example.com,https://exit.example.com |
| 168 | ``` |
| 169 | |
| 170 | Ask Portal to select one three-hop route: |
| 171 | |
| 172 | ```bash |
| 173 | portal expose 3000 --multi-hop-depth 3 |
| 174 | ``` |
| 175 | |
| 176 | Ban relays on MITM probe detection: |
| 177 | |
| 178 | ```bash |
| 179 | portal expose 3000 --ban-mitm |
| 180 | ``` |
| 181 | |
| 182 | Publish a paid HTTP route: |
| 183 | |
| 184 | ```bash |
| 185 | portal expose --name paid-app \ |
| 186 | --http-route "/paid=http://127.0.0.1:3001 GET:0.01" \ |
| 187 | --http-route /=http://127.0.0.1:5173 \ |
| 188 | --x402-pay-to 0x... |
| 189 | ``` |
| 190 | |
| 191 | The optional method list limits which methods require payment; without it, every |
| 192 | method on that route prefix is paid. |
| 193 | |
| 194 | The routed HTTP handler also serves `/x402/client.js` and `/x402/prepare` on the |
| 195 | public tunnel origin. Frontends served by one of the routes can use the shared |
| 196 | browser-only Sui wallet client for an in-page payment flow: |
| 197 | |
| 198 | ```js |
| 199 | import { getSuiWallets, x402Fetch } from '/x402/client.js'; |
| 200 | |
| 201 | const [wallet] = getSuiWallets(); |
| 202 | if (!wallet) { |
| 203 | throw new Error('Install a Sui wallet'); |
| 204 | } |
| 205 | |
| 206 | const [account] = await wallet.accounts(); |
| 207 | if (!account) { |
| 208 | throw new Error('Connect a Sui account'); |
| 209 | } |
| 210 | |
| 211 | const response = await x402Fetch('/paid/photo', { method: 'GET' }, { |
| 212 | wallet, |
| 213 | account, |
| 214 | onEvent: (event) => console.log(event.type, event.message), |
| 215 | }); |
| 216 | ``` |
| 217 | |
| 218 | `x402Fetch()` is a convenience wrapper: it asks `/x402/prepare` for the payment |
| 219 | transaction, asks the wallet to sign it, then retries the protected request with |
| 220 | an `X-PAYMENT` header. `onEvent` receives structured progress events; the older |
| 221 | `onStatus(message)` callback is still accepted for simple UIs. Routed HTTP |
| 222 | payments use Sui mainnet by default; pass `--x402-testnet` when exposing the |
| 223 | tunnel and use `network: 'sui:testnet'` in wallet clients that need an explicit |
| 224 | network. For mainnet, omit `network` or pass `sui:mainnet`. |
| 225 | |
| 226 | Native clients should not load `/x402/client.js`. Call `POST /x402/prepare` with |
| 227 | `{ "sender": "...", "method": "GET", "path": "/paid/photo" }`, execute |
| 228 | `prepareTransaction.transaction` first when present, sign |
| 229 | `paymentTransaction.transaction`, and send the resulting x402 payload as the |
| 230 | `X-PAYMENT` header on the protected request: |
| 231 | |
| 232 | ```js |
| 233 | const payload = { |
| 234 | x402Version: prepared.x402Version, |
| 235 | payload: { |
| 236 | signature, |
| 237 | transaction: prepared.paymentTransaction.transaction, |
| 238 | }, |
| 239 | accepted: prepared.paymentRequirements, |
| 240 | resource: prepared.resource, |
| 241 | }; |
| 242 | const header = base64(JSON.stringify(payload)); |
| 243 | ``` |
| 244 | |
| 245 | The frontend integration is optional. Requests without a valid `X-PAYMENT` |
| 246 | header still receive x402 payment-required responses from the tunnel. |
| 247 | |
| 248 | ## `portal list` |
| 249 | |
| 250 | Print relay URLs resolved for the current invocation: |
| 251 | |
| 252 | ```bash |
| 253 | portal list [flags] |
| 254 | ``` |
| 255 | |
| 256 | | Flag | Type | Default | Description | |
| 257 | |------|------|---------|-------------| |
| 258 | | `--relays` | string | registry | Additional relay URLs | |
| 259 | | `--default-relays` | bool | `true` | Include public registry relays | |
| 260 | |
| 261 | `portal list` does not run the runtime relay discovery expansion loop. It only |
| 262 | resolves the registry seed list plus explicit relay URLs. |
| 263 | |
| 264 | ## `portal agent` |
| 265 | |
| 266 | Run a durable local agent that owns multiple tunnels from one config file: |
| 267 | |
| 268 | ```bash |
| 269 | portal agent run |
| 270 | portal agent dashboard |
| 271 | portal agent stop |
| 272 | portal agent restart |
| 273 | ``` |
| 274 | |
| 275 | | Command | Description | |
| 276 | |---------|-------------| |
| 277 | | `portal agent run` | Install or update and start the managed agent service | |
| 278 | | `portal agent run --config config.toml --foreground` | Run the agent in the current terminal | |
| 279 | | `portal agent dashboard` | Open the local TUI for tunnels, relays, multi-hop routes, and settings | |
| 280 | | `portal agent stop` | Gracefully stop the agent and disable or stop the OS service | |
| 281 | | `portal agent restart` | Stop the current agent if present, install or update the service, and start it again | |
| 282 | |
| 283 | The local control API binds only to loopback and uses a token in the agent state |
| 284 | directory. See [Portal Agent](/portal-agent) for the workflow and |
| 285 | [Configuration Reference](/configuration#configtoml) for the `config.toml` |
| 286 | format. |
| 287 | |
| 288 | Agent flags: |
| 289 | |
| 290 | | Command | Flag | Default | Description | |
| 291 | |---------|------|---------|-------------| |
| 292 | | `portal agent run` | `--config` | platform default | Agent TOML config path | |
| 293 | | `portal agent run` | `--foreground` | `false` | Run in the current process without installing the OS service | |
| 294 | | `portal agent run` | `--service` | `false` | Internal service entrypoint used by the installed OS service | |
| 295 | | `portal agent dashboard` | `--config` | platform default | Config path used for display and state-dir discovery | |
| 296 | | `portal agent dashboard` | `--state-dir` | config/default | Agent state directory to attach to | |
| 297 | | `portal agent stop` | `--config` | platform default | Config path used to resolve state dir and service name | |
| 298 | | `portal agent stop` | `--state-dir` | config/default | Agent state directory to stop | |
| 299 | | `portal agent restart` | `--config` | platform default | Config path used to reinstall and restart the service | |
| 300 | |
| 301 | ## `portal update` |
| 302 | |
| 303 | Update the CLI binary: |
| 304 | |
| 305 | ```bash |
| 306 | portal update |
| 307 | ``` |
| 308 | |
| 309 | The updater resolves the latest GitHub release, compares it with the installed |
| 310 | version, downloads the matching asset, verifies its SHA256 checksum, and |
| 311 | replaces the current executable. |
| 312 | |
| 313 | ## `portal version` |
| 314 | |
| 315 | ```bash |
| 316 | portal version |
| 317 | ``` |
| 318 | |
| 319 | Prints the installed version string and exits. |
| 320 | |
| 321 | ## Behavior Notes |
| 322 | |
| 323 | - `portal expose` and `portal list` check the latest published GitHub Release in |
| 324 | the background. A `main` merge or branch artifact is not offered to installed |
| 325 | clients until the release is created with matching binary and checksum assets. |
| 326 | - `portal expose` loads or creates a signing identity at `identity.json` or |
| 327 | `--identity-path`. |
| 328 | - Multiple relay URLs are registered independently. A failed relay does not stop |
| 329 | healthy relays from serving. |
| 330 | - With discovery enabled, the tunnel consumes relay `/discovery` results and |
| 331 | reconciles its relay pool. |
| 332 | - MITM enforcement is enabled by default for the default stream path. |
| 333 | - When the local stream target is unreachable, the tunnel returns an HTTP 503 |
| 334 | page to browser-style clients. |
| 335 | - Routed HTTP mode is HTTP-only and runs inside the tunnel process. |
| 336 | - `--tcp` requires relay TCP port transport, a valid `MIN_PORT`/`MAX_PORT` |
| 337 | range, and TCP port transport enabled in the admin panel. |
| 338 | - `--udp` requires relay UDP transport, a valid `MIN_PORT`/`MAX_PORT` range, UDP |
| 339 | enabled in the admin panel, and `SNI_PORT/udp` reachable for the QUIC backhaul. |
| 340 | - Bare `portal [flags]` is not accepted; use `portal expose` explicitly. |
| 341 | - Runtime `APP_*`, `RELAYS`, and `DEFAULT_RELAYS` environment variable fallbacks |
| 342 | are not used. |
| 343 | |
| 344 | ## Next Steps |
| 345 | |
| 346 | - [Getting Started](/getting-started): run your first tunnel |
| 347 | - [Portal Agent](/portal-agent): run durable multi-tunnel services |
| 348 | - [Wallet and ENS](/wallet-and-ens): understand admin tokens, wallet auth, and ENS gasless DNS |
| 349 | - [Concepts](/concepts): understand the relay and transport model |
| 350 | - [TCP and UDP Tunneling](/tcp-udp-tunneling): raw TCP and UDP setup |
| 351 | - [Deployment](/deployment): run your own relay server |