Skip to main content
Glama
README.md
# Agent Commerce SDK

Install our SDK on a Shopify merchant, connect Razorpay, and expose the
merchant's commerce capabilities through MCP so AI agents can shop and
transact on behalf of users under explicit authorization and policy.

This is a control plane, not another storefront, chatbot, or payment gateway.
Shopify remains the system of record for commerce; Razorpay remains the
system of record for payments. This SDK owns orchestration, identity,
delegated authorization, deterministic policy, approval, reconciliation, and
audit — see `docs/ARCHITECTURE.md` for the full design and the boundaries
between what Shopify/Razorpay/our SDK each own.

## Architecture

```
AI AGENT (Local, or any external MCP client)
        |
        v
UNIFIED MCP SERVER  (/mcp -- official `mcp` SDK, streamable-HTTP)
        |
        v
mcp.tools.execute_tool  <-- the single dispatch choke point
        |
        v
AGENT COMMERCE SDK (sdk/agent_commerce/)
  commerce/  identity/  policy/  audit/  security/
        |                    |
        v                    v
  shopify/ (ShopifyPort)   razorpay/ (RazorpayPort)
        |                    |
        v                    v
  Shopify commerce state   Razorpay payment state
```

Full component/data-flow diagrams, the database ERD, the Shopify/Razorpay
integration boundaries, and every ambiguity resolved during the build are in
[`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md). The REST API contract both the
backend and the dashboard build against is in
[`docs/API_CONTRACT.md`](docs/API_CONTRACT.md).

## Repository layout

```
sdk/agent_commerce/   the SDK -- all business logic lives here
  core/                shared types, errors, Protocols (ShopifyPort/RazorpayPort)
  shopify/             Admin API GraphQL client, webhooks, demo-mode fallback
  razorpay/            orders, payments, verification, webhooks, reconciliation
  identity/            users, agents, delegated grants, scopes
  policy/              deterministic rules, evaluator, versioning
  commerce/            catalog, cart, checkout, orders, fingerprinting
  agent/               Ollama qwen2.5:7b bounded tool-calling loop
  mcp/                 the unified MCP server + tool dispatcher
  audit/               append-only ledger + evidence-backed decision records
  security/            input validation, idempotency, signatures, encryption
  client.py            the `AgentCommerce` SDK facade (see below)
api/app/               FastAPI REST routes + app wiring; mounts the MCP server
frontend/              Next.js merchant dashboard
tests/                 unit, security, shopify, razorpay, ai, e2e, mcp, integration
scripts/               seed_demo.py
```

## Prerequisites

- Python 3.12+
- PostgreSQL (via Docker Compose, or your own instance)
- [Ollama](https://ollama.com), running locally, with `qwen2.5:7b` pulled:
  ```
  ollama pull qwen2.5:7b
  ```
  The application never substitutes another model — `OLLAMA_MODEL` is
  hard-validated to equal exactly `qwen2.5:7b` at startup.
- Node 20+ (for the dashboard)
- A Razorpay account in **TEST MODE** for real payment testing (optional — see
  "Running without credentials" below)
- A Shopify development store for real catalog/order testing (optional — see
  below)

## 1. Install the SDK

```
python -m venv .venv
# Windows: .venv\Scripts\activate    macOS/Linux: source .venv/bin/activate
pip install -e ./sdk
pip install -r api/requirements.txt
pip install -r requirements-dev.txt   # adds pytest, ruff for development
```

## 2. Configure environment variables

```
cp .env.example .env
```

Fill in what you have; the application runs with partial configuration (see
"Running without credentials"). Key variables:

| Variable | Required for | Notes |
|---|---|---|
| `DATABASE_URL` | everything | defaults to local Postgres |
| `APP_SECRET_KEY` | dashboard sessions, credential encryption | set a real random value outside development |
| `OLLAMA_BASE_URL`, `OLLAMA_MODEL` | the agent runtime | model must be exactly `qwen2.5:7b` |
| `SHOPIFY_SHOP_DOMAIN`, `SHOPIFY_CLIENT_ID`, `SHOPIFY_CLIENT_SECRET` | real Shopify catalog/orders | the app mints + refreshes its own token; see step 4 |
| `RAZORPAY_KEY_ID`, `RAZORPAY_KEY_SECRET`, `RAZORPAY_WEBHOOK_SECRET` | real payments | **test mode only** — see step 5 |

## 3. Start Postgres and run migrations

```
docker compose up -d postgres
alembic upgrade head
```

## 4. Connect a Shopify store (optional)

Without this step, the SDK runs against a dynamically generated demo catalog
(`shopify/demo_data.py`) so the full search → cart → checkout → policy →
approval flow is usable immediately. Real Shopify activates automatically the
moment credentials are set — nothing else changes.

To connect a real development store:

1. In the Shopify **Dev Dashboard**, create an app (custom-app creation moved
   out of store admin in 2026).
2. Grant Admin API scopes: `read_products`, `read_inventory`, `read_orders`,
   `write_draft_orders`, `read_customers` — plus `write_discounts` and
   `write_orders` if you want the growth agent's campaigns and synthetic
   seeding. Scopes take effect only once you **release a version** and the
   store approves it.
3. Install the app on your store.
4. Set `SHOPIFY_SHOP_DOMAIN` (e.g. `your-store.myshopify.com`) plus
   `SHOPIFY_CLIENT_ID` and `SHOPIFY_CLIENT_SECRET` in `.env`. Leave
   `SHOPIFY_ACCESS_TOKEN` empty — see below.
5. Sync the catalog via the dashboard's onboarding flow, or `POST
   /api/merchants/{id}/shopify/sync`.

**You do not paste an access token.** Shopify stopped issuing non-expiring
tokens for custom apps in January 2026 — every token now lasts about 24 hours,
so a pasted one dies overnight (it surfaces as a 401 mid-session). Instead the
app mints its own from `SHOPIFY_CLIENT_ID`/`SHOPIFY_CLIENT_SECRET` via the
client-credentials grant, caches it in-process, refreshes it five minutes
before expiry, and re-mints once automatically if a request is rejected with a
401 — see `sdk/agent_commerce/shopify/tokens.py`. Setting
`SHOPIFY_ACCESS_TOKEN` still works and takes precedence, but a pinned token is
never refreshed.

One prerequisite worth knowing: the client-credentials grant only works when
the app and the store belong to the **same Shopify organization** (a store
created from the Dev Dashboard's *Dev stores* page). A store created from
Shopify admin sits outside the org and the grant is refused regardless of how
correct the credentials are.

The SDK uses Shopify's GraphQL Admin API exclusively — never storefront HTML
scraping. Checkout is built on Shopify **Draft Orders** (not Storefront
Cart/Checkout), since payment capture happens through Razorpay rather than
Shopify's native checkout; see `docs/ARCHITECTURE.md` for why.

## 5. Connect Razorpay Test Mode (optional)

Without this, every read/browse/cart/checkout/policy/approval tool call still
works — only `complete_checkout` (the one tool that moves money) fails with a
clear "Razorpay Test Mode integration is ready to wire" message rather than
faking a payment result. Money movement is never simulated.

1. Razorpay Dashboard → **Settings → API Keys**, generate a **Test Mode** key
   pair.
2. Set `RAZORPAY_KEY_ID` and `RAZORPAY_KEY_SECRET` in `.env`.
3. For webhook-driven payment reconciliation, add a webhook endpoint pointing
   at `POST /api/webhooks/razorpay` subscribed to `payment.captured` and
   `payment.failed`, and set `RAZORPAY_WEBHOOK_SECRET` from the webhook
   configuration.

**Never use live/production Razorpay credentials with this project.**

## 6. Start the backend

```
python run_api.py --reload
```

This serves the REST API under `/api/*` and mounts the unified MCP server at
`/mcp` in the same process (see `api/app/main.py` for how the two ASGI apps'
lifespans are combined).

**Use `run_api.py`, not `uvicorn` directly** — especially on Windows. psycopg's
async mode cannot run on Windows' `ProactorEventLoop`, and uvicorn hardcodes
that loop on win32 via an explicit `loop_factory`, which bypasses the asyncio
event-loop *policy* entirely (so setting the policy has no effect). `run_api.py`
passes a `SelectorEventLoop` factory through uvicorn's supported `loop=` hook.
Running `uvicorn api.app.main:app` directly on Windows starts fine but fails
every database request with
`InterfaceError: Psycopg cannot use the 'ProactorEventLoop'`.

Verify it's up:

```
curl http://localhost:8000/api/health
```

Interactive API docs: <http://localhost:8000/docs>

## 7. Start the dashboard

```
cd frontend
npm install
npm run dev
```

Visit `http://localhost:3000`. The onboarding flow walks through connecting
Shopify, connecting Razorpay, configuring policy, and confirming MCP is live.

## 8. Connect an MCP-compatible agent

Fastest path — one command creates a user, merchant, policy, agent, and grant,
syncs the catalog, and prints a ready-to-use bearer token:

```
python scripts/seed_demo.py
```

(Or do the same through the dashboard's onboarding flow.) Then point any MCP
client (Claude Desktop, a custom client, etc.) at:

```
http://localhost:8000/mcp
Authorization: Bearer <session token from the grant>
```

The token is shown exactly once — only its SHA-256 hash is stored.

## 9. Perform a test transaction

With the dashboard's chat interface (backed by `agent/runtime.py` and real
Ollama inference) or any connected MCP client:

```
"Find me a black hoodie under ₹2,000."
  -> search_catalog (real Shopify or demo catalog)
"Choose the best one."
  -> agent selects from actual returned products
"Buy it."
  -> create_cart -> create_checkout -> policy evaluation
     -> ALLOW: proceeds automatically
     -> REQUIRES_APPROVAL: approve via the dashboard's Purchase Approval screen
  -> complete_checkout -> real Razorpay order (test mode) -> webhook -> reconciliation
```

## Running without credentials

Per the project's design, missing external credentials never block
development:

- **No Shopify credentials** → `shopify/demo_data.py` provides a dynamically
  generated (Faker-backed, never hardcoded) catalog satisfying the exact same
  `ShopifyPort` interface the real client does.
- **No Razorpay credentials** → every tool up through `request_purchase_approval`
  works; `complete_checkout` raises a clear, actionable configuration error
  instead of faking success.

## Accounts: admin and customer

Two roles share one login form. The role is a property of the stored account,
decided at registration — it is never sent in a request body or picked on the
login screen, because a self-selected role would be a bypassable boundary.

- **The first account to register becomes the admin.** They connect Shopify and
  Razorpay, register agents, edit policy, and see Transactions/Audit.
- **Every account after that is a customer.** They get their own spending policy
  (inherited from the admin's active policy), and can chat with an agent an admin
  has assigned to them, approving their own purchases.

Assigning is one click: **Agents → Assign customer**. Mechanically it issues an
`AgentGrant` whose `user_id` is the *customer*, which is what that column already
means — `identity/grants.py:resolve_request_context` sets
`RequestContext.user_id = grant.user_id`, so the customer's carts, checkouts,
approvals, and daily-spend tracking are all their own. One agent can therefore
serve many customers, each isolated from the others by the same
`require_ownership` checks that protect any two users.

Enforcement lives in `api/app/deps.py:current_admin` (403 for a customer), not in
the UI — hiding a nav item is presentation, not security.

Customers are also mirrored into Shopify as Customer records. That needs the
`write_customers` scope **and** protected-customer-data approval on your app; if
Shopify refuses, registration logs a warning and continues, and the local
customer works fully regardless.

## Security model

- **The LLM never authorizes payment.** Qwen2.5:7B selects tools and explains
  results; it never computes prices, evaluates policy, or resolves identity.
  Every tool call is treated as untrusted input.
- **Deterministic policy engine.** `policy/evaluator.py` is pure, rule-based
  logic (scope → inventory → category → max transaction → daily limit →
  approval threshold). No LLM involvement, ever.
- **Checkout fingerprinting.** Every checkout is hashed from its trusted
  commerce facts (merchant, currency, total, line items). An approval is
  bound to that exact fingerprint; if it changes before payment (e.g. a price
  change), the approval is invalidated and a new one is required.
- **Ownership enforcement.** Every resource load checks
  `identity/grants.py:require_ownership` — one user's agent cannot read or
  mutate another user's cart, checkout, or order.
- **Explicit tool allowlist.** `security/validation.py` defines a strict
  Pydantic schema per tool (`extra="forbid"`); there is no raw GraphQL, raw
  SQL, or arbitrary HTTP passthrough anywhere in the tool surface.
- **Prompt-injection resistance.** Product descriptions/tags are treated as
  untrusted data; the agent's system prompt explicitly instructs it never to
  follow instructions embedded in tool output. See `tests/security/`.
- **Idempotency everywhere it matters.** Webhook processing, payment
  execution, and approval creation are all deduplicated via
  `security/idempotency.py` so a retried request with the same key replays
  its prior result rather than double-charging, double-creating, or
  double-applying state. Reusing a key with a different payload raises a
  conflict instead of silently applying the new payload.
- **Signed webhooks only.** Shopify (`X-Shopify-Hmac-Sha256`, base64) and
  Razorpay (`X-Razorpay-Signature`, hex) webhooks are verified via HMAC-SHA256
  before any processing; an invalid or missing signature is rejected.

Full details, including the exact evidence-backed Decision Record format and
the audit event schema, are in `docs/ARCHITECTURE.md`.

## MCP tools

Exposed by the unified MCP server — the agent never talks to Shopify or
Razorpay directly.

| Tool | Risk | Scope |
|---|---|---|
| `search_catalog`, `get_product`, `get_inventory` | READ | `catalog:read` / `inventory:read` |
| `get_cart`, `get_checkout`, `get_order`, `get_payment_status`, `get_order_history`, `get_customer_profile` | READ | various `*:read` |
| `create_cart`, `add_to_cart`, `remove_from_cart`, `update_cart` | LOW_WRITE | `cart:write` |
| `create_checkout`, `refresh_checkout` | MONEY / LOW_WRITE | `checkout:create` |
| `request_purchase_approval` | MONEY | `payment:request` |
| `complete_checkout` | MONEY | `payment:request` |
| `list_approvals`, `get_policy` | READ | `approval:read` |
| `decide_approval` | MONEY | `approval:decide` — opt-in, see below |

Read-only MCP resources (`resources/read`): `merchant://catalog`,
`merchant://policies`, `user://orders/{id}`, `checkout://{id}`,
`transaction://{id}`.

### Connecting Claude / ChatGPT

The `/mcp` endpoint is a standard streamable-HTTP MCP server, so Claude
Desktop, Claude Code, ChatGPT (Developer Mode, paid plan) and any other MCP
client can drive the full user-side surface: browse, cart, checkout, request
approval, pay via Razorpay, read orders and the audit timeline. Setup steps
per client — including the ngrok tunnel ChatGPT needs, since it cannot reach
`localhost` — are in [`docs/MCP_CLIENTS.md`](docs/MCP_CLIENTS.md).

`approval:decide` is the one scope to think about before granting. It lets a
client approve its own pending purchase from inside the chat instead of the
dashboard — convenient for an agent you drive personally, but it means the
agent that proposed a purchase can approve it, turning the approval threshold
into a speed bump rather than a two-party gate. It is in no default scope set,
and `POST /api/agents/{id}/assign` refuses it outright so an agent assigned to
a customer can never hold it. Ownership, stale-price invalidation, and an
audit record marked `source="mcp"` all still apply when it is granted.

## Merchant companion agent (growth/revenue insights for admins)

A second agent persona, admin-facing rather than shopper-facing: it reads a
merchant's real Shopify order/customer data, surfaces growth insights (e.g.
"revenue is up 12% this month, largely from N repeat customers"), and can
propose campaigns like a loyalty discount for repeat buyers — but a proposed
campaign only takes effect on Shopify after an admin approves it on the
dashboard. Built on the same MCP-tool/scope architecture as the shopper
agent, so it's pluggable the same way: create one via
`POST /api/agents {"kind": "merchant_companion"}`, issue it a grant with
`analytics:read`/`campaign:write`/`data:seed_write` scopes, then chat with it
through the same `/api/chat/{agent_id}` endpoint used for shopping.

| Tool | Risk | Scope |
|---|---|---|
| `get_growth_insights`, `get_customer_segments` | READ | `analytics:read` |
| `propose_campaign` | LOW_WRITE (drafts only — never touches Shopify) | `analytics:read` |
| `launch_campaign` | HIGH_WRITE (creates a real discount code) | `campaign:write` |
| `seed_synthetic_data` | HIGH_WRITE | `data:seed_write` |

These scopes are disjoint from the shopper scopes above: a shopper-scoped
grant can never call a growth tool, and a growth-scoped grant can never move
money. Full flow (insights → propose → admin approves → launch → metrics
snapshot) and the campaign state machine are in `docs/ARCHITECTURE.md`
"Merchant companion agent"; the REST surface is in `docs/API_CONTRACT.md`
"Growth & Campaigns".

## Example: using the SDK directly

```python
from agent_commerce import AgentCommerce

commerce = AgentCommerce(session=session, shopify=shopify, razorpay=razorpay, ctx=ctx)

products = await commerce.catalog.search(query="hoodie", max_price=Decimal("2000"))
cart = await commerce.cart.create(lines=[{"variant_id": "...", "quantity": 1}])
checkout = await commerce.checkout.create(cart_id=cart["cart_id"])
if checkout["next_step"] == "request_purchase_approval":
    await commerce.checkout.request_approval(checkout["checkout_id"])
    # ... user approves via the dashboard ...
payment = await commerce.checkout.complete(checkout["checkout_id"])
```

Both this facade and the MCP transport route through the identical
`mcp.tools.execute_tool` dispatcher — business logic exists in exactly one
place.

## Testing

```
pytest tests/unit tests/security          # fast, no external dependencies
pytest tests/mcp                          # MCP protocol conformance (tools/call, resources/read)
pytest tests/razorpay                     # real Razorpay test-mode API (needs credentials)
pytest tests/shopify                      # real Shopify Admin API (needs credentials; skips cleanly without)
pytest tests/ai tests/e2e                 # real Ollama qwen2.5:7b inference, no mocking (slower)
```

`tests/ai/test_real_ollama.py` and `tests/e2e/test_real_agent_shopify_flow.py`
never mock the LLM, the policy engine, or the commerce layer — they exercise
real inference against a real (demo or seeded) catalog and real PostgreSQL,
per the project's testing philosophy: prefer real integrations over mocked
demonstrations. `tests/shopify/` and `tests/integration/` are reserved for
real-Shopify-credential coverage and cross-module integration tests
respectively — currently unpopulated (no test files yet).