| 1 | --- |
| 2 | title: Portal Agent |
| 3 | description: Run durable multi-tunnel Portal services from a local config file. |
| 4 | --- |
| 5 | |
| 6 | # Portal Agent |
| 7 | |
| 8 | `portal agent` is the long-lived version of `portal expose`. It runs one local |
| 9 | agent process, reads a TOML config file, and keeps every declared tunnel |
| 10 | registered with the selected relays. |
| 11 | |
| 12 | Use the agent when tunnels should survive terminal closes, login sessions, or |
| 13 | manual restarts. Use `portal expose` for one-off development sessions. |
| 14 | |
| 15 | ## What The Agent Owns |
| 16 | |
| 17 | The agent owns: |
| 18 | |
| 19 | - one `config.toml` |
| 20 | - one local loopback control API |
| 21 | - one OS service when run in managed mode |
| 22 | - one or more tunnel runtimes declared under `[[tunnels]]` |
| 23 | - tunnel identities stored under the agent state directory unless overridden |
| 24 | |
| 25 | Each tunnel still uses the normal Portal SDK path internally: it registers a |
| 26 | lease, opens reverse sessions, renews the lease, and proxies traffic to the |
| 27 | configured local target. |
| 28 | |
| 29 | ## Create A Config |
| 30 | |
| 31 | `portal agent run` requires an existing config file. The installer does not |
| 32 | create one. |
| 33 | |
| 34 | Default config paths: |
| 35 | |
| 36 | | OS | Config path | |
| 37 | |----|-------------| |
| 38 | | Linux user | `$XDG_CONFIG_HOME/portal-tunnel/agent/config.toml` or `~/.config/portal-tunnel/agent/config.toml` | |
| 39 | | Linux root | `/etc/portal-tunnel/agent/config.toml` | |
| 40 | | macOS user | `~/Library/Application Support/Portal Tunnel/Agent/config.toml` | |
| 41 | | macOS root | `/Library/Application Support/Portal Tunnel/Agent/config.toml` | |
| 42 | | Windows | `%ProgramData%\Portal Tunnel\Agent\config.toml` | |
| 43 | |
| 44 | Minimal config: |
| 45 | |
| 46 | ```toml |
| 47 | [agent] |
| 48 | control_addr = "127.0.0.1:4018" |
| 49 | service_name = "portal-agent" |
| 50 | |
| 51 | [[tunnels]] |
| 52 | id = "web" |
| 53 | name = "myapp" |
| 54 | target = "127.0.0.1:3000" |
| 55 | relays = ["https://portal.example.com"] |
| 56 | discovery = false |
| 57 | description = "Managed web tunnel" |
| 58 | tags = ["web"] |
| 59 | ``` |
| 60 | |
| 61 | Routed HTTP config: |
| 62 | |
| 63 | ```toml |
| 64 | [agent] |
| 65 | control_addr = "127.0.0.1:4018" |
| 66 | service_name = "portal-agent" |
| 67 | |
| 68 | [[tunnels]] |
| 69 | id = "frontend" |
| 70 | name = "myapp" |
| 71 | relays = ["https://portal.example.com"] |
| 72 | discovery = false |
| 73 | x402_pay_to = "0x..." |
| 74 | x402_testnet = true |
| 75 | |
| 76 | [[tunnels.http_routes]] |
| 77 | prefix = "/api" |
| 78 | upstream = "http://127.0.0.1:3001" |
| 79 | methods = ["GET"] |
| 80 | amount = "0.01" |
| 81 | |
| 82 | [[tunnels.http_routes]] |
| 83 | prefix = "/" |
| 84 | upstream = "http://127.0.0.1:5173" |
| 85 | ``` |
| 86 | |
| 87 | If a route has `amount`, the tunnel serves `/x402/client.js` and |
| 88 | `/x402/prepare` on the public tunnel origin. A browser frontend served by the |
| 89 | `/` route can import the helper and call `x402Fetch()` from its own UI. Native |
| 90 | clients use `/x402/prepare` directly and send the signed payload as |
| 91 | `X-PAYMENT`. The tunnel still verifies and settles payment before proxying the |
| 92 | paid route. Paid routes use Sui mainnet by default; set `x402_testnet = true` |
| 93 | to use Sui testnet. |
| 94 | |
| 95 | Relative paths in the config are resolved from the config file directory. |
| 96 | |
| 97 | ## Run The Agent |
| 98 | |
| 99 | Run as a managed OS service: |
| 100 | |
| 101 | ```bash |
| 102 | portal agent run |
| 103 | ``` |
| 104 | |
| 105 | Run in the current terminal: |
| 106 | |
| 107 | ```bash |
| 108 | portal agent run --config config.toml --foreground |
| 109 | ``` |
| 110 | |
| 111 | Open the local dashboard: |
| 112 | |
| 113 | ```bash |
| 114 | portal agent dashboard |
| 115 | ``` |
| 116 | |
| 117 | Restart or stop: |
| 118 | |
| 119 | ```bash |
| 120 | portal agent restart |
| 121 | portal agent stop |
| 122 | ``` |
| 123 | |
| 124 | `portal agent run`, `stop`, and `restart` load the config so they can find the |
| 125 | state directory and service name. `portal agent dashboard` can attach with only |
| 126 | the default state directory or an explicit `--state-dir`. |
| 127 | |
| 128 | `portal agent run --service` is the internal service entrypoint installed by |
| 129 | `portal agent run`. Operators normally do not run it directly. |
| 130 | |
| 131 | ## Dashboard |
| 132 | |
| 133 | The dashboard is a local terminal UI. It polls agent status every two seconds |
| 134 | and edits the same TOML config file that the service uses. |
| 135 | |
| 136 | Dashboard panes: |
| 137 | |
| 138 | | Pane | Purpose | |
| 139 | |------|---------| |
| 140 | | Tunnels | Add, select, and delete tunnels | |
| 141 | | Settings | Edit max active relays and public metadata | |
| 142 | | Relays | Connect or disconnect relays for the selected tunnel | |
| 143 | | Multi-hop | Build and apply an ordered multi-hop route | |
| 144 | |
| 145 | Keyboard controls: |
| 146 | |
| 147 | | Key | Action | |
| 148 | |-----|--------| |
| 149 | | `left` / `right` | Switch panes | |
| 150 | | `up` / `down` | Move within the active pane | |
| 151 | | `enter` | Apply the active action | |
| 152 | | `delete` | Delete the selected tunnel or disconnect the selected relay | |
| 153 | | `c` | Connect the selected relay in the Relays pane | |
| 154 | | `d` | Disconnect the selected relay in the Relays pane | |
| 155 | | `o` | Open the selected public tunnel URL | |
| 156 | | `a` | Add the selected relay as a multi-hop hop | |
| 157 | | `p` | Apply a drafted multi-hop route | |
| 158 | | `esc` | Cancel input or return to the Tunnels pane | |
| 159 | | `ctrl+c` | Exit the dashboard | |
| 160 | |
| 161 | The Add Tunnel action opens a form. Fill either `Target` for a simple loopback |
| 162 | tunnel or `Routes` for routed HTTP. Routes use this syntax: |
| 163 | |
| 164 | ```text |
| 165 | /paid=3001 GET:0.01; /=5173 |
| 166 | ``` |
| 167 | |
| 168 | Each entry is `PATH=UPSTREAM [METHOD[,METHOD...]:USDC_AMOUNT]`. Fill `X402 Pay |
| 169 | To` when any route has an amount, and set `X402 Testnet` to `true` for Sui |
| 170 | testnet. The form also accepts explicit `Relays`, |
| 171 | `Discovery`, and `Max Relays`; max relays caps auto-selected discovery relays |
| 172 | while explicit relays are still included. |
| 173 | |
| 174 | After creation, routed HTTP paths, x402 payment amounts, payment network, and |
| 175 | discovery mode are read-only in the Settings pane. To change routes, payment |
| 176 | amounts, payment network, or discovery mode, edit `http_routes`, |
| 177 | `x402_pay_to`, `x402_testnet`, and `discovery` in `config.toml`, then restart |
| 178 | the agent or tunnel. Other advanced options such as UDP, TCP, custom |
| 179 | identity JSON, or explicit multi-hop defaults are also configured in |
| 180 | `config.toml`. |
| 181 | |
| 182 | ## Tunnel Config Fields |
| 183 | |
| 184 | Common fields: |
| 185 | |
| 186 | | Field | Description | |
| 187 | |-------|-------------| |
| 188 | | `id` | Stable local tunnel ID used by the dashboard and control API | |
| 189 | | `name` | Public lease name, used as the subdomain label | |
| 190 | | `target` | Local TCP target, equivalent to `portal expose <target>` | |
| 191 | | `http_routes` | Routed HTTP mappings; cannot be combined with `target` or `udp` | |
| 192 | | `relays` | Explicit relay API URLs | |
| 193 | | `discovery` | Include registry and relay discovery expansion | |
| 194 | | `max_active_relays` | Maximum auto-selected relays kept connected | |
| 195 | | `identity_path` | Tunnel identity JSON path | |
| 196 | | `identity_json` | Identity JSON payload; persisted to `identity_path` when both are set | |
| 197 | | `udp`, `udp_addr` | UDP transport settings | |
| 198 | | `tcp` | Dedicated raw TCP port setting | |
| 199 | | `multi_hop` | Explicit ordered multi-hop relay URLs | |
| 200 | | `multi_hop_depth` | Automatically choose one multi-hop route with this depth | |
| 201 | | `ban_mitm` | Ban relays when the TLS self-probe detects termination; defaults to warning-only | |
| 202 | | `description`, `tags`, `owner`, `thumbnail`, `hide` | Public relay metadata | |
| 203 | | `x402_pay_to` | Tunnel-owned Sui USDC x402 recipient for paid HTTP routes | |
| 204 | | `x402_testnet` | Use Sui testnet for tunnel-owned x402 paid routes; omitted or `false` uses Sui mainnet | |
| 205 | | `http_routes[].amount` | Optional Sui USDC x402 amount, such as `0.01`, for one HTTP route prefix | |
| 206 | | `http_routes[].methods` | Optional HTTP methods that require payment on that route; empty means every method | |
| 207 | |
| 208 | Constraints match `portal expose`: |
| 209 | |
| 210 | - `target` cannot be combined with `http_routes`. |
| 211 | - `http_routes` cannot be combined with `udp`. |
| 212 | - `multi_hop` requires at least two relay URLs. |
| 213 | - `multi_hop` cannot be combined with `multi_hop_depth`. |
| 214 | - Multi-hop currently supports only the default stream transport, not UDP or raw |
| 215 | TCP port mode. |
| 216 | - `http_routes[].amount` requires `x402_pay_to`. |
| 217 | - `http_routes[].methods` requires `http_routes[].amount`. |
| 218 | |
| 219 | ## Identity Layout |
| 220 | |
| 221 | If `identity_path` is omitted: |
| 222 | |
| 223 | - a single tunnel uses `<state_dir>/identity.json` |
| 224 | - multiple tunnels use `<state_dir>/<tunnel-id>/identity.json` |
| 225 | |
| 226 | Reusing an identity keeps the same tunnel address and lease identity across |
| 227 | restarts. Use separate identity paths when two tunnels should have separate |
| 228 | lease identities. |
| 229 | |
| 230 | ## Local Control API |
| 231 | |
| 232 | The agent writes this file while running: |
| 233 | |
| 234 | ```text |
| 235 | <state_dir>/agent-endpoint.json |
| 236 | ``` |
| 237 | |
| 238 | It contains the loopback control address and a random bearer token. CLI commands |
| 239 | read this file and send `Authorization: Bearer <token>` to the local control |
| 240 | API. |
| 241 | |
| 242 | The agent refuses non-loopback `control_addr` values. Use `127.0.0.1`, |
| 243 | `localhost`, or another loopback address. |
| 244 | |
| 245 | Control endpoints: |
| 246 | |
| 247 | | Method | Path | Auth | Purpose | |
| 248 | |--------|------|------|---------| |
| 249 | | `GET` | `/agent/status` | Bearer token or wallet session | Read agent and tunnel status | |
| 250 | | `POST` | `/agent/shutdown` | Bearer token | Ask the agent to stop | |
| 251 | | `POST` | `/agent/tunnels` | Bearer token | Add a tunnel | |
| 252 | | `PATCH` | `/agent/tunnels/{id}` | Bearer token | Update metadata or max active relays | |
| 253 | | `DELETE` | `/agent/tunnels/{id}` | Bearer token | Delete a tunnel | |
| 254 | | `POST` | `/agent/tunnels/{id}/relays` | Bearer token | Connect a relay | |
| 255 | | `DELETE` | `/agent/tunnels/{id}/relays` | Bearer token | Disconnect a relay | |
| 256 | | `POST` | `/agent/tunnels/{id}/multi-hop` | Bearer token | Apply a multi-hop route | |
| 257 | | `DELETE` | `/agent/tunnels/{id}/multi-hop` | Bearer token | Clear multi-hop routing | |
| 258 | |
| 259 | Wallet auth endpoints also exist under `/agent/auth/*`. Wallet-authenticated |
| 260 | requests are read-only and can only call `/agent/status`; mutating operations |
| 261 | use the local bearer token from the state directory. |
| 262 | |
| 263 | ## Agent Wallet Access |
| 264 | |
| 265 | Set `agent.allowed_wallets` to restrict wallet-authenticated status access: |
| 266 | |
| 267 | ```toml |
| 268 | [agent] |
| 269 | allowed_wallets = ["0x1234567890abcdef1234567890abcdef12345678"] |
| 270 | ``` |
| 271 | |
| 272 | When `allowed_wallets` is empty, any wallet can sign in to the loopback agent |
| 273 | auth endpoint. This does not grant mutation rights; the bearer token still owns |
| 274 | config and tunnel changes. |
| 275 | |
| 276 | ## Troubleshooting |
| 277 | |
| 278 | If the dashboard says the agent is unavailable, start it explicitly: |
| 279 | |
| 280 | ```bash |
| 281 | portal agent run --config config.toml |
| 282 | ``` |
| 283 | |
| 284 | If the OS service manager is unavailable: |
| 285 | |
| 286 | ```bash |
| 287 | portal agent run --config config.toml --foreground |
| 288 | ``` |
| 289 | |
| 290 | If a tunnel is stuck in `error`, check the selected tunnel row in the dashboard. |
| 291 | Common causes are an invalid local target, a relay URL that cannot be reached, a |
| 292 | transport disabled on the relay, or an invalid multi-hop route. |
| 293 | |
| 294 | ## Next Steps |
| 295 | |
| 296 | - [Configuration Reference](/configuration#configtoml): every agent config field |
| 297 | - [Wallet and ENS](/wallet-and-ens): admin tokens, wallet auth, and ENS gasless behavior |
| 298 | - [CLI Reference](/cli-reference): command flags and examples |