Summary
Today every obol sell offer is published under a shared path prefix on the single tunnel
hostname (/services/<name>/*). This proposes binding an offer to its own hostname so
multiple services can be routed by origin (Host header) — e.g. a.example.com → service A,
b.example.com → service B — through a single Cloudflare tunnel.
Background — the host-routing mechanism already exists
Host-based routing is already how obol-stack isolates internal services: Traefik's Gateway matches
HTTPRoute.spec.hostnames against the Host header.
- Frontend / eRPC pin
hostnames: ["obol.stack"] to keep them off the public tunnel —
internal/embed/infrastructure/base/templates/obol-frontend.yaml:41-42, erpc.yaml:41-42.
- The storefront pins the tunnel hostname —
CreateStorefront, internal/tunnel/tunnel.go:838 /
:934 ("hostnames": []string{hostname}).
A single cloudflared connector can serve many Public Hostnames (standard Cloudflare Zero Trust):
each *.example.com → http://traefik.traefik.svc.cluster.local:80 — the same origin all tunnel modes
already point at. cloudflared forwards the original Host header; Traefik routes by hostnames.
No second tunnel is required to route by origin.
The gap
ServiceOfferSpec has no hostname/domain field — internal/monetizeapi/types.go (only Path, ~:125).
buildHTTPRoute emits only a PathPrefix /services/<name> match, with no hostnames —
internal/serviceoffercontroller/render.go:579-620.
- No
--hostname flag on obol sell http (confirmed — the lone hostname hit in sell.go is an
unrelated comment).
Proposed change
- CRD field — add
Hostname string (spec.hostname, FQDN-validated) to ServiceOfferSpec
(internal/monetizeapi/types.go, alongside Path) and the embedded CRD
(internal/embed/infrastructure/base/templates/serviceoffer-crd.yaml; validated by
internal/embed/embed_crd_test.go).
- Renderer — in
buildHTTPRoute (internal/serviceoffercontroller/render.go:579), when
spec.hostname != "", set spec.hostnames: [hostname] and match PathPrefix / (the whole origin
maps to this backend) instead of /services/<name>. Mirror the storefront pattern at
tunnel.go:934. Backend stays x402-verifier, so payment gating is preserved. Add a regression
test in internal/serviceoffercontroller/render_test.go.
- CLI — add
--hostname to obol sell http (and likely sell agent/sell inference) in
cmd/obol/sell.go, threaded into the offer spec.
- Discovery host-awareness —
/skill.md, /api/services.json, and ERC-8004 advertise
https://<hostname>/ instead of <tunnelbase>/services/<name> when a hostname is set
(render.go:854, :977, :1138).
- Operator action (no code) — add each Public Hostname →
http://traefik.traefik.svc.cluster.local:80
in the Cloudflare dashboard. Optional polish: automate via cloudflareClient.UpdateTunnelConfiguration
(internal/tunnel/cloudflare.go, called from provision.go:101) to push multiple ingress rules.
Acceptance criteria
obol sell http <name> --hostname a.example.com … publishes an HTTPRoute with
spec.hostnames: [a.example.com] matching /, backed by x402-verifier (still 402-gated).
- Two offers on distinct hostnames route independently; behavior is unchanged (path-prefix) when
--hostname is omitted.
- Catalog /
/skill.md / ERC-8004 advertise the hostname-based endpoint when set.
render_test.go covers both modes; embed_crd_test.go accepts the new field.
- Host-bound
/-prefix routes do not shadow the internal obol.stack routes (different hostname) —
asserted by test.
Out of scope — Option B (several named tunnels)
Running multiple cloudflared connectors (distinct tokens/credentials per hostname) is a separate,
heavier change: the tunnel is hardcoded to one Helm release / Deployment / token Secret
(cloudflared, cloudflared-tunnel-token) plus a single Hostname in tunnel state (internal/tunnel/,
~10 references). It would require parameterizing the chart/release/secret and turning tunnel state into
a list. It is only needed for distinct connector trust/exposure boundaries per hostname (e.g. separate
Cloudflare accounts/zones) and still requires this issue's HTTPRoute change on top. Track separately
if a real need arises.
Security note
Host-bound offer routes remain gated — their backend is x402-verifier (render.go:608-611), so
payment enforcement is unchanged. The /-prefix only applies to the offer's own hostname, so it cannot
shadow obol.stack internal routes; a test should assert this.
Spec derived from a code walk of internal/tunnel/, internal/serviceoffercontroller/render.go,
internal/monetizeapi/types.go, and cmd/obol/sell.go. Filed via Claude Code:
https://claude.ai/code/session_01YUTW7NfxjUoVtyKgR6nPQC
Summary
Today every
obol selloffer is published under a shared path prefix on the single tunnelhostname (
/services/<name>/*). This proposes binding an offer to its own hostname somultiple services can be routed by origin (Host header) — e.g.
a.example.com → service A,b.example.com → service B— through a single Cloudflare tunnel.Background — the host-routing mechanism already exists
Host-based routing is already how obol-stack isolates internal services: Traefik's Gateway matches
HTTPRoute.spec.hostnamesagainst the Host header.hostnames: ["obol.stack"]to keep them off the public tunnel —internal/embed/infrastructure/base/templates/obol-frontend.yaml:41-42,erpc.yaml:41-42.CreateStorefront,internal/tunnel/tunnel.go:838/:934("hostnames": []string{hostname}).A single cloudflared connector can serve many Public Hostnames (standard Cloudflare Zero Trust):
each
*.example.com → http://traefik.traefik.svc.cluster.local:80— the same origin all tunnel modesalready point at. cloudflared forwards the original Host header; Traefik routes by
hostnames.No second tunnel is required to route by origin.
The gap
ServiceOfferSpechas no hostname/domain field —internal/monetizeapi/types.go(onlyPath, ~:125).buildHTTPRouteemits only aPathPrefix /services/<name>match, with nohostnames—internal/serviceoffercontroller/render.go:579-620.--hostnameflag onobol sell http(confirmed — the lonehostnamehit insell.gois anunrelated comment).
Proposed change
Hostname string(spec.hostname, FQDN-validated) toServiceOfferSpec(
internal/monetizeapi/types.go, alongsidePath) and the embedded CRD(
internal/embed/infrastructure/base/templates/serviceoffer-crd.yaml; validated byinternal/embed/embed_crd_test.go).buildHTTPRoute(internal/serviceoffercontroller/render.go:579), whenspec.hostname != "", setspec.hostnames: [hostname]and matchPathPrefix /(the whole originmaps to this backend) instead of
/services/<name>. Mirror the storefront pattern attunnel.go:934. Backend staysx402-verifier, so payment gating is preserved. Add a regressiontest in
internal/serviceoffercontroller/render_test.go.--hostnametoobol sell http(and likelysell agent/sell inference) incmd/obol/sell.go, threaded into the offer spec./skill.md,/api/services.json, and ERC-8004 advertisehttps://<hostname>/instead of<tunnelbase>/services/<name>when a hostname is set(
render.go:854,:977,:1138).http://traefik.traefik.svc.cluster.local:80in the Cloudflare dashboard. Optional polish: automate via
cloudflareClient.UpdateTunnelConfiguration(
internal/tunnel/cloudflare.go, called fromprovision.go:101) to push multiple ingress rules.Acceptance criteria
obol sell http <name> --hostname a.example.com …publishes an HTTPRoute withspec.hostnames: [a.example.com]matching/, backed byx402-verifier(still 402-gated).--hostnameis omitted./skill.md/ ERC-8004 advertise the hostname-based endpoint when set.render_test.gocovers both modes;embed_crd_test.goaccepts the new field./-prefix routes do not shadow the internalobol.stackroutes (different hostname) —asserted by test.
Out of scope — Option B (several named tunnels)
Running multiple cloudflared connectors (distinct tokens/credentials per hostname) is a separate,
heavier change: the tunnel is hardcoded to one Helm release / Deployment / token Secret
(
cloudflared,cloudflared-tunnel-token) plus a singleHostnamein tunnel state (internal/tunnel/,~10 references). It would require parameterizing the chart/release/secret and turning tunnel state into
a list. It is only needed for distinct connector trust/exposure boundaries per hostname (e.g. separate
Cloudflare accounts/zones) and still requires this issue's HTTPRoute change on top. Track separately
if a real need arises.
Security note
Host-bound offer routes remain gated — their backend is
x402-verifier(render.go:608-611), sopayment enforcement is unchanged. The
/-prefix only applies to the offer's own hostname, so it cannotshadow
obol.stackinternal routes; a test should assert this.Spec derived from a code walk of
internal/tunnel/,internal/serviceoffercontroller/render.go,internal/monetizeapi/types.go, andcmd/obol/sell.go. Filed via Claude Code:https://claude.ai/code/session_01YUTW7NfxjUoVtyKgR6nPQC