Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
f77eb81
fix(network): stop reth liveness probe from killing consistency recovery
bussyjd Jul 14, 2026
d419620
fix(x402): default 402 challenge resource.url to https on public hosts
bussyjd Jul 14, 2026
440c59f
chore(deps): update dependency kubernetes-sigs/gateway-api to v1.6.1
github-actions[bot] Jul 16, 2026
a04c05d
fix(ui): honor OBOL_NONINTERACTIVE on a real TTY
bussyjd Jul 18, 2026
f5c880b
fix(agentcrd): strip server-managed metadata before ResumeAll re-apply
bussyjd Jul 18, 2026
c70634a
fix(serviceoffercontroller): align well-known/x402 resource path with…
bussyjd Jul 18, 2026
58c7db2
fix(stack): don't treat own cluster's ports as conflicts under --force
bussyjd Jul 18, 2026
8427f10
fix(tunnel): make hostname add idempotent for already-bound hosts
bussyjd Jul 18, 2026
8908241
chore(deps): update cloudflare/cloudflared docker tag to v2026.7.2
github-actions[bot] Jul 20, 2026
c3afc6e
chore(deps): update dependency @scalar/api-reference to v1.62.9
github-actions[bot] Jul 20, 2026
c632485
chore(deps): update ethereum el/cl client updates
github-actions[bot] Jul 20, 2026
947c843
chore(deps): update obolup.sh dependency updates
github-actions[bot] Jul 20, 2026
aa74352
feat(x402): auth-capture unlock gate + fee revenue metrics
bussyjd Jul 20, 2026
5d9a849
Merge remote-tracking branch 'origin/fix/reth-liveness-recovery' into…
bussyjd Jul 20, 2026
7a3964e
Merge remote-tracking branch 'origin/renovate/kubernetes-sigs-gateway…
bussyjd Jul 20, 2026
bb1f94f
Merge remote-tracking branch 'origin/fix/noninteractive-prompt-guard'…
bussyjd Jul 20, 2026
5115f9f
Merge remote-tracking branch 'origin/fix/agent-resume-strip-metadata'…
bussyjd Jul 20, 2026
bb0d1e3
Merge remote-tracking branch 'origin/fix/discovery-x402-resource-path…
bussyjd Jul 20, 2026
36d918b
Merge remote-tracking branch 'origin/fix/init-force-own-cluster-ports…
bussyjd Jul 20, 2026
252f9b9
Merge remote-tracking branch 'origin/fix/tunnel-hostname-idempotent' …
bussyjd Jul 20, 2026
53b45f1
Merge remote-tracking branch 'origin/renovate/cloudflared-updates' in…
bussyjd Jul 20, 2026
2b34310
Merge remote-tracking branch 'origin/renovate/scalar-api-reference-up…
bussyjd Jul 20, 2026
0dd03af
Merge remote-tracking branch 'origin/renovate/ethereum-clients' into …
bussyjd Jul 20, 2026
316d664
Merge remote-tracking branch 'origin/renovate/obolup.sh-dependency-up…
bussyjd Jul 20, 2026
ca07281
Merge remote-tracking branch 'origin/feat/authcapture-unlock' into in…
bussyjd Jul 20, 2026
3fe7ea1
Merge remote-tracking branch 'origin/fix/x402-challenge-url-scheme' i…
bussyjd Jul 20, 2026
0e157e2
fix(x402): drop the model from agent 402 copy + pay-agent
bussyjd Jun 26, 2026
35ee8f0
fix(x402): stop surfacing agentModel in the 402 extra
bussyjd Jun 26, 2026
8fafdbb
fix(x402): keep the internal model out of the bazaar example for agen…
bussyjd Jun 26, 2026
2cddee0
fix(serviceoffer-controller): drop the internal model from /skill.md …
bussyjd Jun 26, 2026
fc5080e
fix(serviceoffer-controller): drop internal model from /api/services.…
bussyjd Jun 27, 2026
84ab5f3
fix(serviceoffer-controller): serve catalog as UTF-8 to stop em-dash …
bussyjd Jun 26, 2026
d65c2ed
Revert "revert(storefront): carve chat widget out of v0.14.0-rc0"
bussyjd Jul 20, 2026
f420d78
fix(x402): surface on-chain settle tx hash on unlock settle failure
bussyjd Jul 20, 2026
6cef65f
fix(chat-widget): cap per-turn spend at displayed price; document MPC…
bussyjd Jul 20, 2026
1eb291e
Merge remote-tracking branch 'origin/feat/authcapture-unlock' into in…
bussyjd Jul 20, 2026
ab28470
Merge branch 'main' into integration/v0.14.0-rc1
bussyjd Jul 20, 2026
f534f60
Merge branch 'main' into feat/chat-widget-reland
bussyjd Jul 21, 2026
d7f191b
Merge feat/chat-widget-reland into integration/v0.14.0-rc1
bussyjd Jul 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions internal/agentcrd/agent_contract_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ import (
// (agentcrd.HostNoBundledSkillsMarkerPath), so Hermes' installer/sync
// skips seeding its ~80 bundled skills;
// (2) the rendered hermes-config ConfigMap in the agent's namespace carries
// the capped knobs: lifetime_seconds: 90, max_turns: 30,
// reasoning_effort: low, and disabled_toolsets {memory, web};
// the capped knobs: lifetime_seconds: 180, max_turns: 30,
// reasoning_effort: low, and disabled_toolsets {memory, web, code_execution};
// (3) a BEHAVIORAL signal that bundled skills were actually skipped — see
// assertBundledSkillsSkippedInPod for why we assert pod filesystem state
// rather than grep a log line.
Expand Down Expand Up @@ -233,7 +233,7 @@ func getHermesConfigYAML(t *testing.T, cfg *config.Config, ns string) string {
func assertHermesConfigCaps(t *testing.T, cfgYAML string) {
t.Helper()
for _, want := range []string{
"lifetime_seconds: 90",
"lifetime_seconds: 180",
"max_turns: 30",
"reasoning_effort: low",
"disabled_toolsets:",
Expand Down
11 changes: 7 additions & 4 deletions internal/buyprompts/buyprompts.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,10 @@ func modelOr(in Input, placeholder string) string {
}

func agentBlock(in Input) Block {
modelFlag := modelOr(in, "<model-id>")
// Deliberately no model anywhere: an Obol Agent runs its own pinned
// model, skills, and memory — the buyer never picks one, pay-agent takes
// no --model, and the agent ignores the chat-completions `model` field,
// so the wire example omits it too.
return Block{
CallShape: CallShape{
Method: "POST",
Expand All @@ -171,8 +174,8 @@ func agentBlock(in Input) Block {
"Use the buy-x402 skill's `pay-agent` command to buy one round of work from this "+
"Obol Agent — it has its own skills, tools, and memory, not just a model. Edit the "+
"message, then run:\n\n"+
"pay-agent %s --model %q --message %q",
in.URL, modelFlag, task(in),
"pay-agent %s --message %q",
in.URL, task(in),
),
PromptGenericLLM: fmt.Sprintf(
"Help me call the Obol Agent at %s — it's an autonomous agent (tools + skills + memory), "+
Expand All @@ -187,7 +190,7 @@ func agentBlock(in Input) Block {
in.URL, task(in),
),
},
Example: ChatExample(in.URL, in.Model, in.TaskExample),
Example: ChatExample(in.URL, "", in.TaskExample),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/embed/infrastructure/cloudflared/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ transport:

image:
repository: cloudflare/cloudflared
tag: "2026.7.1@sha256:188bb03589a32affed3cf4d0590565ffe67b78866e6b5582574afab2b705bafe"
tag: "2026.7.2@sha256:4f6655284ab3d252b7f28fedb19fe6c8fc82ee5b1295c20ac74d475e5398a52d"

metrics:
address: "0.0.0.0:2000"
Expand Down
2 changes: 1 addition & 1 deletion internal/embed/infrastructure/helmfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ repositories:
# Single source of truth: change this to switch networks
values:
- network: mainnet
- gatewayApiVersion: v1.6.0
- gatewayApiVersion: v1.6.1
# Default the cloudflared release to enabled. `obol stack up` overrides via
# `--state-values-set cloudflared.enabled=false` when it detects a running
# quick tunnel that should be preserved across the sync.
Expand Down
22 changes: 18 additions & 4 deletions internal/embed/networks/ethereum/helmfile.yaml.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,34 @@ releases:
image:
{{- if eq .Values.executionClient "reth" }}
# renovate: datasource=github-releases depName=paradigmxyz/reth
tag: v2.3.0
tag: v2.4.1
{{- else if eq .Values.executionClient "geth" }}
# renovate: datasource=github-releases depName=ethereum/go-ethereum
tag: v1.17.4
{{- else if eq .Values.executionClient "nethermind" }}
# renovate: datasource=github-releases depName=NethermindEth/nethermind
tag: "1.39.0"
tag: "1.39.1"
{{- else if eq .Values.executionClient "besu" }}
# renovate: datasource=github-releases depName=hyperledger/besu
tag: "26.7.0"
{{- else if eq .Values.executionClient "erigon" }}
# renovate: datasource=github-releases depName=erigontech/erigon
tag: v3.5.1
tag: v3.5.2
{{- end }}
{{- if eq .Values.executionClient "reth" }}
# reth keeps p2p (30303) closed while running post-crash consistency
# recovery, which can exceed any bounded probe window on a large
# datadir. The chart's default tcp liveness probe then SIGKILLs it
# mid-recovery, forcing a longer recovery on the next start — a
# permanent crash loop. A genuinely dead process exits the container
# on its own, so make the probe effectively never fire.
livenessProbe:
tcpSocket:
port: p2p-tcp
initialDelaySeconds: 60
periodSeconds: 120
failureThreshold: 10000
{{- end }}
persistence:
enabled: true
size: {{ if (index .Values "executionStorageSize" | default "") }}{{ index .Values "executionStorageSize" }}{{ else }}{{ if eq .Values.network "mainnet" }}{{ if eq .Values.mode "archive" }}4500Gi{{ else }}500Gi{{ end }}{{ else }}{{ if eq .Values.mode "archive" }}300Gi{{ else }}100Gi{{ end }}{{ end }}{{ end }}
Expand All @@ -120,7 +134,7 @@ releases:
tag: v8.2.0
{{- else if eq .Values.consensusClient "prysm" }}
# renovate: datasource=github-releases depName=OffchainLabs/prysm
tag: v7.1.6
tag: v7.1.7
{{- else if eq .Values.consensusClient "teku" }}
# renovate: datasource=github-releases depName=Consensys/teku
tag: "26.7.1"
Expand Down
6 changes: 3 additions & 3 deletions internal/embed/skills/buy-x402/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py go <u
`go` never creates persistent state; for a pre-authorized inference pool it points you at `buy`. The expert flows underneath, picked by usage shape:

- **`pay <url>`** — single-shot. Probe the URL, sign **one** payment authorization, attach `X-PAYMENT`, send the request, return the response. Stateless. Use for `type:http` services and any one-off purchase. Max loss = price of one request. Settlement normally lands only after the request succeeds — but a facilitator can submit the settle tx on-chain and *then* fail the request. When that happens the failure report prints `⚠️ SETTLEMENT MAY HAVE COMPLETED ON-CHAIN` with the tx hash: verify with `balance --chain <X>` before retrying (mechanism: docs/observability.md, "Verify settlement against the chain"). Applies to `pay-agent` too.
- **`pay-agent <url> --model <id>`** — single-shot paid **streaming** agent call. Same payment shape as `pay` (one auth, X-PAYMENT, max-loss = price), but POSTs to `<url>/v1/chat/completions` with `stream: true` and forwards every SSE event verbatim to stdout as it arrives. Use this for `type:agent` ServiceOffers when the calling agent wants to consume the response *itself* (memory, tool-call traces, partial results) instead of routing it through LiteLLM as a paid alias. Default HTTP read timeout is **1 hour** — agent calls can legitimately run for many minutes; override with `--timeout <seconds>`.
- **`pay-agent <url>`** — single-shot paid **streaming** agent call. Same payment shape as `pay` (one auth, X-PAYMENT, max-loss = price), but POSTs to `<url>/v1/chat/completions` with `stream: true` and forwards every SSE event verbatim to stdout as it arrives. No `--model`: a `type:agent` offer runs its own model (the request `model` field is ignored), so you only send a prompt. Use this for `type:agent` ServiceOffers when the calling agent wants to consume the response *itself* (memory, tool-call traces, partial results) instead of routing it through LiteLLM as a paid alias. Default HTTP read timeout is **1 hour** — agent calls can legitimately run for many minutes; override with `--timeout <seconds>`.
- **`buy <name>`** — pre-authorize a budget. Sign **N** authorizations up front (the buyer pays nothing yet), declare them in a `PurchaseRequest` CR, let the `x402-buyer` sidecar redeem them transparently as the agent calls the model through LiteLLM at `paid/<remote-model>`. Use for long-running paid inference. Max loss = N × price (only as vouchers are spent); runtime path holds zero signer access.
- **`buy <name> --model <id> --set-default`** — same as `buy` above, then adopt `paid/<remote-model>` as the agent's **own primary model**, in-pod, by itself: an atomic `hermes config set model.default` that Hermes re-reads per request (effective next chat turn, **no restart**, no host-side `obol model prefer`/`obol model sync`). Refuses if the model isn't selectable in LiteLLM. Pair with `--auto-refill` so the primary model doesn't brick when the pre-authorized budget runs out.

Expand Down Expand Up @@ -170,7 +170,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay h
# One-shot paid STREAMING agent call (SSE events flushed to stdout as they arrive)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
https://seller.example.com/services/demo-quant \
--model qwen3.5:9b --message 'summarize the latest research on staking'
--message 'summarize the latest research on staking'

# Pay-agent with a full OpenAI-compatible body (stream:true is forced on)
python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py pay-agent \
Expand Down Expand Up @@ -224,7 +224,7 @@ python3 ${OBOL_SKILLS_DIR:-/data/.openclaw/skills}/buy-x402/scripts/buy.py maint
| `go <url> [--message <text>] [--data <json>] [--method GET\|POST]` | Probe, detect offer type (agent / chat inference / http), dispatch to the right flow |
| `probe <url> [--model <id>] [--type http\|inference\|agent] [--method GET\|POST]` | Send request without payment, parse 402 response for pricing |
| `pay <url> [--type http\|inference] [--method GET\|POST] [--data <body>]` | Single-shot paid request: sign 1 auth, attach X-PAYMENT, send |
| `pay-agent <url> --model <id> [--message <text> \| --data <json>] [--timeout <s>]` | Single-shot paid streaming agent call: SSE events flush to stdout as they arrive (default timeout 1h) |
| `pay-agent <url> [--message <text> \| --data <json>] [--timeout <s>]` | Single-shot paid streaming agent call (no `--model` — the agent runs its own): SSE events flush to stdout as they arrive (default timeout 1h) |
| `buy <name> --endpoint <url> --model <id> [--budget <amount>] [--count N]` | Pre-sign auths, create/update `PurchaseRequest`, expose `paid/<model>`. `--budget` takes atomic units or token units (`'1.5 USDC'`) |
| `buy <name> --endpoint <url> --model <id> --set-default [--auto-refill]` | As above, then set `paid/<model>` as the agent's own primary model in-pod (no restart, no host CLI) |
| `process <name> \| --all` | Reconcile `autoRefill` policies against live `x402-buyer` status |
Expand Down
29 changes: 14 additions & 15 deletions internal/embed/skills/buy-x402/scripts/buy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2647,7 +2647,7 @@ def cmd_pay(url, method="GET", data=None, kind="http", network=None, timeout=Non
sys.exit(1)


def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None, body=None, token=None, payment_option=None):
def cmd_pay_agent(url, messages=None, network=None, timeout=None, body=None, token=None, payment_option=None):
"""Single-shot paid streaming agent call: probe -> sign one auth -> SSE-stream.

Sibling of `cmd_pay` for `type=agent` ServiceOffers. Differences from
Expand All @@ -2667,9 +2667,10 @@ def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None,
alias.

`body` is an optional JSON-encoded request body. When omitted, `messages`
+ `model_id` are required and a `{model, messages, stream:true}` body is
synthesized. When provided, the body is parsed and `"stream": true` is
forced onto whatever the caller passed.
is required and a `{messages, stream:true}` body is synthesized — NO `model`
field: a type=agent offer runs its own model and ignores any `model` sent.
When provided, the body is parsed and `"stream": true` is forced onto
whatever the caller passed.
"""
if timeout is None or float(timeout) <= 0:
timeout = 3600.0
Expand All @@ -2689,27 +2690,24 @@ def cmd_pay_agent(url, messages=None, model_id=None, network=None, timeout=None,
# Force streaming on. cmd_pay handles non-streaming; cmd_pay_agent
# exists precisely to stream.
parsed_body["stream"] = True
if model_id and not parsed_body.get("model"):
parsed_body["model"] = model_id
else:
if not messages:
print(
"Error: --message (or --data <json>) is required for `pay-agent`.\n"
"Example: pay-agent <url> --model qwen3.5:9b --message 'summarize the docs'",
"Example: pay-agent <url> --message 'summarize the docs'",
file=sys.stderr,
)
sys.exit(1)
if not model_id:
print("Error: --model is required when using --message.", file=sys.stderr)
sys.exit(1)
# type=agent ServiceOffers run their own model — there is nothing to
# select and the agent ignores any `model` field — so pay-agent sends
# only the prompt.
parsed_body = {
"model": model_id,
"messages": [{"role": "user", "content": messages}],
"stream": True,
}

print(f"Probing {url} ...")
pricing = _probe_endpoint(url, model_id=model_id or "test", kind="inference")
pricing = _probe_endpoint(url, model_id="probe", kind="inference")
if not pricing:
print("Failed to get x402 pricing.", file=sys.stderr)
sys.exit(1)
Expand Down Expand Up @@ -3095,7 +3093,9 @@ def usage():
print(" pay <url> [--type http|inference] [--method GET|POST] [--data '<body>'] [--timeout <seconds>]")
print(" [--token <SYMBOL>] [--network <name>] [--payment-option <N>]")
print(" Single-shot paid request (sign 1 auth, attach X-PAYMENT)")
print(" pay-agent <url> --model <id> [--message '<text>' | --data '<json>'] [--timeout <seconds>]")
print(" Multi-currency offers: pick which asset/price to pay with")
print(" --token/--network/--payment-option (probe to see options)")
print(" pay-agent <url> [--message '<text>' | --data '<json>'] [--timeout <seconds>]")
print(" [--token <SYMBOL>] [--network <name>] [--payment-option <N>]")
print(" Single-shot paid streaming agent call (POST /v1/chat/completions,")
print(" stream: true). Each SSE event flushes to stdout. Default timeout 1h.")
Expand Down Expand Up @@ -3190,7 +3190,7 @@ def usage():
positional, opts = parse_flags(rest)
if not positional:
print(
"Usage: pay-agent <url> --model <id> [--message '<text>' | --data '<json>'] "
"Usage: pay-agent <url> [--message '<text>' | --data '<json>'] "
"[--network <name>] [--timeout <seconds>]",
file=sys.stderr,
)
Expand All @@ -3207,7 +3207,6 @@ def usage():
cmd_pay_agent(
positional[0],
messages=opts.get("message"),
model_id=opts.get("model"),
network=opts.get("network"),
timeout=timeout,
body=opts.get("data"),
Expand Down
1 change: 1 addition & 0 deletions internal/monetizeapi/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ type ServiceOfferPayment struct {
// x402 payment scheme.
// +kubebuilder:default="exact"
// +kubebuilder:validation:Enum=exact
// TODO(auth-capture): regenerate CRD manifests + wire per-offer unlock config
Scheme string `json:"scheme,omitempty"`
// Chain identifier for payments (human-friendly). Reconciler resolves
// to CAIP-2 format (e.g., "base-sepolia" → "eip155:84532").
Expand Down
55 changes: 55 additions & 0 deletions internal/serviceoffercontroller/assets/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Agent chat widget assets

`chat.html` — the self-contained agent chat page served at `/chat` on every
agent-type offer's dedicated origin. Hand-maintained; no build step. It
derives the agent name from the hostname and price/model/network/asset from
the live 402 challenge, so the same file works for every agent offer on any
stack and network (Base mainnet `eip155:8453` and Base Sepolia
`eip155:84532` are supported).

## Known limitations

- **Session key is derived from a wallet signature** (`keccak256(personal_sign(<EIP-4361 message>))`),
so the wallet must produce a *deterministic* signature for the same message.
Standard RFC-6979 EOAs (MetaMask, Rabby, Ledger, Trezor) do; the `getCode`
guard rejects contract wallets (non-reproducible ERC-1271) while allowing
EIP-7702-delegated EOAs. **Residual gap:** MPC / threshold-ECDSA wallets are
code-less EOAs that pass the guard but sign non-deterministically — funding a
session from one strands the balance on the next visit. There is no on-chain
signal to detect this without a second signature popup, which this
one-signature flow deliberately avoids. Use a standard EOA.
- **Per-turn spend is capped at the price shown when the page loaded** (and the
session balance). A turn whose 402 amount exceeds the displayed price is
refused; a legitimate price change is picked up on reload. Max loss per
session is bounded by what you fund into the session wallet.
- **The signature itself is key material.** Anything that can read it (a
malicious extension, a hooked `window.ethereum`) controls the session funds —
keep session balances small.

`chat-vendor.js` — generated single-file ESM bundle of the widget's
dependencies. Do not edit by hand. Rebuild:

```sh
npm init -y && npm i viem@2.21.25 @x402/fetch@2.18.0 @x402/evm@2.18.0
cat > vendor-entry.mjs <<'EOF'
export { createWalletClient, createPublicClient, custom, http, erc20Abi,
formatUnits, parseUnits, keccak256 } from "viem";
export { privateKeyToAccount } from "viem/accounts";
export { base, baseSepolia } from "viem/chains";
export { wrapFetchWithPayment, x402Client } from "@x402/fetch";
export { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
EOF
npx esbuild vendor-entry.mjs --bundle --format=esm --minify --target=es2022 \
--outfile=chat-vendor.js
```

When the bundle is rebuilt, bump the `?v=` cache-buster on the
`chat-vendor.js` import in `chat.html` to the new sha256's first 8 hex
chars — intermediaries (e.g. Cloudflare) cache `.js` aggressively.

sha256 of the committed bundle:
`895fd923aa84d7cf80e2b1df299068aa38dba7307a9a380526c0b5426489724d`

The pinned versions are the exact pair validated end-to-end against the
x402-verifier with real on-chain settlements (X-PAYMENT v1 and
PAYMENT-SIGNATURE v2 flows both accepted since #690).
57 changes: 57 additions & 0 deletions internal/serviceoffercontroller/assets/chat-vendor.js

Large diffs are not rendered by default.

Loading
Loading