Skip to content

feat(agent): sell agents as x402 services via CRD#453

Merged
bussyjd merged 9 commits into
integration/pr450-pr451-cloudflare-obolfrom
oisin/sell-agent
May 9, 2026
Merged

feat(agent): sell agents as x402 services via CRD#453
bussyjd merged 9 commits into
integration/pr450-pr451-cloudflare-obolfrom
oisin/sell-agent

Conversation

@OisinKyne
Copy link
Copy Markdown
Contributor

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

  • obol.org/v1alpha1 Agent (spec.runtime|model|skills|objective|wallet.create,
    status.phase|pinnedModel|walletAddress|endpoint|conditions)
  • ServiceOffer extended with spec.type=agent, spec.agent.ref,
    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

  • Validates spec, pins model (spec.model → status.pinnedModel)
  • Drops a finalizer (agents.obol.org/finalizer) for orderly teardown
  • Provisions Namespace, ServiceAccount, PVC (local-path-provisioner maps it under
    /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
  • When wallet.create=true: mints a fresh secp256k1 keypair, V3-encrypts in
    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
  • Resolves ServiceOffer.spec.agent.ref → reads Agent status → derives upstream,
    model, skills, endpoint into status.agentResolution
  • 402 verifier merges agentModel, agentSkills, agentRuntime into accepts[].extra
    for buyer discovery
  • Deletion path tears down Service → Deployment → ConfigMap → PVC → Secrets →
    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

  • 30 packages green; 0 failures across the suite (go test ./...)
  • go vet ./... clean (no new warnings; pre-existing enclave noise unchanged)
  • New unit tests cover: CRD parses + field shape + printer columns +
    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.
  • flows/flow-15-sell-agent.sh finalised: obol agent new → host seed assertion →
    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)

  • Master agent (hermes-obol-agent) migration onto the new CRD path — keeps the
    existing host-rendered helmfile flow for now
  • ConfigMap-driven in-cluster skill+soul.md seed for in-cluster
    manager-agents-spawning-sub-agents (host-side seed is the only path today)
  • AgentTask CRD for audit trail / cached-result-on-retry — x402 nonce semantics
    already give us at-most-once settlement
  • obol agent sweep --to 0xCold for revenue management
  • LiteLLM alias indirection so the underlying model can be swapped without
    changing offer contracts
  • Per-offer system-prompt injection for tier differentiation on a shared agent
  • Tools-within-a-skill allowlist (skills are the unit in v1)
  • Restricted Hermes container image (capability sandbox at the image layer)

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

  • ref + status)
    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

@OisinKyne OisinKyne requested a review from bussyjd May 8, 2026 18:57
@bussyjd bussyjd force-pushed the oisin/sell-agent branch from 3bb6455 to fade3d7 Compare May 9, 2026 14:39
@bussyjd bussyjd changed the base branch from main to integration/pr450-pr451-cloudflare-obol May 9, 2026 14:39
@bussyjd
Copy link
Copy Markdown
Collaborator

bussyjd commented May 9, 2026

Integration prep update

I ported this PR onto integration/pr450-pr451-cloudflare-obol and force-updated the PR branch at exact head fade3d70c2c265ac237175dd3b334ed5c314e624.

Integration-specific adaptations

  • retargeted the PR base from main to integration/pr450-pr451-cloudflare-obol
  • renamed the new flow from flow-15-sell-agent.sh to flow-16-sell-agent.sh to avoid colliding with the existing flow-15-live-obol-faucet-alice-bob.sh
  • removed the committed plans/obol-sell-agent.md planning doc to match repo policy
  • tightened CRD-agent update behavior while integrating:
    • reject legacy-only flags when obol agent new dispatches to the CRD path
    • keep obol agent list --runtime ... aggregate-only for CRD agents instead of mixing them into runtime-filtered output
    • make explicit --objective updates rewrite soul.md without unnecessarily rewriting skill files
    • harden exact skill sync with staging/rollback coverage plus symlink-safety tests
  • made the new flow comments honest about current scope: flow-16 is a metadata smoke, while paid-path/settlement coverage remains in flow-08

Validation at exact head

Passed on fade3d70c2c265ac237175dd3b334ed5c314e624:

  • bash -n flows/flow-16-sell-agent.sh
  • go test ./cmd/obol ./internal/agentcrd -count=1
  • go test ./...
  • go vet ./cmd/obol ./internal/agentcrd ./internal/agentruntime ./internal/defaults ./internal/demo ./internal/embed ./internal/monetizeapi ./internal/openclaw ./internal/serviceoffercontroller ./internal/x402
  • git diff --check

Notes

  • I did not run a live end-to-end paid sell-agent flow here; the renamed flow-16 is still intentionally scoped as the agent-offer metadata smoke and now says so explicitly.
  • The branch is now prepared for the integration review/merge path with the numbering conflict removed.

@bussyjd bussyjd merged commit a56348e into integration/pr450-pr451-cloudflare-obol May 9, 2026
6 checks passed
@bussyjd bussyjd deleted the oisin/sell-agent branch May 9, 2026 16:17
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>
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.
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.

2 participants