UCP Checker

Build a UCP shopping agent

A UCP shopping agent reads a store's profile, connects to the endpoint it names and calls the tools in order, from a search to a paid order. Here is one, call by call: about 70 lines of Python or TypeScript, run end to end against a real store.

What you are building

Your agent
A model, if you want oneChooses the next call from the tools and the last response.
Profile readerFinds the endpoint and what the store declares.
MCP clientJSON-RPC over HTTP: tools/list, tools/call.
Your platform profileA public URL. The store fetches it.
  1. GET /.well-known/ucp
  2. tools/call, with meta["ucp-agent"]
  3. fetches your profile
The store
/.well-known/ucpRelease, services, capabilities, payment handlers.
MCP endpointsearch_catalog, create_cart, create_checkout and the rest.
NegotiationAnswers only in what you and the store both declare.

Everything happens between your agent and the store. UCP Checker can tell your agent which stores to try; it never sits in the transaction.

Seven calls to an order

Each call with the code that makes it, what was sent and what came back. The code is one script: joined in order, it ran against the store below and placed a test order.

Recorded on 16 October 2026 against Northlight Flowers, our test store at flowers.ucpsandbox.com (release 2026-08-25, test cards, nothing is for sale). Responses are trimmed to the fields that matter, nothing added; long ids are cut short with …, and the agent's own profile is shown as agent.example.

Once, at the top: two helpers. rpc sends one JSON-RPC request; call wraps a UCP tool and puts meta on it.
Python
import uuid
import httpx

STORE = "https://flowers.ucpsandbox.com"
PROFILE = "https://agent.example/.well-known/ucp"  # your platform profile: public, the store fetches it
ME = {"ucp-agent": {"profile": PROFILE}}

def rpc(endpoint, method, params=None):
    res = httpx.post(endpoint, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}},
                     headers={"Accept": "application/json, text/event-stream"}, timeout=30)
    body = res.json()
    if "error" in body:
        raise RuntimeError(body["error"])
    return body["result"]

def call(tool, args, key=None):
    meta = {**ME, "idempotency-key": key} if key else ME
    return rpc(endpoint, "tools/call", {"name": tool, "arguments": {"meta": meta, **args}})["structuredContent"]
1

Read the profile

The agent asks the store's fixed address what it offers, before it sends anything else.

Python
profile = httpx.get(f"{STORE}/.well-known/ucp").json()["ucp"]
endpoint = next(s["endpoint"] for s in profile["services"]["dev.ucp.shopping"]
                if s["transport"] == "mcp")
assert "dev.ucp.shopping.catalog.search" in profile["capabilities"]
GET /.well-known/ucp HTTP/1.1
Host: flowers.ucpsandbox.com
Accept: application/json
200, JSON: release, services, capabilities

{
  "ucp": {
    "version": "2026-08-25",
    "services": {
      "dev.ucp.shopping": [
        {
          "transport": "rest",
          "endpoint": "https://flowers.ucpsandbox.com/ucp"
        },
        {
          "transport": "mcp",
          "endpoint": "https://flowers.ucpsandbox.com/mcp"
        }
      ]
    },
    "capabilities": {
      "dev.ucp.shopping.catalog.search": "…",
      "dev.ucp.shopping.catalog.lookup": "…",
      "dev.ucp.shopping.cart": "…",
      "dev.ucp.shopping.checkout": "…",
      "dev.ucp.shopping.fulfillment": "…",
      "dev.ucp.shopping.order": "…"
    },
    "payment_handlers": {
      "dev.mock.payment_handler": "…"
    }
  }
}

What the agent knows now. Release 2026-08-25, an MCP endpoint, catalogue search and checkout declared, one payment handler. Enough to go on.

2

List the tools

An MCP client asks the endpoint from the profile which tools it serves. Their names are fixed by the UCP binding.

Python
rpc(endpoint, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
                             "clientInfo": {"name": "my-agent", "version": "0.1"}})
tools = {t["name"] for t in rpc(endpoint, "tools/list")["tools"]}
assert {"search_catalog", "create_checkout", "complete_checkout"} <= tools
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/list",
  "params": {}
}
15 tools

{
  "tools": [
    "search_catalog",
    "lookup_catalog",
    "get_product",
    "create_cart",
    "get_cart",
    "update_cart",
    "cancel_cart",
    "create_checkout",
    "get_checkout",
    "update_checkout",
    "complete_checkout",
    "cancel_checkout",
    "get_order",
    "search_locations",
    "lookup_locations"
  ]
}

What the agent knows now. 15 tools, each with an inputSchema. The ones it needs for this run are all there.

4

Put it in a cart

dev.ucp.shopping.cart

A line item is an id from the catalogue and a quantity. The store prices it and returns the cart.

Python
lines = [{"item": {"id": variant["id"]}, "quantity": 1}]
cart = call("create_cart", {"cart": {"line_items": lines}})
print(cart["totals"][-1], cart["continue_url"])
{
  "meta": {
    "ucp-agent": {
      "profile": "https://agent.example/.well-known/ucp"
    }
  },
  "cart": {
    "line_items": [
      {
        "item": {
          "id": "bouquet_roses"
        },
        "quantity": 1
      }
    ]
  }
}
a cart, total 3500 USD

{
  "id": "01a14534-5383-…",
  "currency": "USD",
  "line_items": [
    {
      "id": "li_3",
      "item": {
        "id": "bouquet_roses",
        "title": "Bouquet of Red Roses",
        "price": 3500
      },
      "quantity": 1
    }
  ],
  "totals": [
    {
      "type": "subtotal",
      "amount": 3500
    },
    {
      "type": "total",
      "amount": 3500
    }
  ],
  "continue_url": "https://flowers.ucpsandbox.com/cart",
  "expires_at": "2026-10-17T14:53:39+00:00"
}

What the agent knows now. A real cart that lasts a day, and a continue_url to hand the buyer to if the agent stops here.

5

Open a checkout

dev.ucp.shopping.checkout

The same line items, the buyer and a delivery address. The store says what is still missing in messages[].

Python
address = {"id": "dest_1", "first_name": "Sam", "last_name": "Lee", "street_address": "1 Main St",
           "address_locality": "Chicago", "address_region": "IL", "postal_code": "60601",
           "address_country": "US"}
checkout = {
    "line_items": lines,
    "buyer": {"first_name": "Sam", "last_name": "Lee", "email": "[email protected]"},
    "fulfillment": {"methods": [{"type": "shipping", "destinations": [address],
                                 "selected_destination_id": "dest_1"}]},
}
co = call("create_checkout", {"checkout": checkout})
for m in co.get("messages") or []:
    print(m["severity"], m["path"], m["content"])
{
  "meta": {
    "ucp-agent": {
      "profile": "https://agent.example/.well-known/ucp"
    }
  },
  "checkout": {
    "line_items": [
      {
        "item": {
          "id": "bouquet_roses"
        },
        "quantity": 1
      }
    ],
    "buyer": {
      "first_name": "Sam",
      "last_name": "Lee",
      "email": "[email protected]"
    },
    "fulfillment": {
      "methods": [
        {
          "type": "shipping",
          "destinations": [
            {
              "id": "dest_1",
              "first_name": "Sam",
              "last_name": "Lee",
              "street_address": "1 Main St",
              "address_locality": "Chicago",
              "address_region": "IL",
              "postal_code": "60601",
              "address_country": "US"
            }
          ],
          "selected_destination_id": "dest_1"
        }
      ]
    }
  }
}
status incomplete: choose a delivery option

{
  "id": "01a14534-5475-…",
  "status": "incomplete",
  "fulfillment": {
    "methods": [
      {
        "id": "method_1",
        "type": "shipping",
        "groups": [
          {
            "id": "group_1",
            "options": [
              {
                "id": "free-ship",
                "title": "Free Shipping",
                "totals": [
                  {
                    "type": "total",
                    "amount": 0
                  }
                ]
              },
              {
                "id": "std-ship",
                "title": "Standard Shipping",
                "totals": [
                  {
                    "type": "total",
                    "amount": 500
                  }
                ]
              },
              {
                "id": "exp-ship-us",
                "title": "Express Shipping (US)",
                "totals": [
                  {
                    "type": "total",
                    "amount": 1500
                  }
                ]
              }
            ],
            "selected_option_id": null
          }
        ]
      }
    ]
  },
  "messages": [
    {
      "type": "error",
      "code": "missing",
      "path": "$.fulfillment.methods[0].groups[0].selected_option_id",
      "content": "Choose a delivery option.",
      "severity": "recoverable"
    }
  ]
}

What the agent knows now. Status incomplete, and the store says exactly why: no delivery option yet. Three to choose from.

6

Choose delivery

dev.ucp.shopping.fulfillment

An update replaces the checkout, so the agent sends all of it again with the option chosen. Sending only the change was refused: "line_items is required on update."

Python
method = co["fulfillment"]["methods"][0]
group = method["groups"][0]
checkout["fulfillment"]["methods"][0] |= {
    "id": method["id"],
    "groups": [{"id": group["id"], "selected_option_id": group["options"][0]["id"]}],
}
# An update replaces the checkout: send all of it again.
co = call("update_checkout", {"id": co["id"], "checkout": checkout})
assert co["status"] == "ready_for_complete", co.get("messages")
{
  "meta": {
    "ucp-agent": {
      "profile": "https://agent.example/.well-known/ucp"
    }
  },
  "id": "01a14534-5475-…",
  "checkout": {
    "line_items": [
      {
        "item": {
          "id": "bouquet_roses"
        },
        "quantity": 1
      }
    ],
    "buyer": {
      "first_name": "Sam",
      "last_name": "Lee",
      "email": "[email protected]"
    },
    "fulfillment": {
      "methods": [
        {
          "id": "method_1",
          "type": "shipping",
          "destinations": [
            {
              "id": "dest_1",
              "first_name": "Sam",
              "last_name": "Lee",
              "street_address": "1 Main St",
              "address_locality": "Chicago",
              "address_region": "IL",
              "postal_code": "60601",
              "address_country": "US"
            }
          ],
          "selected_destination_id": "dest_1",
          "groups": [
            {
              "id": "group_1",
              "selected_option_id": "free-ship"
            }
          ]
        }
      ]
    }
  }
}
status ready_for_complete

{
  "status": "ready_for_complete",
  "totals": [
    {
      "type": "subtotal",
      "amount": 3500
    },
    {
      "type": "fulfillment",
      "amount": 0,
      "display_text": "Shipping"
    },
    {
      "type": "total",
      "amount": 3500
    }
  ]
}

What the agent knows now. ready_for_complete: nothing missing. $35.00 with free shipping.

7

Pay and place the order

dev.ucp.shopping.checkout

Only the payment, through a handler the store declared, and a fresh idempotency key so a retry cannot charge twice. This is the one call that places an order.

Python
handler = next(iter(co["ucp"]["payment_handlers"].values()))[0]["id"]
payment = {"instruments": [{"id": "instr_1", "handler_id": handler, "type": "card"}]}
done = call("complete_checkout", {"id": co["id"], "checkout": {"payment": payment}},
            key=str(uuid.uuid4()))
print(done["status"], done["order"]["label"], done["order"]["permalink_url"])
{
  "meta": {
    "ucp-agent": {
      "profile": "https://agent.example/.well-known/ucp"
    },
    "idempotency-key": "6f1c…"
  },
  "id": "01a14534-5475-…",
  "checkout": {
    "payment": {
      "instruments": [
        {
          "id": "instr_1",
          "handler_id": "mock_payment_handler",
          "type": "card"
        }
      ]
    }
  }
}
status completed, order NF-GKFVSO

{
  "status": "completed",
  "order": {
    "id": "01a14534-fc3c-…",
    "label": "NF-GKFVSO",
    "permalink_url": "https://flowers.ucpsandbox.com/orders/01a14534-fc3c-…"
  }
}

What the agent knows now. An order, with a page the buyer can open. Seven calls from a domain name to a paid order.

The whole script

Replace PROFILE with your platform profile's URL before you run it. Against a real store, stop before complete_checkout.

agent.py
import uuid
import httpx

STORE = "https://flowers.ucpsandbox.com"
PROFILE = "https://agent.example/.well-known/ucp"  # your platform profile: public, the store fetches it
ME = {"ucp-agent": {"profile": PROFILE}}

def rpc(endpoint, method, params=None):
    res = httpx.post(endpoint, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params or {}},
                     headers={"Accept": "application/json, text/event-stream"}, timeout=30)
    body = res.json()
    if "error" in body:
        raise RuntimeError(body["error"])
    return body["result"]

def call(tool, args, key=None):
    meta = {**ME, "idempotency-key": key} if key else ME
    return rpc(endpoint, "tools/call", {"name": tool, "arguments": {"meta": meta, **args}})["structuredContent"]

profile = httpx.get(f"{STORE}/.well-known/ucp").json()["ucp"]
endpoint = next(s["endpoint"] for s in profile["services"]["dev.ucp.shopping"]
                if s["transport"] == "mcp")
assert "dev.ucp.shopping.catalog.search" in profile["capabilities"]

rpc(endpoint, "initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
                             "clientInfo": {"name": "my-agent", "version": "0.1"}})
tools = {t["name"] for t in rpc(endpoint, "tools/list")["tools"]}
assert {"search_catalog", "create_checkout", "complete_checkout"} <= tools

found = call("search_catalog", {"catalog": {
    "query": "roses",
    "context": {"address_country": "US", "currency": "USD"},
    "pagination": {"limit": 3},
}})
product = found["products"][0]
variant = product["variants"][0]
print(product["title"], variant["price"]["amount"] / 100, variant["price"]["currency"])

lines = [{"item": {"id": variant["id"]}, "quantity": 1}]
cart = call("create_cart", {"cart": {"line_items": lines}})
print(cart["totals"][-1], cart["continue_url"])

address = {"id": "dest_1", "first_name": "Sam", "last_name": "Lee", "street_address": "1 Main St",
           "address_locality": "Chicago", "address_region": "IL", "postal_code": "60601",
           "address_country": "US"}
checkout = {
    "line_items": lines,
    "buyer": {"first_name": "Sam", "last_name": "Lee", "email": "[email protected]"},
    "fulfillment": {"methods": [{"type": "shipping", "destinations": [address],
                                 "selected_destination_id": "dest_1"}]},
}
co = call("create_checkout", {"checkout": checkout})
for m in co.get("messages") or []:
    print(m["severity"], m["path"], m["content"])

method = co["fulfillment"]["methods"][0]
group = method["groups"][0]
checkout["fulfillment"]["methods"][0] |= {
    "id": method["id"],
    "groups": [{"id": group["id"], "selected_option_id": group["options"][0]["id"]}],
}
# An update replaces the checkout: send all of it again.
co = call("update_checkout", {"id": co["id"], "checkout": checkout})
assert co["status"] == "ready_for_complete", co.get("messages")

handler = next(iter(co["ucp"]["payment_handlers"].values()))[0]["id"]
payment = {"instruments": [{"id": "instr_1", "handler_id": handler, "type": "card"}]}
done = call("complete_checkout", {"id": co["id"], "checkout": {"payment": payment}},
            key=str(uuid.uuid4()))
print(done["status"], done["order"]["label"], done["order"]["permalink_url"])

What going wrong looks like

Failures arrive in three different places, and an agent handles each one differently. These three came from the same store on the same day.

  1. Protocol errorsearch_catalog with no meta["ucp-agent"]
    {
      "error": {
        "code": -32001,
        "message": "invalid_profile_url",
        "data": {
          "content": "meta[\"ucp-agent\"].profile is required."
        }
      }
    }

    The store could not tell who is calling, so it refused before searching. Send your platform profile on every call.

  2. Nothing foundsearch_catalog for "zzqx unicorn"
    {
      "products": [],
      "pagination": {
        "has_next_page": false,
        "total_count": 0
      }
    }

    Not an error: a normal result with no products. Broaden the query instead of retrying it.

  3. Business outcomecomplete_checkout with the declining test card
    {
      "status": "ready_for_complete",
      "messages": [
        {
          "type": "error",
          "code": "payment_failed",
          "content": "Payment was declined by the issuer.",
          "path": "$.payment.instruments[0]",
          "severity": "recoverable"
        }
      ]
    }

    The call worked and the payment did not. The checkout stays open; recoverable means try another instrument.

Other answers to expect

what they mean
HTTP
HTTP 404 on /.well-known/ucp
The domain has no public UCP profile. Check it, or pick another store. Check a store →
Protocol
JSON-RPC -32001 version_unsupported
Your profile names a release the store doesn't serve. Compare with the store's ucp.version. Releases →
Protocol
JSON-RPC -32000 with HTTP 401 or 403
The call needs authentication. Catalogue reads are often open; carts, checkout and orders may not be. Buyer accounts use identity linking. Capabilities →
Protocol
JSON-RPC -32000 with HTTP 429
Rate limited. Honour Retry-After and back off; never retry in a tight loop.
Protocol
JSON-RPC -32602
Invalid params. Check your arguments against the inputSchema from tools/list: the operation goes under its own key (catalog, cart, checkout) beside meta. MCP tools →
Store
An empty tools list
The profile declares MCP but the endpoint serves no tools. Skip the store.

Stores to test against

Start on a test store: test cards, nothing is for sale, and complete_checkout is safe to call. Flowers and food take the script above with STORE and the search term changed.

Then a real store

verified, with the most capabilities, at most two per platform

Open a store's status page to see what it declares before you call it. These are real stores: don't call complete_checkout.

Next

Questions

What language should I build it in?
Any language that can send JSON over HTTP. MCP is JSON-RPC, and the script above is plain HTTP calls in Python or TypeScript with no SDK. An MCP SDK adds session handling and streaming if you need them.
Do I need an API key?
Not for the calls on this page. Every call carries meta["ucp-agent"] with your platform profile URL, which the store fetches. Catalogue reads are usually open; carts, checkout and orders may need authentication, and buyer accounts use identity linking.
What is a platform profile, and why did my first call hang?
It is your own UCP profile, the same shape as a store's /.well-known/ucp, at a public https URL. The store fetches it on each call to agree a release and capabilities. If the URL doesn't answer, the store waits on it, and so does your call.
How do I test without buying anything?
Use the test stores above: test cards, nothing is for sale. On a real store, stop before complete_checkout, the one call that places an order. UCP Playground runs AI agents against stores and shows every call.
Where does a model come in?
Wherever a choice is made: which product, which variant, which delivery option. The script makes those choices in code (the first result, the first option); an agent hands the tools and the responses to a model with tool calling and lets it choose. The calls are the same either way.
How do I find stores to shop?
Connect https://ucpchecker.com/mcp and call list-stores or check-domain, or browse the directory. Then call each store on its own endpoint; UCP Checker never sits in the transaction.

Explore protocol data