Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
107 changes: 0 additions & 107 deletions collect-ux-layers-1-3.md

This file was deleted.

2 changes: 2 additions & 0 deletions scw_js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions scw_js/openapi.llm.json
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
9 changes: 5 additions & 4 deletions scw_js/sc_llm_x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
2 changes: 2 additions & 0 deletions scw_js/test/sc_llm_x402.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
44 changes: 43 additions & 1 deletion website/components/AgentInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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 (
<div className={css({ fontSize: "xs", color: "gray.700" })}>
<div className={css({ fontWeight: "medium" })}>{agentCard.operator ?? agentCard.origin}</div>
<div className={css({ color: "gray.500", wordBreak: "break-all" })}>{agentCard.origin}</div>
{agentCard.payTo && (
<div className={css({ fontFamily: "mono", color: "gray.600", mt: "1" })} title="Payment recipient">
pays → {agentCard.payTo.slice(0, 6)}…{agentCard.payTo.slice(-4)}
</div>
)}
{isThirdParty && (
<div className={css({ color: "amber.600", mt: "1" })}>third-party agent · at your own risk</div>
)}
</div>
);
}

// 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 (
<div className={css({ fontSize: "xs", color: "gray.500", textAlign: isSidebar ? "left" : "center", mt: "2" })}>
Loading agent…
</div>
);
}

if (isLoading) {
return (
<div className={css({ fontSize: "xs", color: "gray.500", textAlign: isSidebar ? "left" : "center", mt: "2" })}>
Expand Down
105 changes: 105 additions & 0 deletions website/components/AgentSelector.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className={css({ mt: "3" })}>
<div className={css({ fontSize: "xs", color: "gray.500", mb: "1" })}>
{customCard ? "Custom agent (at your own risk)" : "Use a different agent"}
</div>

{customCard ? (
// Pre-payment disclosure for the selected custom agent (G4/G6).
<div>
<div className={monoStyle}>{customCard.operator ?? customCard.origin}</div>
{customCard.payTo && (
<div className={monoStyle} title="Payment recipient">
pays → {customCard.payTo.slice(0, 6)}…{customCard.payTo.slice(-4)}
</div>
)}
<div className={rowStyle}>
<button onClick={onUseDefaultAgent} className={`${styles.actionButton} ${styles.actionButtonSecondary}`}>
Back to default agent
</button>
</div>
</div>
) : (
<div>
<input
type="url"
inputMode="url"
placeholder="https://another-agent.example"
value={customUrlInput}
onChange={(e) => onCustomUrlInputChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
onTryCustomAgent();
}
}}
disabled={checking}
className={inputStyle}
/>
<div className={rowStyle}>
<button
onClick={onTryCustomAgent}
disabled={checking || !customUrlInput.trim()}
className={styles.actionButton}
>
{checking ? "Checking…" : "Use this agent"}
</button>
</div>
{checkState === "error" && checkError && <div className={errorStyle}>{checkError}</div>}
</div>
)}
</div>
);
}

export default AgentSelector;
Loading
Loading