Skip to content

feat: PurchaseRequest CRD — controller-based buy-side (replace direct ConfigMap writes) #329

Description

@bussyjd

Motivation

buy.py writes x402-buyer-config and x402-buyer-auths ConfigMaps directly in the llm namespace. The agent's ServiceAccount has no cross-namespace ConfigMap write access (#328), and the direct-write pattern has correctness and security risks even if RBAC were granted:

  • No validation — malformed JSON crashes the sidecar
  • Race condition — concurrent buy.py runs corrupt ConfigMaps (read-modify-write without locking)
  • Prompt injection — agent LLM decides when/what to write
  • No audit trail — who bought what, when, at what price
  • Auth pool double-spend — with 2 LiteLLM replicas, both pods pop the same auth nonce

Proposal: PurchaseRequest CRD + controller

Mirror the sell-side pattern: ServiceOffer CR → serviceoffer-controller.

apiVersion: obol.org/v1alpha1kind: PurchaseRequestmetadata:
name: alice-inferencenamespace: openclaw-obol-agentspec:
endpoint: https://tunnel.example.com/services/alice/v1/chat/completionsmodel: qwen3.5:9bcount: 5signerNamespace: openclaw-obol-agentbuyerNamespace: llmautoRefill:
enabled: truethreshold: 2count: 5maxTotal: 100# cap total auths ever signedmaxSpendPerDay: "100000"# 0.1 USDC daily cappayment:
network: base-sepoliapayTo: "0xC0De..."price: "1000"asset: "0x036CbD..."status:
conditions:
- type: Probed
- type: AuthsSigned
- type: Configured
- type: ReadypublicModel: paid/qwen3.5:9bremaining: 4spent: 1totalSpent: "1000"probedAt: "2026-04-09T..."probedPrice: "1000"walletBalance: "50000"lastPaymentTime: "2026-04-09T..."

Reconciliation stages

  1. Probed — probe endpoint → 402, validate pricing matches spec. Store status.probedPrice and status.probedAt.
  2. AuthsSigned — re-probe to catch pricing drift. Call remote-signer via cluster DNS (http://remote-signer.<signerNamespace>.svc.cluster.local:9000). Pre-flight USDC balance check.
  3. Configured — write buyer ConfigMaps in spec.buyerNamespace with resourceVersion-based optimistic concurrency. Requeue on conflict.
  4. Ready — poll sidecar /status on all LiteLLM pods until auths are loaded. Requeue until confirmed.

Architectural decisions

DecisionRationale
Remote-signer via cluster DNS (not port-forward)Controllers are long-lived; kubectl port-forward is fragile and races with process lifecycle
ResourceVersion optimistic concurrency on ConfigMap writesPrevents merge race when multiple PurchaseRequests reconcile concurrently
Per-upstream data keys in ConfigMap (not single JSON blob)Enables Kubernetes SSA merge per-upstream without application-level conflict resolution
Auto-refill spending caps (maxTotal, maxSpendPerDay)Prevents infinite-money bug if sidecar rapidly consumes auths without successful payments
Re-probe before signingCatches pricing drift (TOCTOU) between probe and auth signing
LiteLLM replicas: 1 for nowAuth pool double-spend across replicas has no clean solution without a centralized dispenser. Document the constraint.
Separate SA for purchase controllerMinimize blast radius: ConfigMaps in llm only, PurchaseRequests read/write, pods/list in llm

Auth pool and replicas

With N LiteLLM replicas, all read the same ConfigMap auth pool. If both pods pop the same nonce simultaneously, one payment fails on-chain. Options:

  1. Single replica (recommended for now) — document constraint
  2. Partitioned pools (future) — controller assigns disjoint auth subsets per pod
  3. Centralized dispenser (future) — shared service atomically dispenses auths

CLI

obol buy <name> --endpoint <url> --model <id> --count 5
obol buy list
obol buy status <name>
obol buy refill <name> --count 5
obol buy delete <name>

Migration from buy.py

  1. buy.py currently can't write ConfigMaps cross-namespace (RBAC fix: buy.py needs ConfigMap write access to llm namespace for buyer sidecar #328) — so nothing to migrate from
  2. Modify buy.py to create PurchaseRequest CRs instead (same namespace, no RBAC issue)
  3. Controller takes over ConfigMap writes

Known constraints to document

  • LiteLLM replicas must be 1 when using the buyer sidecar (double-spend risk)
  • ConfigMap propagation latency: ~60-120s between controller write and sidecar reload
  • Auto-refill has spending caps to prevent runaway charges
  • Auth pool size limited by ConfigMap 1MB data limit (~2500 auths at 400 bytes each)

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions