Skip to content

feat: ServiceOffer controller + PaymentRoute CRD (replaces monetize.py)#298

Open
bussyjd wants to merge 3 commits intomainfrom
worktree-serviceoffer-controller
Open

feat: ServiceOffer controller + PaymentRoute CRD (replaces monetize.py)#298
bussyjd wants to merge 3 commits intomainfrom
worktree-serviceoffer-controller

Conversation

@bussyjd
Copy link
Copy Markdown
Collaborator

@bussyjd bussyjd commented Mar 29, 2026

Summary

Production-ready Kubernetes controller for ServiceOffer CRDs using controller-runtime. Replaces the 1927-line monetize.py Python reconciliation loop with a generation-driven Go operator. Introduces PaymentRoute CRD to eliminate the ConfigMap mutation race. Refactors the x402-verifier to watch PaymentRoute CRs via dynamic informer.

Refs: #296

Architecture

Two Deployments, one binary family:

Component Runtime Scales on Failure impact
serviceoffer-controller (obol-system ns) controller-runtime, leader election CR count Stops convergence (not user-visible)
x402-verifier (x402 ns) HTTP server + PaymentRoute informer Request QPS Drops ForwardAuth (user-visible)
ServiceOffer CR → controller derives:
  ├── Middleware (traefik.io ForwardAuth)
  ├── PaymentRoute CR (obol.org) ──→ verifier watches, builds route table
  └── HTTPRoute (gateway API)

What changed

Before After
monetize.py polls every 10-60s Informer fires on CR events (sub-second)
Runs inside obol-agent pod Own Deployment, independent
ConfigMap string append/remove race PaymentRoute CR per route (no race)
No finalizer Finalizer ensures cleanup on any deletion
Single phase string Conditions array + observedGeneration
File watcher in verifier (60-120s) PaymentRoute informer (sub-second)

Files

Path Action Purpose
cmd/serviceoffer-controller/main.go New Controller binary
internal/controller/reconciler.go New Generation-driven reconcile loop
internal/controller/helpers.go New Spec parsing, conditions, GVK matcher
internal/controller/resources.go New Middleware, PaymentRoute, HTTPRoute builders
internal/controller/reconciler_test.go New 12 unit tests
internal/x402/source/paymentroute.go New PaymentRoute informer for verifier
internal/x402/source/paymentroute_test.go New 4 source tests
internal/x402/verifier.go Modified Added Config() accessor
cmd/x402-verifier/main.go Modified PaymentRoute informer replaces file watcher
internal/embed/.../paymentroute-crd.yaml New PaymentRoute CRD definition
internal/embed/.../serviceoffer-controller.yaml New SA + ClusterRole + Deployment
Dockerfile.serviceoffer-controller New Distroless container
internal/embed/skills/sell/scripts/monetize.py Deleted Replaced by controller
internal/x402/watcher.go Deleted Replaced by PaymentRoute informer
CLAUDE.md Modified Document new architecture

Net: +1,900 lines Go, -1,984 lines Python

Test plan

  • go build ./... passes
  • go test ./internal/controller/ — 12 tests pass (spec parsing, conditions, ownerRefs, phases)
  • go test ./internal/x402/source/ — 4 tests pass (PaymentRoute conversion, validation)
  • Deploy to cluster: obol sell http → verify PaymentRoute CR created → verify 402 response
  • Delete ServiceOffer → verify PaymentRoute cleaned up via ownerRef cascade
  • Kill controller pod → verify verifier continues serving existing routes

bussyjd added 3 commits March 29, 2026 04:42
Event-driven controller-runtime reconciler for ServiceOffer CRDs,
independent of the obol-agent runtime. Replaces the Python polling loop
with proper K8s informer watches, finalizers, and status conditions.

Reconciler creates/manages:
- Traefik Middleware (ForwardAuth → x402-verifier)
- x402-pricing ConfigMap route entries
- Gateway API HTTPRoute (/services/<name>/*)

On deletion: finalizer removes pricing route; ownerRefs cascade the rest.

Status conditions: UpstreamHealthy, PaymentGateReady, RoutePublished, Ready
with observedGeneration tracking.

Phase 1 (KISS): keeps ConfigMap for pricing (no PaymentRoute CRD yet),
skips ERC-8004 registration (stays in monetize.py for now).

Includes:
- cmd/serviceoffer-controller/ — binary entrypoint
- internal/controller/ — reconciler, helpers, resource builders, tests
- Dockerfile.serviceoffer-controller — distroless image
- K8s manifests (SA, ClusterRole, Deployment in obol-system ns)

Refs: #296
Replace ConfigMap string mutation with PaymentRoute CRD. Each
ServiceOffer now creates an owned PaymentRoute CR in the x402 namespace.
The verifier watches these via dynamic informer instead of polling a file.

Changes:
- PaymentRoute CRD (obol.org/v1alpha1) with spec and status.admitted
- Controller creates PaymentRoute CRs via createOrUpdate (not ConfigMap)
- Verifier: --route-source=paymentroute (default) watches CRs,
  --route-source=configmap falls back to legacy file watcher
- PaymentRouteSource (internal/x402/source/) builds route table from
  CRs and marks status.admitted when loaded
- Verifier.Config() accessor for PaymentRouteSource to read globals
- Removed .well-known handler from verifier (per issue #296)
- 16 unit tests across controller and source packages

No backward compat with ConfigMap pricing — this is the new path.
monetize.py deletion and CLAUDE.md updates in next commit.

Refs: #296
…cture

- Delete monetize.py (1927 lines) — replaced by serviceoffer-controller
- Delete watcher.go (57 lines) — replaced by PaymentRoute informer
- Update CLAUDE.md: document PaymentRoute CRD, serviceoffer-controller,
  verifier PaymentRoute watch. Remove references to monetize.py and
  ConfigMap-based pricing.

Refs: #296
@bussyjd bussyjd changed the title feat: ServiceOffer controller — Phase 1 (replaces monetize.py) feat: ServiceOffer controller + PaymentRoute CRD (replaces monetize.py) Mar 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant