diff --git a/.gitignore b/.gitignore index 2ffa2a6b..609e0df6 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,7 @@ node_modules/ # Next.js build artifacts .next/ next-env.d.ts +tsconfig.tsbuildinfo # Environment variables .env diff --git a/internal/x402/paymentrequired.go b/internal/x402/paymentrequired.go index aa7e3152..d23c261d 100644 --- a/internal/x402/paymentrequired.go +++ b/internal/x402/paymentrequired.go @@ -421,6 +421,17 @@ func inferenceCopy(url, siteURL string, d PaymentDisplay) typeCopy { PrimaryPayload: cmd, PromptObol: prompt, PromptOther: other, + ChatCompletionsNote: "Direct HTTP buyers use OpenAI-style chat-completions. A minimal paid request looks like:", + ChatCompletionsBody: fmt.Sprintf(`POST %s/v1/chat/completions +Content-Type: application/json +X-PAYMENT: + +{ + "model": "%s", + "messages": [ + {"role": "user", "content": ""} + ] +}`, strings.TrimSuffix(url, "/"), model), } } @@ -450,11 +461,12 @@ X-PAYMENT: ] }`, url, modelClause) + modelFlag := sanitizeDisplayToken(d.Model, "") prompt := fmt.Sprintf( - "Use the buy-x402 skill's `pay` command to call the Obol Agent at %s%s. "+ - "This is an *agent*, not a raw model — it has its own skills, tools, and memory. "+ - "Include a clear instruction in the chat-completions body so the agent knows what to do.", - url, modelLine, + "Use the buy-x402 skill's `pay-agent` command to buy one round of work from this Obol Agent%s. "+ + "This is an *agent*, not a raw model — it has its own skills, tools, and memory. Example:\n\n"+ + "pay-agent %s --model %s --message \"\"", + modelLine, url, modelFlag, ) other := fmt.Sprintf( @@ -495,7 +507,10 @@ func httpCopy(url, siteURL string, d PaymentDisplay) typeCopy { if d.NetworkLabel != "" { netClause = " Network: " + d.NetworkLabel + "." } - prompt := fmt.Sprintf("Use the buy-x402 skill's `pay` command to call %s once.%s%s", url, priceClause, netClause) + prompt := fmt.Sprintf( + "Use the buy-x402 skill's `pay` command to call %s once.%s%s "+ + "Use the method and payload the seller documents.", + url, priceClause, netClause) priceWord := "the listed price" if d.PriceDisplay != "" { diff --git a/internal/x402/paymentrequired_test.go b/internal/x402/paymentrequired_test.go index daf41a2c..a9ef9563 100644 --- a/internal/x402/paymentrequired_test.go +++ b/internal/x402/paymentrequired_test.go @@ -210,6 +210,8 @@ func TestHTMLAware_InferenceShowsCLIPrimaryAndDescription(t *testing.T) { // in "operator's hardware" appears raw, not entity-encoded. mustContain(t, body, "agent runs locally") mustContain(t, body, "remote operator's hardware") + mustContain(t, body, "OpenAI-style chat-completions") + mustContain(t, body, `/v1/chat/completions`) } // Agent offers should explain that the buyer is paying an autonomous @@ -284,6 +286,7 @@ func TestHTMLAware_HTTPKeepsLegacyCopy(t *testing.T) { body := w.Body.String() mustContain(t, body, "Pay with your Obol Agent") mustContain(t, body, "buy-x402 skill") + mustContain(t, body, "Use the method and payload the seller documents") if strings.Contains(body, "obol buy inference") { t.Errorf("http-type 402 page should NOT show the inference CLI primary card") } diff --git a/web/public-storefront/package-lock.json b/web/public-storefront/package-lock.json index 59e712c9..27b417d5 100644 --- a/web/public-storefront/package-lock.json +++ b/web/public-storefront/package-lock.json @@ -990,6 +990,7 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1500,6 +1501,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1509,6 +1511,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, diff --git a/web/public-storefront/src/components/ServiceCard.tsx b/web/public-storefront/src/components/ServiceCard.tsx index 34d9f796..65de6af1 100644 --- a/web/public-storefront/src/components/ServiceCard.tsx +++ b/web/public-storefront/src/components/ServiceCard.tsx @@ -54,16 +54,48 @@ function normalizeOfferType(t: string): "inference" | "agent" | "http" { } type Tab = "agent" | "other-ai" | "code"; +const AGENT_TASK_PLACEHOLDER = "Summarise the README and list the top 3 risks."; + +function resolvedAgentTask(task: string): string { + return task.trim() || AGENT_TASK_PLACEHOLDER; +} + +function quoteAgentTask(task: string): string { + return JSON.stringify(resolvedAgentTask(task)); +} + +function buildAgentPayAgentCommand( + endpoint: string, + model: string | undefined, + agentTask: string, +): string { + const modelId = model || ""; + return `pay-agent ${endpoint} --model ${JSON.stringify(modelId)} --message ${quoteAgentTask(agentTask)}`; +} + +function buildAgentObolPrompt( + endpoint: string, + model: string | undefined, + agentTask: string, +): string { + return `Use the buy-x402 skill's \`pay-agent\` command to buy one round of work from this Obol Agent (skills, tools, and memory — not a raw LLM): + +${buildAgentPayAgentCommand(endpoint, model, agentTask)}`; +} export function ServiceCard({ service }: { service: Service }) { const [open, setOpen] = useState(false); const [tab, setTab] = useState("agent"); const [copied, setCopied] = useState(false); + const [agentTask, setAgentTask] = useState(""); const options = paymentOptions(service); const [optIdx, setOptIdx] = useState(0); const opt = options[optIdx] ?? options[0]; const multiPay = options.length > 1; + const kind = normalizeOfferType(service.type); + const needsAgentTask = kind === "agent"; + const taskReady = agentTask.trim().length > 0; const anchorId = `service-${service.name}`; const copyAnchor = () => { @@ -194,11 +226,61 @@ export function ServiceCard({ service }: { service: Service }) { )} + {needsAgentTask && ( +
+ +