This guide covers how to run the proxy locally, build new adapters, and the patterns established from WooCommerce and Wix implementations.
# Demo mode (in-memory, no external dependencies)
MERCHANT_ID=demo ADAPTER_TYPE=demo go run ./cmd/proxy
# WooCommerce (requires config file)
CONFIG_FILE=config.woo.local.json go run ./cmd/proxy
# Wix (requires config file)
CONFIG_FILE=config.wix.local.json go run ./cmd/proxygo test ./... # All tests
go test ./internal/woocommerce/ # Specific package
go test -v -run TestCartToUCP # Specific test{
"port": "8080",
"environment": "development",
"log_level": "debug",
"adapter_type": "woocommerce",
"merchant_id": "my-store",
"merchant": {
"store_url": "https://mystore.com",
"api_key": "ck_xxx",
"api_secret": "cs_xxx",
"policy_links": {
"privacy_policy": "https://mystore.com/privacy",
"terms_of_service": "https://mystore.com/terms"
},
"payment_handlers": {
"com.google.pay": [{
"id": "gpay-checkout",
"version": "2026-01-11",
"spec": "https://ucp.dev/handlers/google.pay",
"config": { "...": "handler-specific config" }
}]
}
}
}See config.example.json for the full schema with all optional fields.
Every adapter implements the Adapter interface defined in internal/adapter/adapter.go. The interface has six methods: GetProfile, CreateCheckout, GetCheckout, UpdateCheckout, CompleteCheckout, and CancelCheckout.
internal/{platform}/
├── adapter.go # Adapter interface impl (entry point, orchestration)
├── client.go # HTTP client, API calls (keep adapter.go thin)
├── transform.go # Platform types ↔ UCP types
├── types.go # Platform API request/response types
├── batch.go # Batch operations (if platform supports)
└── *_test.go # Tests for each file
Why this split?
types.go: Pure data structures, easy to review against API docstransform.go: Isolated mapping logic, testable without HTTPclient.go: HTTP concerns isolated, easier to mockadapter.go: Thin orchestration layer
The proxy must remain stateless. Encode session tokens in the checkout ID.
WooCommerce (cart token):
gid://store.com/Cart/{cart_token}
gid://store.com/Checkout/{order_id}:{cart_token}
Wix (OAuth access token):
gid://wix.{site_id}/Checkout/{checkout_id}:{access_token}
See internal/woocommerce/transform.go and internal/wix/transform.go for the actual BuildCheckoutID and ParseCheckoutID implementations.
Trade-offs:
- (+) No server-side session storage
- (+) Horizontally scalable
- (-) IDs are longer/opaque
- (-) Tokens may expire (handle with retry or refresh)
Status should be derived from platform state, not stored separately. See:
internal/woocommerce/transform.go—determineCartStatus()checks items, errors, buyer email, shipping selectioninternal/wix/transform.go—determineStatus()checks items, buyer info, then returnsrequires_escalation(escalation flow)
Key insight: Billing address comes with the payment instrument at complete time (per UCP spec).
Only buyer email is required upfront for order notifications. Full billing address is provided
via PaymentInstrument.BillingAddress when calling complete.
Status transitions are:
incomplete→ building cart, missing required fields (email, shipping if physical)ready_for_complete→ can call CompleteCheckout with payment instrumentrequires_escalation→ must redirect to browser checkoutcompleted→ order finalized
Keep platform types and UCP types strictly separated:
types.go— Platform API types matching their docs exactlytransform.go— Conversion functions likeCartToUCP(),AddressFromUCP()
See internal/woocommerce/types.go and internal/woocommerce/transform.go for examples.
Benefits:
- Tests don't need HTTP mocking
- API changes isolated to types.go + transform.go
- Clear audit trail for field mappings
Different platforms require different auth approaches:
WooCommerce — Nonce per mutation. Every write operation requires a fresh nonce fetched via GET request. See internal/woocommerce/client.go for fetchNonce() and doCheckoutRequest().
Wix — OAuth token embedded in checkout ID. Token obtained during CreateCheckout, extracted and used for all subsequent API calls. See internal/wix/adapter.go.
Map platform errors to UCP error types consistently. Standard error constructors are defined in internal/model/errors.go: NewValidationError, NewNotFoundError, NewUpstreamError, NewRateLimitError.
Each adapter's client maps HTTP status codes and platform-specific error responses to these types. See internal/woocommerce/client.go for an example of parseErrorResponse().
Direct processing (WooCommerce + Stripe) — Payment token submitted server-side, with 3DS escalation handling. See internal/woocommerce/client.go for CompleteCheckout().
Browser handoff (Wix) — Current implementation always returns requires_escalation with a redirect URL to Wix's hosted checkout. Programmatic payment to be added. See internal/wix/adapter.go for CompleteCheckout().
UCP expects specific total types. See internal/woocommerce/transform.go for buildTotals() implementation.
Required totals: subtotal, total
Optional: items_discount, discount, fulfillment, tax, fee
The proxy implements capability negotiation per UCP spec. This ensures agents only receive capabilities they can handle and merchants only expose capabilities they support.
Negotiation is transport-agnostic. The core logic lives in internal/negotiation/ with transport-specific extraction:
┌────────────────────────────────────────────────────────────────┐
│ NEGOTIATION CORE (shared) │
│ internal/negotiation/ │
│ │
│ - ProfileFetcher: fetch + cache agent profiles (HTTP) │
│ - Intersect(business, agent) → NegotiatedContext │
│ - NegotiatedContext stored in request context │
└────────────────────────────────────────────────────────────────┘
▲ ▲
│ profileURL │ profileURL
┌──────────────────┴──────────┐ ┌───────────┴────────────────┐
│ REST TRANSPORT │ │ MCP TRANSPORT │
│ (middleware pattern) │ │ (per-invocation) │
│ │ │ │
│ Extract from: │ │ Extract from: │
│ UCP-Agent header │ │ meta.ucp-agent.profile │
│ (RFC 8941 syntax) │ │ (in request params) │
└─────────────────────────────┘ └────────────────────────────┘
Agents include profile URL in the UCP-Agent header (RFC 8941 Dictionary Structured Field):
POST /checkout-sessions HTTP/1.1
UCP-Agent: profile="https://agent.example/profile"
Content-Type: application/jsonThe NegotiationMiddleware in internal/negotiation/middleware.go:
- Parses
UCP-Agentheader - Fetches agent profile (with caching)
- Computes capability intersection
- Stores
NegotiatedContextin request context
Agents include profile URL in meta.ucp-agent.profile:
{
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"name": "create_checkout",
"arguments": {
"meta": {
"ucp-agent": { "profile": "https://agent.example/profile" }
},
"checkout": {
"line_items": [{"product_id": "123", "quantity": 1}]
}
}
}
}Each MCP handler extracts meta from tool arguments and calls the negotiation core.
The HTTPProfileFetcher implements HTTP-compliant caching:
- Respects
Cache-Control: max-age=Nheaders - Supports ETag-based conditional requests (
If-None-Match) - Falls back to cached data on fetch failures (stale-while-error)
- Default TTL: 5 minutes when no cache headers present
- LRU eviction when cache exceeds 1000 entries
See internal/negotiation/intersection.go:
- Include business capability if agent has matching
name - Prune orphaned extensions (where parent not in intersection)
- Repeat until stable
Adapters use getEffectiveConfig(ctx) to get negotiated capabilities:
func (c *Client) getEffectiveConfig(ctx context.Context) *model.TransformConfig {
negotiated := negotiation.GetNegotiatedContext(ctx)
if negotiated == nil {
return c.transformConfig // No negotiation, use full config
}
cfg := *c.transformConfig
if negotiated.Capabilities != nil {
cfg.Capabilities = negotiated.Capabilities
}
if negotiated.PaymentHandlers != nil {
cfg.PaymentHandlers = negotiated.PaymentHandlers
}
return &cfg
}All transform calls use this helper to filter capabilities in responses.
ucpclient hosts its own profile server for spec-compliant testing:
# ucpclient serves profile at localhost:9999/profile
ucpclient create -proxy http://localhost:8080 -product 60 \
-profile-port 9999 -profile-file ./my-agent-profile.jsonFlags:
-profile-port: Port to serve profile (0 = auto-select)-profile-file: Path to agent profile JSON (uses default if not set)
Transform tests — Test conversion logic without HTTP. Create platform structs, call transform functions, assert UCP output. See internal/woocommerce/transform_test.go.
Integration tests — Use httptest.NewServer to mock platform APIs. See internal/woocommerce/client_test.go for examples.
# Build and deploy
gcloud run deploy ucp-proxy \
--source . \
--region us-central1 \
--allow-unauthenticated \
--set-env-vars "MERCHANT_ID=my-store,ADAPTER_TYPE=woocommerce"| Variable | Required | Description |
|---|---|---|
MERCHANT_ID |
Yes | Unique identifier for this merchant |
ADAPTER_TYPE |
Yes | demo, woocommerce, or wix |
PORT |
No | HTTP port (default: 8080) |
CONFIG_FILE |
No | Path to JSON config file |
GCP_PROJECT |
Prod | For Secret Manager integration |
Use GCP Secret Manager for API credentials:
echo -n "ck_xxx" | gcloud secrets create woo-api-key --data-file=-
echo -n "cs_xxx" | gcloud secrets create woo-api-secret --data-file=-The proxy loads secrets via internal/config/secrets.go.
- Implement
Adapterinterface inadapter.go - Define platform types in
types.gomatching their API docs - Build transform layer in
transform.go - Implement HTTP client in
client.go - Design checkout ID format with embedded session token
- Implement status determination logic
- Map platform errors to UCP error types
- Handle payment (direct or escalation)
- Write transform tests (no HTTP)
- Write integration tests (with HTTP mock)
- Add config loading in
internal/config/ - Register adapter type in
cmd/proxy/main.go - Document platform-specific gotchas