diff --git a/internal/x402/paymentrequired.go b/internal/x402/paymentrequired.go index 81853874..c2535871 100644 --- a/internal/x402/paymentrequired.go +++ b/internal/x402/paymentrequired.go @@ -384,9 +384,11 @@ func inferenceCopy(url string, d PaymentDisplay) typeCopy { other := fmt.Sprintf( "Read https://obol.org/llms.txt to learn how Obol's x402 micropayments work. "+ "I want to use the remote LLM at %s (model %s) as a paid OpenAI-compatible "+ - "chat-completions endpoint. Pre-sign a budget of EIP-3009/Permit2 authorizations "+ - "and POST chat-completions bodies with the X-PAYMENT header attached.", - url, model, + "chat-completions endpoint. Pre-sign a budget of EIP-3009/Permit2 authorizations, "+ + "then POST to `/v1/chat/completions` with a body like "+ + `{"model":"%s","messages":[{"role":"user","content":""}]} `+ + "and the X-PAYMENT header attached.", + url, model, model, ) return typeCopy{ @@ -403,6 +405,17 @@ func inferenceCopy(url 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), } } @@ -435,15 +448,17 @@ X-PAYMENT: 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.", + "Send an OpenAI-style chat-completions body with a clear user message in `messages` "+ + "(see the example under Pay manually below). Do not pay without a real instruction.", url, modelLine, ) other := fmt.Sprintf( "Read https://obol.org/llms.txt to learn how Obol's x402 micropayments work. "+ "Help me call the Obol Agent at %s%s — it's an autonomous agent (tools + skills + memory), "+ - "not a raw LLM. POST OpenAI-style chat-completions JSON with a real prompt in `messages`, "+ - "attach a signed EIP-3009/Permit2 authorization as `X-PAYMENT`, and report what the agent does.", + "not a raw LLM. POST OpenAI-style chat-completions JSON: `messages` must include a user turn "+ + "with your task (e.g. {\"role\":\"user\",\"content\":\"Summarise …\"}), plus a signed "+ + "EIP-3009/Permit2 voucher as `X-PAYMENT`. Report what the agent returns.", url, modelLine, ) @@ -477,7 +492,10 @@ func httpCopy(url 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 4105c629..17d3e201 100644 --- a/internal/x402/paymentrequired_test.go +++ b/internal/x402/paymentrequired_test.go @@ -208,6 +208,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 @@ -282,6 +284,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 f2dbf032..7046d689 100644 --- a/web/public-storefront/src/components/ServiceCard.tsx +++ b/web/public-storefront/src/components/ServiceCard.tsx @@ -179,7 +179,7 @@ function BuyViaObolAgent({ service }: { service: Service }) { } // http (default): legacy single-shot pay. - const prompt = `Use the buy-x402 skill's \`pay\` command to call ${service.endpoint} once. Pay ${service.price} on ${service.network}. Report what it returns.`; + const prompt = `Use the buy-x402 skill's \`pay\` command to call ${service.endpoint} once. Pay ${service.price} on ${service.network}. Use the method and payload the seller documents. Report what it returns.`; return (