README.md

# Bazaar 🎪

**Open your store to AI agents.** Elixir SDK for [UCP](https://ucp.dev) and [ACP](https://github.com/agentic-commerce-protocol/acp-spec).

Bazaar helps you build commerce APIs in Elixir/Phoenix that work with both Google Shopping agents (UCP) and OpenAI/Stripe agents (ACP) from a single handler.

> [!TIP]
> See [bazaar-merchant](https://github.com/georgeguimaraes/bazaar-merchant) for a complete Phoenix app demonstrating a dual-protocol merchant.

## Supported Protocols

| Protocol | Used By | Spec |
|----------|---------|------|
| **UCP** (Universal Commerce Protocol) | Google Shopping agents | [ucp.dev](https://ucp.dev) |
| **ACP** (Agentic Commerce Protocol) | OpenAI Operator, Stripe | [GitHub](https://github.com/agentic-commerce-protocol/acp-spec) |

Both protocols enable AI agents to:

- Discover what your store offers
- Create and manage shopping carts
- Complete checkouts
- Track orders

UCP was announced by Google at NRF 2026, co-developed with Shopify, Walmart, Etsy, and Target. ACP is backed by OpenAI and Stripe.

## Features

- **Generated Schemas**: Smelter-generated Ecto schemas from official UCP JSON Schemas
- **Phoenix Router Macro**: Mount all UCP routes with a single line
- **Handler Behaviour**: Clean interface for your commerce logic
- **Built-in Plugs**: Request validation, idempotency, and UCP headers
- **Auto-generated Discovery**: `/.well-known/ucp` endpoint from your handler
- **Business Logic Helpers**: Currency conversion, message factories, order creation

## Architecture

Bazaar separates concerns cleanly:

```
lib/bazaar/
├── schemas/           # Generated from UCP JSON Schemas (via Smelter)
│   ├── shopping/      # Checkout, Order, Payment types
│   ├── capability/    # Capability definitions
│   └── ucp/           # Discovery profile, response types
├── checkout.ex        # Business logic: currency helpers
├── order.ex           # Business logic: from_checkout helper
├── message.ex         # Business logic: error/warning/info factories
├── fulfillment.ex     # Business logic: field definitions
├── handler.ex         # Handler behaviour
├── phoenix/           # Router and controller
└── plugs/             # Request validation, headers, idempotency
```

**Schemas** are generated from JSON and provide validation via `new/1` and `fields/0`.
**Business logic** modules add helpers and factories on top of the schemas.

## How It Works

Bazaar is a thin layer that connects your Phoenix app to AI shopping agents via UCP.

```
AI Agent → UCP Request → Bazaar Router → Bazaar Controller → Your Handler → Response
```

Bazaar handles the HTTP/JSON plumbing. You write the commerce logic.

| Bazaar | You |
|--------|-----|
| Parses JSON, validates structure | Write business logic |
| Routes to correct callback | Query your database |
| Handles UCP headers | Calculate prices, tax, shipping |
| Returns proper HTTP responses | Integrate with payment/fulfillment |

Bazaar doesn't touch your database or know about your products. It just speaks UCP so AI agents can shop at your store.

## Installation

Add `bazaar` to your dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:bazaar, "~> 0.1.0"}
  ]
end
```

Then run:

```bash
mix deps.get
```

## Quick Start

### Step 1: Create a Handler

The handler defines your store's capabilities and commerce logic:

```elixir
defmodule MyApp.UCPHandler do
  use Bazaar.Handler

  @impl true
  def capabilities, do: [:checkout, :orders]

  @impl true
  def business_profile do
    %{
      "name" => "My Awesome Store",
      "description" => "We sell amazing products"
    }
  end

  @impl true
  def create_checkout(params, _conn) do
    # params already validated by Bazaar
    # Save to your DB, return the checkout map
    {:ok, %{"id" => "chk_123", "status" => "incomplete", ...}}
  end

  @impl true
  def get_checkout(id, _conn) do
    # Fetch from your DB
    {:ok, checkout} or {:error, :not_found}
  end

  # ... other callbacks: update_checkout, cancel_checkout, get_order, cancel_order
end
```

### Step 2: Mount Routes

Add UCP routes to your Phoenix router:

```elixir
defmodule MyAppWeb.Router do
  use Phoenix.Router
  use Bazaar.Phoenix.Router

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/" do
    pipe_through :api
    bazaar_routes "/", MyApp.UCPHandler
  end
end
```

This creates these endpoints:

| Method | Path | Description |
|--------|------|-------------|
| GET | `/.well-known/ucp` | Discovery endpoint |
| POST | `/checkout-sessions` | Create checkout |
| GET | `/checkout-sessions/:id` | Get checkout |
| PATCH | `/checkout-sessions/:id` | Update checkout |
| DELETE | `/checkout-sessions/:id` | Cancel checkout |
| GET | `/orders/:id` | Get order |
| POST | `/orders/:id/actions/cancel` | Cancel order |
| POST | `/webhooks/ucp` | Receive webhooks |

### Step 3: Test It

Start your server and test the discovery endpoint:

```bash
curl http://localhost:4000/.well-known/ucp
```

You should see your store's profile and capabilities.

## Guides

New to Bazaar? Check out these guides:

- **[Getting Started](guides/getting-started.md)** - Build your first UCP merchant
- **[Protocols](guides/protocols.md)** - Support both UCP and ACP
- **[Handlers](guides/handlers.md)** - Implement commerce logic
- **[Schemas](guides/schemas.md)** - Validate checkout and order data
- **[Plugs](guides/plugs.md)** - Add validation, idempotency, and headers
- **[Testing](guides/testing.md)** - Test your implementation

## Core Concepts

### Capabilities

UCP defines three capabilities your store can support:

| Capability | Description | Callbacks |
|------------|-------------|-----------|
| `:checkout` | Shopping cart management | `create_checkout`, `get_checkout`, `update_checkout`, `cancel_checkout` |
| `:orders` | Order tracking | `get_order`, `cancel_order` |
| `:identity` | User identity linking | `link_identity` |

### Schemas

Bazaar schemas are generated from official UCP JSON Schemas using [Smelter](https://github.com/georgeguimaraes/smelter):

```elixir
# Validate checkout response
changeset = Bazaar.Schemas.Shopping.CheckoutResp.new(params)

# Create order params from checkout
order_params = Bazaar.Order.from_checkout(checkout, "order_123", "https://shop.com/orders/123")

# Currency helpers
cents = Bazaar.Checkout.to_minor_units(19.99)  # => 1999
dollars = Bazaar.Checkout.to_major_units(1999)  # => 19.99

# Message factories
error = Bazaar.Message.error(%{"code" => "out_of_stock", "content" => "Item unavailable"})
```

### Regenerating Schemas

If UCP schemas are updated, regenerate with:

```bash
mix bazaar.gen.schemas priv/ucp_schemas/2026-01-11
```

### Plugs

Optional plugs for production use:

```elixir
pipeline :ucp do
  plug Bazaar.Plugs.UCPHeaders      # Extract UCP headers
  plug Bazaar.Plugs.ValidateRequest  # Validate request body
  plug Bazaar.Plugs.Idempotency      # Handle retry safety
end
```

## Example: Complete Handler

Here's a more complete handler example:

```elixir
defmodule MyApp.Commerce.Handler do
  use Bazaar.Handler

  alias MyApp.{Repo, Checkout, Order}

  @impl true
  def capabilities, do: [:checkout, :orders]

  @impl true
  def business_profile do
    %{
      "name" => "Cool Gadgets Store",
      "description" => "The best gadgets on the internet",
      "support_email" => "help@coolgadgets.example"
    }
  end

  # Checkout callbacks

  @impl true
  def create_checkout(params, _conn) do
    case Bazaar.Schemas.Shopping.CheckoutResp.new(params) do
      %{valid?: true} = changeset ->
        data = Ecto.Changeset.apply_changes(changeset)
        checkout = Repo.insert!(Checkout.from_ucp(data))
        {:ok, Checkout.to_ucp(checkout)}

      %{valid?: false} = changeset ->
        {:error, changeset}
    end
  end

  @impl true
  def get_checkout(id, _conn) do
    case Repo.get(Checkout, id) do
      nil -> {:error, :not_found}
      checkout -> {:ok, Checkout.to_ucp(checkout)}
    end
  end

  @impl true
  def update_checkout(id, params, _conn) do
    case Repo.get(Checkout, id) do
      nil ->
        {:error, :not_found}

      checkout ->
        checkout
        |> Checkout.changeset(params)
        |> Repo.update()
        |> case do
          {:ok, updated} -> {:ok, Checkout.to_ucp(updated)}
          {:error, changeset} -> {:error, changeset}
        end
    end
  end

  @impl true
  def cancel_checkout(id, _conn) do
    case Repo.get(Checkout, id) do
      nil -> {:error, :not_found}
      checkout ->
        {:ok, _} = Repo.update(Checkout.cancel(checkout))
        {:ok, %{id: id, status: "cancelled"}}
    end
  end

  # Order callbacks

  @impl true
  def get_order(id, _conn) do
    case Repo.get(Order, id) do
      nil -> {:error, :not_found}
      order -> {:ok, Order.to_ucp(order)}
    end
  end

  @impl true
  def cancel_order(id, _conn) do
    case Repo.get(Order, id) do
      nil ->
        {:error, :not_found}

      %{status: :shipped} ->
        {:error, :invalid_state}

      order ->
        {:ok, _} = Repo.update(Order.cancel(order))
        {:ok, %{id: id, status: "cancelled"}}
    end
  end

  # Webhook callback

  @impl true
  def handle_webhook(%{"event" => "payment.completed", "data" => data}) do
    # Handle payment completion
    {:ok, :processed}
  end

  def handle_webhook(_), do: {:error, :unknown_event}
end
```

## ACP (Agentic Commerce Protocol) Support

Bazaar also supports [ACP](https://github.com/agentic-commerce-protocol/acp-spec), the commerce protocol used by OpenAI's Operator and Stripe. You can serve both UCP and ACP clients from the same handler.

### Internal Format: UCP

Bazaar uses UCP as its internal/canonical format. Your handler always works with UCP field names and status values, regardless of which protocol the client uses:

```
ACP Request → [transform to UCP] → Your Handler → [transform to ACP] → ACP Response
UCP Request → Your Handler → UCP Response
```

This means you write your handler once using UCP conventions (`items`, `street_address`, `incomplete`), and Bazaar automatically translates for ACP clients (`line_items`, `line_one`, `not_ready_for_payment`).

### Dual Protocol Setup

Mount both protocols at different paths:

```elixir
defmodule MyAppWeb.Router do
  use Phoenix.Router
  use Bazaar.Phoenix.Router

  scope "/" do
    pipe_through :api

    # UCP routes (Google agents)
    bazaar_routes "/", MyApp.UCPHandler

    # ACP routes (OpenAI/Stripe agents)
    bazaar_routes "/acp", MyApp.UCPHandler, protocol: :acp
  end
end
```

### Protocol Differences

Bazaar automatically handles the differences between UCP and ACP:

| Aspect | UCP | ACP |
|--------|-----|-----|
| URL style | `/checkout-sessions` | `/checkout_sessions` |
| Update method | `PATCH` | `POST` |
| Cancel method | `DELETE` | `POST /cancel` |
| Discovery | `/.well-known/ucp` | None |
| Status: incomplete | `incomplete` | `not_ready_for_payment` |
| Status: ready | `ready_for_complete` | `ready_for_payment` |
| Address: street | `street_address` | `line_one` |
| Address: city | `address_locality` | `city` |
| Items key | `items` | `line_items` |

Your handler code stays the same: Bazaar transforms requests and responses automatically.

### ACP Routes

When using `protocol: :acp`, these endpoints are created:

| Method | Path | Description |
|--------|------|-------------|
| POST | `/checkout_sessions` | Create checkout |
| GET | `/checkout_sessions/:id` | Get checkout |
| POST | `/checkout_sessions/:id` | Update checkout |
| POST | `/checkout_sessions/:id/complete` | Complete checkout |
| POST | `/checkout_sessions/:id/cancel` | Cancel checkout |

## Related Protocols

UCP integrates with:

- [Agent2Agent (A2A)](https://developers.googleblog.com/en/a2a-a-new-era-of-agent-interoperability/) - Agent communication
- [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) - AI model integration
- [Agent Payments Protocol (AP2)](https://developers.google.com/merchant/ucp) - Secure payments

## License

Apache 2.0