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 sidecarRace condition — concurrent buy.py runs corrupt ConfigMaps (read-modify-write without locking)Prompt injection — agent LLM decides when/what to writeNo audit trail — who bought what, when, at what priceAuth pool double-spend — with 2 LiteLLM replicas, both pods pop the same auth nonceProposal: PurchaseRequest CRD + controller Mirror the sell-side pattern: ServiceOffer CR → serviceoffer-controller.
apiVersion : obol.org/v1alpha1 kind : PurchaseRequest metadata :
name : alice-inference namespace : openclaw-obol-agent spec :
endpoint : https://tunnel.example.com/services/alice/v1/chat/completions model : qwen3.5:9b count : 5 signerNamespace : openclaw-obol-agent buyerNamespace : llm autoRefill :
enabled : true threshold : 2 count : 5 maxTotal : 100 # cap total auths ever signedmaxSpendPerDay : " 100000" # 0.1 USDC daily cappayment :
network : base-sepolia payTo : " 0xC0De..." price : " 1000" asset : " 0x036CbD..." status :
conditions :
- type : Probed
- type : AuthsSigned
- type : Configured
- type : Ready publicModel : paid/qwen3.5:9b remaining : 4 spent : 1 totalSpent : " 1000" probedAt : " 2026-04-09T..." probedPrice : " 1000" walletBalance : " 50000" lastPaymentTime : " 2026-04-09T..." Reconciliation stages Probed — probe endpoint → 402, validate pricing matches spec. Store status.probedPrice and status.probedAt.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.Configured — write buyer ConfigMaps in spec.buyerNamespace with resourceVersion-based optimistic concurrency. Requeue on conflict.Ready — poll sidecar /status on all LiteLLM pods until auths are loaded. Requeue until confirmed.Architectural decisions Decision Rationale 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 signing Catches pricing drift (TOCTOU) between probe and auth signing LiteLLM replicas: 1 for now Auth pool double-spend across replicas has no clean solution without a centralized dispenser. Document the constraint. Separate SA for purchase controller Minimize 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:
Single replica (recommended for now) — document constraintPartitioned pools (future) — controller assigns disjoint auth subsets per podCentralized dispenser (future) — shared service atomically dispenses authsCLI 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 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 Modify buy.py to create PurchaseRequest CRs instead (same namespace, no RBAC issue) 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
Motivation
buy.pywritesx402-buyer-configandx402-buyer-authsConfigMaps directly in thellmnamespace. 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:Proposal: PurchaseRequest CRD + controller
Mirror the sell-side pattern: ServiceOffer CR → serviceoffer-controller.
Reconciliation stages
status.probedPriceandstatus.probedAt.http://remote-signer.<signerNamespace>.svc.cluster.local:9000). Pre-flight USDC balance check.spec.buyerNamespacewith resourceVersion-based optimistic concurrency. Requeue on conflict./statuson all LiteLLM pods until auths are loaded. Requeue until confirmed.Architectural decisions
kubectl port-forwardis fragile and races with process lifecyclemaxTotal,maxSpendPerDay)llmonly, PurchaseRequests read/write, pods/list inllmAuth 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:
CLI
Migration from buy.py
Known constraints to document
References
internal/serviceoffercontroller/controller.gointernal/x402/buyer/proxy.gointernal/embed/skills/buy-inference/scripts/buy.pyflows/flow-11-dual-stack.sh