diff --git a/collect-ux-layers-1-3.md b/collect-ux-layers-1-3.md
deleted file mode 100644
index 6fc402472..000000000
--- a/collect-ux-layers-1-3.md
+++ /dev/null
@@ -1,107 +0,0 @@
-# Collect/Mint UX — Problems & Mitigations (Layers 1–3)
-
-Reference note for a "collect / mint" button. Scope: **payment currency, wallet, gas, chain** friction.
-Out of scope here: fiat on-ramps, Apple Pay, the full x402 protocol.
-Principle: **KISS** — each layer lists the *smallest fix that removes the failure* first, then the fuller option.
-
-## The core idea
-
-"L2 complexity" is not one problem. It is a stack of frictions, and a user can fail at any one. Fix them independently. The payment-currency choice (Layer 0) sits underneath the rest — decide it first, because it changes everything above it.
-
-| Layer | What the user hits | Failure mode |
-|---|---|---|
-| 0. Currency | "The price is some tiny ETH fraction" | Confused before starting |
-| 1. Wallet | "I have to install something / manage a seed phrase" | Never starts |
-| 2. Gas | "I have no ETH for fees on this chain" | Connected, can't pay |
-| 3. Chain | "Which network? Wrong network." | Connected, funded, wrong place |
-
----
-
-## Layer 0 — Payment currency (cross-cutting; decide this first)
-
-**Problem:** Pricing/paying in native ETH means unreadable prices ("0.00031 ETH"), forces users to hold the gas token, and blocks clean chain abstraction.
-
-**Mitigation (KISS):** Denominate the collect in a **regulated stablecoin** — USDC, and/or **EURC** for a European audience. This one decision improves Layers 2 and 3 at the same time.
-
-**Why it's the keystone:** USDC/EURC are *programmable*. They support **EIP-3009** (`receiveWithAuthorization`): the user signs one off-chain authorization, the contract pulls the exact amount, no persistent allowance, bound to a specific recipient. That is the gasless, single-signature payment primitive the layers below build on. It is also the clean fix for the approve/`transferFrom` "spender" security warnings.
-
-**Cross-chain bonus:** USDC has native burn-and-mint transfer via **Circle CCTP V2** (Fast Transfer ~8–20s, Hooks for same-transaction destination logic) — no wrapped tokens, no bridge liquidity pools. This is why "pay from any chain" becomes near-trivial once you are USDC-denominated.
-
-**Honest trade-off:** USDC/EURC carry a Circle **freeze/blacklist** function; native ETH does not. You trade a slice of censorship-resistance for a large UX gain. Usually an easy trade for a creative-collect button — but name it, don't discover it later.
-
-**Note:** x402 is one productized, backend-settled way to consume EIP-3009. Out of scope for this doc, but the EIP-3009 primitive is available to you with or without it.
-
----
-
-## Layer 1 — Wallet & keys
-
-**Problem:** Requires an injected wallet (MetaMask etc.) and a seed phrase. Non-crypto users bounce immediately.
-
-**Root cause:** The flow gates on a connected EOA from a browser-extension wallet.
-
-**Mitigation (KISS):** Offer a passkey smart wallet as an *additional* entry point (FaceID / no seed phrase), keeping "connect existing wallet" for crypto-natives.
-- ERC-4337 → new smart-contract accounts (passkey sign-in, social recovery).
-- ERC-7702 → upgrades an *existing* EOA in place (live since Pectra, May 2025).
-
-**Smallest first step:** Don't remove wallet-connect. Add one passkey option beside it.
-
-**Openness / trade-off:** Account contracts are open-source (Safe, Coinbase Smart Wallet). You depend on a swappable bundler + paymaster service (Pimlico / ZeroDev / Alchemy / self-host). Infra dependency, not vendor lock-in.
-
-*Unchanged by the USDC move — key management is orthogonal to payment currency.*
-
----
-
-## Layer 2 — Gas (highest leverage — and USDC makes it nearly free)
-
-**Problem:** User must hold native ETH on the exact L2 to pay gas. The most common dead-end.
-
-**Root cause:** User pays their own gas, and the mint is priced in native ETH.
-
-**Mitigation (KISS):** With Layer 0 in place, the payment is a **signature**, and someone else submits the transaction and pays gas:
-- A relayer / paymaster fronts gas → user needs zero ETH.
-- EIP-3009 means no separate "approve" transaction either — one signature covers everything.
-
-So "pay in USDC" delivers "gasless" almost for free; you stop solving gas as a separate problem. (A plain sponsoring paymaster on an ETH-priced flow still works if you delay Layer 0, but it's strictly less clean.)
-
-**Smallest first step:** EIP-3009 authorization relayed by your backend/facilitator, or an AA paymaster. User signs once, holds no ETH.
-
-**Openness / trade-off:** ERC-4337 and EIP-3009 are open and vendor-neutral. You fund the relayer/paymaster (real cost). Cap / allowlist to avoid drain.
-
----
-
-## Layer 3 — Chains
-
-**Problem:** User must know the right L2 and switch networks. Even technical users get confused.
-
-**Root cause:** Network-switching logic puts chain selection on the user; funds may sit on another chain.
-
-**Mitigation (KISS), simplest → fullest:**
-1. **Single chain, no switching.** Commit to one L2, remove the switcher. Simplest fix; removes a whole decision.
-2. **Accept-from-any-chain.** Once USDC-denominated, this is well-supported:
- - **CCTP V2** for native USDC burn-and-mint from other chains.
- - **ERC-7683 intents** (Across, Uniswap, CoW, Eco) or **Daimo Pay** (non-custodial, open contracts) to accept any coin / any chain and settle USDC on yours.
-
-**Watch-out (now inverted):** Previously, an ETH-denominated bonding curve broke stablecoin routers. **USDC denomination removes that** — "settle as USDC on your chain" now maps directly onto your contract. The former obstacle has become the enabler.
-
-**Openness / trade-off:** CCTP is permissionless Circle infra. Intents / Daimo = open contracts + non-custodial, but rely on hosted solver/routing (leaveable; self-runnable).
-
----
-
-## KISS priority — do these in order
-
-1. **Move to USDC/EURC + EIP-3009 (Layer 0).** The keystone. Retires ETH-fraction pricing, enables gasless, and unblocks chain abstraction — one decision, three wins.
-2. **Gasless via relayer/paymaster (Layer 2).** Falls out of Layer 0 almost for free.
-3. **Drop the chain switcher (Layer 3, option 1).** Commit to one L2.
-4. **Passkey option (Layer 1).** Widen the door for non-crypto users.
-5. **Full chain abstraction (Layer 3, option 2)** — only if users genuinely arrive with funds on other chains. CCTP / intents make this cheap once USDC-denominated.
-
-## The one decision to make first
-
-- **Trustless path:** contract pulls USDC via `receiveWithAuthorization`; user signs one gasless auth; relayer / AA submits. Direct-to-contract, max decentralization.
-- **Backend-settled path:** a facilitator settles the USDC payment, your backend mints. Reuses payment infra; adds a trust assumption on your backend.
-
-**USDC + EIP-3009 narrows the UX gap between these to almost nothing** — both are a single gasless signature. So decide on **trust / decentralization grounds**, not UX. Given a coupling-averse stance, the trustless path is now cheap enough to keep.
-
-## Contract-side reality check
-
-Moving to USDC is a **UUPS upgrade**: accept an ERC-20 via `receiveWithAuthorization`, re-denominate the bonding curve in USDC's **6 decimals** (cleaner than 18-decimal wei), and use **native USDC** on Optimism, not bridged `USDC.e`. Feasible on a proxy setup, but it touches the mint signature and price storage — the one place that needs care.
diff --git a/scw_js/README.md b/scw_js/README.md
index f1714ec28..d8f1b7579 100644
--- a/scw_js/README.md
+++ b/scw_js/README.md
@@ -68,6 +68,8 @@ Generates AI images using Black Forest Labs API with USDC payment via x402 proto
LLM chat paid via x402 batch-settlement USDC payment channels — no bearer token, the payment voucher itself proves wallet control. `llmx402` handles chat requests (deposit/voucher/402 flow); `llmx402cron` claims and settles accumulated channels on a 12h schedule. See [`assistent_plan.md`](../assistent_plan.md) at the repo root for the full design record (deposit/voucher/claim/settle lifecycle, pricing model, gotchas).
+**`llm/v1` contract.** [`openapi.llm.json`](./openapi.llm.json) declares `x-service-type: "llm/v1"` — the interchangeable-agent contract. It is defined entirely by that document: the request/response schema (`LLMChatRequest`/`LLMChatResponse`) plus the `x-interop-floor` (a compatible agent must advertise ≥1 `accepts[]` entry with USDC on Base `eip155:8453`, scheme `batch-settlement`). A `v2` aligning to the OpenAI chat-completions shape is the intended evolution, not yet built.
+
### `growth_api.ts` - Growth Agent Draft Approval
API for reviewing, editing, and approving AI-generated social media drafts. Used by the Growth Agent notebooks and cron job.
diff --git a/scw_js/openapi.llm.json b/scw_js/openapi.llm.json
index 5e965402f..bb3a75ef3 100644
--- a/scw_js/openapi.llm.json
+++ b/scw_js/openapi.llm.json
@@ -16,6 +16,8 @@
"0x08fd2874a7a85b7250830bf6be396953c108e197739ccc758e373036b2fe78a71ddcaf48183690b5f1d2a0049eb0af93068fb35eaaf59d266168eaeea1df357d1c"
]
},
+ "x-service-type": "llm/v1",
+ "x-interop-floor": "A compatible llm/v1 agent MUST advertise at least one accepts[] entry with asset USDC on network Base (eip155:8453), scheme batch-settlement. Request/response schema is defined by this document's LLMChatRequest/LLMChatResponse. See README.md.",
"servers": [{ "url": "https://llm-agent.fretchen.eu" }],
"tags": [
{ "name": "LLM", "description": "AI chat assistant / text completion" },
diff --git a/scw_js/sc_llm_x402.ts b/scw_js/sc_llm_x402.ts
index d97d41551..15053dd9f 100644
--- a/scw_js/sc_llm_x402.ts
+++ b/scw_js/sc_llm_x402.ts
@@ -36,10 +36,11 @@ const logger = pino({ level: process.env.LOG_LEVEL ?? "info" });
// The REAL, usage-derived charge is computed after the LLM call and passed to
// settlePayment() as a *separate*, smaller requirements.amount — handleBeforeSettle only
// enforces chargedCumulativeAmount + requirements.amount <= voucher.maxClaimableAmount (a
-// ceiling, not equality), so verify and settle are free to use different amounts. This is
-// the "authorize an upper bound, claim the real amount" pattern the SDK's
-// setSettlementOverrides() wraps for Express apps — we do it manually here since we call
-// settlePayment() directly. See getSettleAmount() below.
+// ceiling, not equality), so verify and settle are free to use different amounts. We
+// implement this "authorize an upper bound, claim the real amount" split manually: verify
+// with the ceiling amount, then pass a smaller usage-derived amount to settlePayment().
+// (The installed @x402/evm — 2.18.0 — exposes no higher-level helper for this; we call the
+// resourceServer verify/settle primitives directly.) See getSettleAmount() below.
// This endpoint uses Mistral, not IONOS — see llm_service.ts's LLM_PROVIDERS. Legacy
// sc_llm.ts (merkle settlement) is untouched and stays on IONOS.
const LLM_PROVIDER = "mistral";
diff --git a/scw_js/test/sc_llm_x402.test.ts b/scw_js/test/sc_llm_x402.test.ts
index df7ab91ce..2710ad1ea 100644
--- a/scw_js/test/sc_llm_x402.test.ts
+++ b/scw_js/test/sc_llm_x402.test.ts
@@ -208,6 +208,8 @@ describe("sc_llm_x402", () => {
const body = JSON.parse(res.body);
expect(body.openapi).toBe("3.1.0");
expect(body.info.title).toBeTruthy();
+ // Declares the llm/v1 service contract so discovery clients can classify it.
+ expect(body["x-service-type"]).toBe("llm/v1");
expect(body.paths["/"].post["x-payment-info"]).toEqual({
protocols: ["x402"],
price: { mode: "dynamic", currency: "USD", min: "0", max: "0.003" },
diff --git a/website/components/AgentInfoPanel.tsx b/website/components/AgentInfoPanel.tsx
index 56c8f8936..6d406e298 100644
--- a/website/components/AgentInfoPanel.tsx
+++ b/website/components/AgentInfoPanel.tsx
@@ -20,15 +20,19 @@ import { useAgentInfo } from "../hooks/useAgentInfo";
import { useLocale } from "../hooks/useLocale";
import { useAutoNetwork } from "../hooks/useAutoNetwork";
import { getGenAiNFTAddress, GENAI_NFT_NETWORKS } from "@fretchen/chain-utils";
+import type { AgentCard } from "../hooks/x402Discovery";
interface AgentInfoPanelProps {
// Service context (for display purposes)
service?: "genimg" | "llm";
// Layout variant
variant?: "footer" | "sidebar";
+ // When set (llm escape hatch), show this pre-checked agent's provenance instead of the
+ // default single-tenant registration file. Derived live from the agent's own /openapi.json.
+ agentCard?: AgentCard | null;
}
-export function AgentInfoPanel({ service = "genimg", variant = "footer" }: AgentInfoPanelProps) {
+export function AgentInfoPanel({ service = "genimg", variant = "footer", agentCard = null }: AgentInfoPanelProps) {
const [isExpanded, setIsExpanded] = useState(false);
const { agent, isLoading, error } = useAgentInfo();
@@ -40,6 +44,44 @@ export function AgentInfoPanel({ service = "genimg", variant = "footer" }: Agent
const isSidebar = variant === "sidebar";
+ // llm path: render honest provenance (operator + origin + payTo) read live from the agent's
+ // own /openapi.json + 402 — who the user actually pays. Third-party agents (not a *.fretchen.eu
+ // origin) additionally carry an at-your-own-risk note. The multi-agent picker that would make
+ // a third-party card appear here isn't rendered yet (see AssistantChat / the plan).
+ if (agentCard) {
+ let isThirdParty = true;
+ try {
+ isThirdParty = !new URL(agentCard.origin).hostname.endsWith("fretchen.eu");
+ } catch {
+ // Unparseable origin — treat as third-party (safer disclosure).
+ }
+ return (
+
+
{agentCard.operator ?? agentCard.origin}
+
{agentCard.origin}
+ {agentCard.payTo && (
+
+ pays → {agentCard.payTo.slice(0, 6)}…{agentCard.payTo.slice(-4)}
+
+ )}
+ {isThirdParty && (
+
third-party agent · at your own risk
+ )}
+
+ );
+ }
+
+ // The llm service is card-driven (above). Before the card resolves, show a compact
+ // placeholder rather than falling through to the legacy registration-file rendering (which
+ // carries the retired "Become a provider" / EIP-8004 JSON links we don't want on /assistent).
+ if (service === "llm") {
+ return (
+
+ Loading agent…
+
+ );
+ }
+
if (isLoading) {
return (
diff --git a/website/components/AgentSelector.tsx b/website/components/AgentSelector.tsx
new file mode 100644
index 000000000..925bd3c41
--- /dev/null
+++ b/website/components/AgentSelector.tsx
@@ -0,0 +1,105 @@
+/**
+ * AgentSelector — the /assistent escape hatch (open-agent-platform G3/G4).
+ *
+ * Lets the user point the chat at any llm/v1 agent by URL. The URL is pre-checked
+ * (`precheckLlmV1Agent`) before it can be selected, and the resulting provenance
+ * (operator + payTo + origin) is shown so the user knows who they are about to pay.
+ * State lives in the parent (AssistantChat) because the selected URL also feeds useX402Chat.
+ */
+import React from "react";
+import { css } from "../styled-system/css";
+import * as styles from "../layouts/styles";
+import type { AgentCard } from "../hooks/x402Discovery";
+
+export interface AgentSelectorProps {
+ /** The pasted URL (controlled input). */
+ customUrlInput: string;
+ onCustomUrlInputChange: (value: string) => void;
+ /** Provenance of the currently selected custom agent, or null when on the default. */
+ customCard: AgentCard | null;
+ checkState: "idle" | "checking" | "error";
+ checkError: string | null;
+ onTryCustomAgent: () => void;
+ onUseDefaultAgent: () => void;
+}
+
+const rowStyle = css({ display: "flex", gap: "2", flexWrap: "wrap", alignItems: "center", mt: "2" });
+const monoStyle = css({ fontFamily: "mono", fontSize: "xs", color: "gray.600", wordBreak: "break-all" });
+const errorStyle = css({ fontSize: "xs", color: "red.600", mt: "1" });
+const inputStyle = css({
+ width: "100%",
+ fontSize: "xs",
+ px: "2",
+ py: "1",
+ border: "1px solid",
+ borderColor: "gray.300",
+ borderRadius: "md",
+ _focus: { outline: "none", borderColor: "brand" },
+});
+
+export function AgentSelector({
+ customUrlInput,
+ onCustomUrlInputChange,
+ customCard,
+ checkState,
+ checkError,
+ onTryCustomAgent,
+ onUseDefaultAgent,
+}: AgentSelectorProps) {
+ const checking = checkState === "checking";
+
+ return (
+
+
+ {customCard ? "Custom agent (at your own risk)" : "Use a different agent"}
+
+
+ {customCard ? (
+ // Pre-payment disclosure for the selected custom agent (G4/G6).
+
+
{customCard.operator ?? customCard.origin}
+ {customCard.payTo && (
+
+ pays → {customCard.payTo.slice(0, 6)}…{customCard.payTo.slice(-4)}
+