Skip to content
Open
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
16 changes: 16 additions & 0 deletions .changeset/ucp-commerce-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"eve": patch
---

Add `eve/commerce/ucp` for building agents that buy over the Universal Commerce
Protocol. `defineUcpConnection` connects to a merchant's UCP shopping service
and takes the protocol headers off the model — agent profile identity,
retry-safe `Idempotency-Key`/`Request-Id` derived from the replay-stable call
id, and RFC 9421 request signing. `resolveUcpCheckoutHandoff` collapses a
checkout response into one typed outcome: conversational continuation,
`continue_url` redirect, or embedded checkout.

OpenAPI connections also gain a `prepareRequest` hook, which receives the
fully-built request — including the serialized body — and merges the headers it
returns. Use it for signatures and content digests that `headers` cannot
express.
20 changes: 20 additions & 0 deletions apps/templates/commerce-agent/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The merchant's UCP shopping endpoint, from
# services["dev.ucp.shopping"][transport="rest"].endpoint in their
# https://<merchant>/.well-known/ucp profile.
UCP_MERCHANT_ENDPOINT="https://merchant.example.com/ucp/v1"

# Credential the merchant issued you. UCP allows an API key or OAuth token
# instead of message signatures.
UCP_MERCHANT_TOKEN=""

# Public https origin serving this app's /.well-known/ucp. Merchants fetch it
# to resolve who is calling, so it must be reachable from their network.
# Unset on Vercel, where VERCEL_PROJECT_PRODUCTION_URL is used instead.
UCP_AGENT_ORIGIN=""

# Optional: sign every request with HTTP Message Signatures (RFC 9421).
# UCP_SIGNING_KEY_ID must match the `kid` published in /.well-known/ucp.
# Generate a P-256 key with:
# node --input-type=module -e 'const k=await crypto.subtle.generateKey({name:"ECDSA",namedCurve:"P-256"},true,["sign"]);console.log(JSON.stringify(await crypto.subtle.exportKey("jwk",k.privateKey)))'
UCP_SIGNING_KEY_ID=""
UCP_SIGNING_KEY_JWK=""
48 changes: 48 additions & 0 deletions apps/templates/commerce-agent/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*
!.env.example

# eve
.eve
.swc
.output

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
.env*.local
9 changes: 9 additions & 0 deletions apps/templates/commerce-agent/.vercelignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.output
.swc
.eve
.turbo
.agents
.github
dist
research
node_modules
84 changes: 84 additions & 0 deletions apps/templates/commerce-agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Commerce agent template

An eve agent that shops one merchant over the
[Universal Commerce Protocol](https://ucp.dev/), with a Next.js front end that
renders the checkout handoff.

The template is the buying side of UCP. It does not implement a store: it
talks to a merchant that already publishes a UCP profile.

## What is in here

```
agent/
agent.ts the agent
instructions.md how it works a checkout and when it stops
connections/merchant.ts defineUcpConnection against the merchant endpoint
channels/eve.ts chat transport for the browser client
channels/ucp.ts this agent's own /.well-known/ucp profile
lib/ucp.ts identity, signing key, and a small read client
app/
_commerce/Chat.tsx minimal chat, tracks the active checkout id
_commerce/CheckoutHandoff.tsx renders every branch of the handoff union
api/checkout/[id]/route.ts re-reads the session and resolves the handoff
```

## Setup

```sh
cp .env.example .env.local # fill in the merchant endpoint and token
pnpm install
pnpm dev:eve # eve dev
pnpm dev # next dev, in a second terminal
```

`agent/connections/merchant.ts` throws at build time if
`UCP_MERCHANT_ENDPOINT` or `UCP_AGENT_ORIGIN` is missing, rather than sending
requests a merchant will reject.

### Local development and the agent profile

A merchant resolves who is calling by fetching the URL in your `UCP-Agent`
header, so that URL has to be reachable from the merchant's network.
`http://localhost:3000/.well-known/ucp` is not. Either point
`UCP_AGENT_ORIGIN` at a tunnel (`ngrok http 3000`), or use the example agent
profile the merchant publishes for testing, if they have one.

Check what you are serving:

```sh
curl -i "$UCP_AGENT_ORIGIN/.well-known/ucp"
```

### Signing

Signing is optional: UCP accepts an API key or OAuth token instead. Set
`UCP_SIGNING_KEY_ID` and `UCP_SIGNING_KEY_JWK` and every request is signed per
RFC 9421, with the public half published in `/.well-known/ucp` so the merchant
can verify it. Merchants that require signatures answer unsigned requests with
`signature_missing`.

## How the handoff works

The agent drives the checkout through connection tools. Each response says
what has to happen next, and `resolveUcpCheckoutHandoff` turns that into one
of three outcomes the UI can render:

- **conversational** — the agent keeps working: collect what is missing and
call Update Checkout, or the session is ready and waiting on the buyer.
- **embedded** — the merchant enabled Embedded Checkout for this session, so
their checkout loads in an iframe with the negotiated `ec_*` parameters.
- **continue_url** — the buyer finishes on the merchant's own site.

Plus the terminal `completed`, `canceled`, and `failed`.

The agent never places the order on its own. UCP requires the buyer to review
and authorize a checkout in a trusted UI, and the instructions in
`agent/instructions.md` hold the agent to that.

## Read next

- [UCP commerce agents](../../../docs/protocols/ucp-agent.mdx) — the preset and
the handoff contract in detail.
- [Universal Commerce Protocol (UCP)](../../../docs/protocols/ucp.mdx) —
publishing your own profile as a business.
5 changes: 5 additions & 0 deletions apps/templates/commerce-agent/agent/agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { defineAgent } from "eve";

export default defineAgent({
model: "anthropic/claude-opus-4.6",
});
13 changes: 13 additions & 0 deletions apps/templates/commerce-agent/agent/channels/eve.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { localDev, vercelOidc } from "eve/channels/auth";
import { eveChannel } from "eve/channels/eve";

/**
* The chat transport the browser client talks to.
*
* `localDev` opens it up on `eve dev`; `vercelOidc` covers deployed
* preview and production. Add your own `AuthFn` ahead of these before
* exposing a real storefront to real buyers.
*/
export default eveChannel({
auth: [vercelOidc(), localDev()],
});
27 changes: 27 additions & 0 deletions apps/templates/commerce-agent/agent/channels/ucp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { defineChannel, GET } from "eve/channels";
import { agentProfile } from "@/lib/ucp";

/**
* Serves this agent's UCP profile.
*
* UCP has no registration step: a merchant learns who is calling by
* fetching the URL in the `UCP-Agent` header, and finds the public key to
* verify signatures in this document's `signing_keys`. If this endpoint is
* unreachable, merchants answer with `profile_unreachable` (HTTP 424).
*
* The spec requires HTTPS, forbids 3xx responses, and requires a
* `Cache-Control` of `public` with `max-age` of at least 60 seconds.
*/
export default defineChannel({
cors: true,
routes: [
GET("/.well-known/ucp", async () => {
return new Response(JSON.stringify(agentProfile()), {
headers: {
"cache-control": "public, max-age=300",
"content-type": "application/json",
},
});
}),
],
});
29 changes: 29 additions & 0 deletions apps/templates/commerce-agent/agent/connections/merchant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineUcpConnection, type UcpConnectionDefinition } from "eve/commerce/ucp";
import { agentMetadata, merchantEndpoint, signingKey } from "@/lib/ucp";

const definition: UcpConnectionDefinition = {
agent: agentMetadata(),
auth: {
getToken: async () => ({ token: process.env.UCP_MERCHANT_TOKEN ?? "" }),
},
description:
"The merchant's UCP shopping service: search the catalog and drive a checkout session.",
endpoint: merchantEndpoint(),
// Checkout and catalog are what this template drives. Widen the list
// once the merchant's profile advertises other capabilities you want.
operations: {
allow: [
"cancel_checkout",
"complete_checkout",
"create_checkout",
"get_checkout",
"lookup_catalog",
"search_catalog",
"update_checkout",
],
},
};

const signing = signingKey();

export default defineUcpConnection(signing === undefined ? definition : { ...definition, signing });
41 changes: 41 additions & 0 deletions apps/templates/commerce-agent/agent/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
You are a shopping assistant for one merchant. You help a buyer find items
and get a checkout session ready, and you hand the buyer the controls before
an order is placed.

## Working the checkout

Every `merchant__*_checkout` response carries a `status` and a `messages`
array. Read both before deciding what to do next.

- `incomplete`: something is missing or contested. Read `messages`. For each
error with `severity: "recoverable"`, gather what is needed — ask the buyer
in the conversation if you do not already have it — and call
`merchant__update_checkout` with the full checkout resource. Update
replaces the resource, so send every field you want to keep.
- `ready_for_complete`: everything is collected. Do not place the order
yourself. Tell the buyer the checkout is ready and let them review it.
- `complete_in_progress`: the merchant is placing the order. Call
`merchant__get_checkout` to follow it rather than retrying the completion.
- `requires_escalation`: the buyer has to take over on the merchant's own
surface. Say what is needed, in the buyer's words, and stop calling
checkout operations. The app renders the handoff.
- `completed` / `canceled`: terminal. Report the order or offer to start over.

Any message with `severity: "requires_buyer_input"` or
`"requires_buyer_review"` means the buyer must act, whatever the status says.
Summarize it and stop.

## What you never do

- Never invent buyer details: addresses, emails, and payment instruments come
from the buyer or from what the merchant already has.
- Never call `merchant__complete_checkout` on your own initiative. The buyer
reviews and authorizes the order; you prepare it.
- Never state a total, tax, shipping cost, or availability that did not come
from a merchant response.

## Talking to the buyer

Be brief and concrete. Name items, quantities, and amounts exactly as the
merchant reported them. When you are blocked, say what you need in one
sentence and ask for that one thing.
Loading
Loading