feat(agent): sell agents as x402 services via CRD#453
Merged
bussyjd merged 9 commits intoMay 9, 2026
Conversation
Collaborator
Integration prep updateI ported this PR onto Integration-specific adaptations
Validation at exact headPassed on
Notes
|
a56348e
into
integration/pr450-pr451-cloudflare-obol
6 checks passed
bussyjd
added a commit
that referenced
this pull request
May 9, 2026
) * [verified] feat(tunnel): rebuild persistent tunnel/domain flow on current main * feat: add live OBOL faucet buyer-seller smoke flow * test(flows): tolerate summarized sell status output and dynamic ingress * [verified] fix(tunnel): harden persistent handoff and setup UX * test(flows): assert tunnel does not expose erpc * docs: align live OBOL source of truth * feat(tunnel): add configurable cloudflared transport * fix: harden live OBOL faucet smoke reruns - prefer current Go path for nohup/cron flow execution - poll for remote-signer pod creation before age checks - allow cooldown-safe flow-15 reruns when Bob already has faucet OBOL - poll post-claim balances to tolerate public RPC state lag * test(flows): add portable timeout fallback for live obol registration * fix(flows): harden smoke tunnel and OBOL buyer paths * fix(flows): fail closed on tunnel smoke checks * refactor dual-stack flow helpers (#454) Co-authored-by: bussyjd <bussyjd@users.noreply.github.com> * feat(buy): add `obol buy inference` host CLI (#434) * feat(buy): add `obol buy inference` host CLI Mirrors `obol sell inference` on the buyer side. The host CLI handles default-seller resolution, ERC-8004 identity pre-flight, and USDC->micro-units conversion, then dispatches to the existing `buy.py buy` skill in the obol-agent pod. Single canonical wallet, no host-side keystore. - internal/x402/setup.go: DefaultBuySellerURL, DefaultBuySellerAgentID, DefaultBuySellerChain placeholders (TODO: wire live values once the default seller is provisioned). - internal/agentruntime/exec.go: ExecInPod + BuildExecArgs generalize the kubectl-exec helper that was hardcoded to the hermes binary. - internal/hermes/hermes.go: cliViaKubectlExec + hermesExecArgs delegate to the new agentruntime helpers; existing test stays valid. - internal/buy/discover.go: .well-known/agent-registration.json fetcher and ERC-8004 agentId verification (hard-fail on mismatch). - cmd/obol/buy.go: `obol buy inference [<name>] --seller --model --budget --expected-agent-id --no-verify-identity --auto-refill ...`. * test(flow-11): validate host buy inference on integration * feat(agent): sell agents as x402 services via CRD (#453) * Agent crd * Next phase * 1, 2a, 2b, 2c, 4a, 4b, 5, 6, 7, 8, 9 * 2d * Update with almost all complete, time for testing * Bug fixing * chore: remove stray runtime log * chore(flows): renumber sell-agent smoke flow for integration * fix(agent): harden CRD update sync semantics --------- Co-authored-by: bussyjd <bussyjd@users.noreply.github.com> Co-authored-by: bussyjd <jd@obol.tech> --------- Co-authored-by: bussyjd <bussyjd@users.noreply.github.com> Co-authored-by: Oisín Kyne <4981644+OisinKyne@users.noreply.github.com>
5 tasks
OisinKyne
pushed a commit
that referenced
this pull request
May 11, 2026
Pulls forward five small correctness fixes that were carried on the integration branch behind #452 but did not survive the squash merges. - Re-queue offers when their referenced Agent changes. Without this an Agent status edit (e.g. status.pinnedModel after the user edits spec.model) never propagates into the offer's status.agentResolution because the offer reconciler only runs when the offer itself changes. - Refuse to Update Namespace and PersistentVolumeClaim during applyAgentObject. PVCs reject wholesale Update with "spec is immutable after creation", and the controller's RBAC only grants `create` on Namespaces. Treat existence as success for these kinds and move on; mutable kinds (ConfigMap, Secret, Deployment, Service, ServiceAccount) keep going through the normal Update path. - Fall back to status.agentResolution.Model in the storefront catalog when an offer's spec.model is empty (the canonical state for type=agent offers, where the model lives on the linked Agent). - Bump the serviceoffer-controller Deployment memory request from 64Mi to 128Mi and the limit from 256Mi to 512Mi. The Agent informer + agent reconciler + in-controller keystore generation pushed steady-state past 256Mi after #453 and triggered OOMKilled restart loops. - Set GATEWAY_ALLOW_ALL_USERS=true on CRD-rendered agent pods. CRD agents only expose the API (gated by API_SERVER_KEY + ForwardAuth); no Telegram/Discord/dashboard platforms are wired. The flag silences Hermes' user-gateway startup warning without opening any real surface.
OisinKyne
pushed a commit
that referenced
this pull request
May 11, 2026
Pulls forward five small correctness fixes that were carried on the integration branch behind #452 but did not survive the squash merges. - Re-queue offers when their referenced Agent changes. Without this an Agent status edit (e.g. status.pinnedModel after the user edits spec.model) never propagates into the offer's status.agentResolution because the offer reconciler only runs when the offer itself changes. - Refuse to Update Namespace and PersistentVolumeClaim during applyAgentObject. PVCs reject wholesale Update with "spec is immutable after creation", and the controller's RBAC only grants `create` on Namespaces. Treat existence as success for these kinds and move on; mutable kinds (ConfigMap, Secret, Deployment, Service, ServiceAccount) keep going through the normal Update path. - Fall back to status.agentResolution.Model in the storefront catalog when an offer's spec.model is empty (the canonical state for type=agent offers, where the model lives on the linked Agent). - Bump the serviceoffer-controller Deployment memory request from 64Mi to 128Mi and the limit from 256Mi to 512Mi. The Agent informer + agent reconciler + in-controller keystore generation pushed steady-state past 256Mi after #453 and triggered OOMKilled restart loops. - Set GATEWAY_ALLOW_ALL_USERS=true on CRD-rendered agent pods. CRD agents only expose the API (gated by API_SERVER_KEY + ForwardAuth); no Telegram/Discord/dashboard platforms are wired. The flag silences Hermes' user-gateway startup warning without opening any real surface.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new agent ServiceOffer type that wraps an Obol Stack agent (Hermes) with
x402 payments. Buyers POST a chat-completions prompt, the seller's agent runs the
task with a constrained skill set, returns the result, and only then does x402
settle. Pairs with a new Agent CRD so agents are first-class cluster resources
separate from the act of selling them.
Architecture
obol agent new (host: seed soul.md + skills, apply Agent CR)
↓
serviceoffer-controller (cluster: provision NS + SA + PVC + ConfigMap
+ Secret + Hermes Deployment + Service;
optional remote-signer when wallet.create)
↓
Agent.status.{Ready, walletAddress, endpoint}
obol sell agent (offers it: ServiceOffer type=agent, agent.ref)
↓
controller resolves ref (status.AgentResolution → upstream + model +
skills)
↓
402 response (extra.agentModel, agentSkills, agentRuntime)
↓
Buyer probes → pays → traffic proxies to Hermes
What's in scope
New CRD
status.phase|pinnedModel|walletAddress|endpoint|conditions)
status.agentResolution. spec.upstream no longer required at spec level
(controller synthesises it for agent-typed offers)
New CLI
obol agent new [--model X] [--skills a,b,c] [--objective "..."]
[--create-wallet]
obol agent update [--model X] [--skills +foo,-bar]
obol agent list / delete
obol sell agent [--pay-to 0x...] --price ... --token ... --chain ...
obol sell demo quant # 10 OBOL / ethereum mainnet preset over the above
obol agent new and obol sell agent both prompt interactively when a TTY is
present and flags are missing. obol sell agent offers to inline-create the
agent if it's not found.
--pay-to standardisation
Primary flag across obol sell {inference,http,demo,agent,update,pricing}.
--wallet/--recipient/-w kept as deprecated aliases for one minor release.
Controller responsibilities
/agent-/hermes-data, where the host-side seed already wrote
skills + soul.md), ConfigMap, API-server Secret, Hermes Deployment, Service via
get-or-create-or-update
memory, persists keystore + password in a per-namespace Secret (address pinned as
an annotation so re-reconciles don't re-mint), and provisions a remote-signer
Deployment + Service alongside Hermes
model, skills, endpoint into status.agentResolution
for buyer discovery
ServiceAccount in dependency order, idempotent on re-run
soul.md template
Embedded as a Go string constant in internal/agentruntime (not on the CRD).
Operator-supplied spec.objective is the only piece that rides on the CR; the
controller substitutes it into the template and writes once at first reconcile.
Agent owns soul.md after that. Includes guardrails about untrusted users, scope
discipline, and refusing redirection attempts.
Quant demo rewrite
obol sell demo quant now provisions an agent-backed offer (10 OBOL / ethereum
mainnet, skills: [ethereum-networks, ethereum-local-wallet, addresses, gas],
wallet provisioned, sensible analyst objective). Pure-Go internal/demo/quant.go
deleted.
Tests
walletAddress pattern + runtime enum; Go-type helpers (EffectiveRuntime,
EffectiveModel pinning sequence, IsReady); WriteSkillSubset (writes only
requested, leaves agent-installed alone, rejects unknown, no partial write);
RenderSoul (substitutes / trims / handles empty); agentcrd.SeedHostFiles (fresh,
preserves existing soul.md, force-overwrite); skill-diff syntax +foo,-bar; agent
reconciler (validation, model-unpinned parking, happy-path provisioning,
with-wallet path, finalizer-driven teardown idempotency); ServiceOffer
agent-resolution; 402 extras merging; --pay-to alias presence on every sell
command.
Hermes + remote-signer pods Running → walletAddress populated → obol sell agent →
ServiceOffer carries spec.agent.ref → reaches Ready → 402 surfaces agent
metadata.
Deliberate non-goals (deferred follow-ups)
existing host-rendered helmfile flow for now
manager-agents-spawning-sub-agents (host-side seed is the only path today)
already give us at-most-once settlement
changing offer contracts
Files
plans/obol-sell-agent.md (full design + decisions/tradeoffs + status)
CLAUDE.md (plans-vs-docs split: plans/ allowed for
revisits)
internal/monetizeapi/types.go (+Agent, +AgentSpec, +AgentStatus,
+ServiceOfferAgent
+ServiceOfferAgentResolution,
+Namespace/SA/PVC GVRs)
internal/embed/infrastructure/base/templates/agent-crd.yaml (new)
internal/embed/infrastructure/base/templates/serviceoffer-crd.yaml (+agent type
internal/embed/infrastructure/base/templates/x402.yaml (+RBAC for
agents/SA/PVC/Secret/NS)
internal/embed/embed.go (+WriteSkillSubset)
internal/agentruntime/soul.go (+SoulTemplate, RenderSoul)
internal/agentcrd/ (new pkg: namespace conventions, host-side
seeding,
ParseSkills, BuildAgent manifest helper,
ValidateName)
internal/serviceoffercontroller/agent.go (CRD informer + reconciler
+
finalizer + teardown)
internal/serviceoffercontroller/agent_render.go (Hermes K8s primitives)
internal/serviceoffercontroller/agent_wallet.go (remote-signer
provisioning)
internal/serviceoffercontroller/agent_resolver.go (offer.spec.agent.ref →
status)
internal/openclaw/wallet.go (+GenerateKeystoreInMemory: in-memory V3
keystore)
internal/x402/config.go
(+RouteRule.AgentModel/AgentSkills/AgentRuntime)
internal/x402/serviceoffer_source.go (+derive agent fields from offer status)
internal/x402/verifier.go (+mergeAgentExtras into 402 accepts[].extra)
cmd/obol/sell.go (+payToFlag(), +sellAgentCommand wiring,
quant demo rewired to agent-backed)
cmd/obol/sell_agent.go (new: sell agent CLI + inline
create-and-sell)
cmd/obol/agent.go (+update subcommand, list/delete merge CRD
agents)
cmd/obol/agent_crd.go (new: shared createCRDAgent + agent new
dispatch)
cmd/demo-server/main.go (quant case removed)
internal/demo/quant.go (deleted; quant is agent-backed now)
flows/flow-15-sell-agent.sh (new, end-to-end)
🤖 Generated with Claude Code