> ## Documentation Index
> Fetch the complete documentation index at: https://docs.higherfrequency.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Commerce providers

> A commerce provider is a store platform the platform can attribute sales on.

A **commerce provider** is a store platform the platform can attribute sales on. Shopify is the main one. Others connect through the same interface, so that every provider shares one attribution engine, one escrow and ledger, and one on-chain record.

The rule for every provider is the same: **normalise your world into neutral inputs and delegate**. A provider never touches campaigns, escrow, the ledger or the chain directly.

## The provider contract

Each provider is one registry entry, defined in `src/modules/commerce/registry/commerce-provider.ts`:

```ts theme={null}
interface CommerceProvider {
  key: ProviderKey;                              // 'shopify', ...
  capabilities: ReadonlySet<CommerceCapability>; // what this provider declares
  catalog?: CatalogPort;                         // one port per port-backed capability
  discountCodes?: DiscountCodePort;
  orderEvents?: OrderEventProcessor;
  attributionRecovery?: AttributionRecoveryPort;
  storefront?: StorefrontInstrumentationPort;
  risk?: OrderRiskPort;
  billing?: ProviderBillingPort;
}
```

* The registry **rejects** a declared capability without its port, and a port without its capability. Declare only what you implement.
* Every store row stores its provider key (the `provider` column on the store table). The registry routes each store-scoped operation by that key at runtime.
* A feature that needs a capability checks for it at its boundary. A store can be connected even if its provider lacks some capabilities.

### Capabilities

| Capability | Port | What it enables | Without it |
| - | - | - | - |
| `CATALOG_READ` | `CatalogPort` | Product listing (cursor-paged), product detail, campaign product pickers. | Required. |
| `ORDER_EVENTS` | `OrderEventProcessor` | Inbound orders, refunds, fulfilments, disputes. | Required. |
| `DISCOUNT_CODE_WRITE` | `DiscountCodePort` | Per-click discount codes from the `/go` redirect, and code-first attribution. | Clicks carry only the click id. |
| `PROVIDER_BILLING` | `ProviderBillingPort` | App-store subscription entitlement for brands. | The provider's stores are exempt from the paid-plan gate. |
| `ATTRIBUTION_RECOVERY` | `AttributionRecoveryPort` | Recovering a click id for an order that arrived without one. | Fewer recovered orders. |
| `STOREFRONT_INSTRUMENTATION` | `StorefrontInstrumentationPort` | Storefront tracking-script health. | No embed-health alerts. |
| `ORDER_RISK` | `OrderRiskPort` | Provider risk verdicts feeding the escrow risk tiers. | Provider risk signals are not used. |

## Providers today

| Provider | Capabilities declared | Connection | Clicks | Orders | Refunds | Discount codes | Catalogue |
| - | - | - | - | - | - | - | - |
| **Shopify** | catalog, order events, discount codes, billing | OAuth app install + theme app embed | `/go` + embed carriers | Signed webhooks | Full and partial, plus disputes | Yes | Admin API, cached |
| **Partner marketplace** | catalog, order events | Partner wire (server to server) | `/go`, token stored by the partner | Signed pushes | Full refunds | No | Pulled from the partner's API |
| **Internal test providers** | catalog, order events (one also discount codes) | Internal | `/go` or n/a | Internal | Internal | One only | Hard-coded or in-memory |

Two internal providers exist only to exercise the abstraction in development and automated tests.

### Shopify

Shopify is the reference implementation (`src/modules/commerce-providers/shopify/` plus the older `src/modules/shopify/`).

* **Connection.** A brand installs the app and authorises the store with OAuth. The access token is stored per store connection. Stores are soft-deleted only; uninstalling never removes history.
* **Webhooks.** Orders created, refunds created, disputes created and updated, orders fulfilled, checkouts updated, products updated and deleted, app uninstalled, and the mandatory privacy compliance topics. Every webhook is verified by HMAC before it is read.
* **Attribution.** Shopify orders use the dedicated ladder described in [Attribution engine](https://docs.higherfrequency.io/developers/attribution-engine.md#3-resolving-a-shopify-order), fed by the theme embed's cart and line-item carriers, the Admin API and the customer journey.
* **Billing.** Brands subscribe through Shopify's app pricing. When billing enforcement is switched on (off by default), creating and funding campaigns, among other actions, requires an active plan, or an active FREEQ stake as an alternative.
* **Extra signals.** Shopify's order risk assessments and theme-embed health are consumed by the Shopify module directly; the matching capabilities in the interface are there for future providers.

### Partner marketplace (partner wire)

A partner marketplace is integrated as a partner provider. It shows the pattern for a platform whose truth lives in a remote system:

* **Order events** are pushed to `POST /partner/stores/{storeId}/events`, signed with HMAC-SHA256 over the raw body (`X-Uam-Hmac-Sha256`), with `X-Uam-Event-Id` and `X-Uam-Event-Type` headers that must match the body. Delivery is at-least-once from a transactional outbox; the inbox de-duplicates on `eventId`. `2xx` means accepted (duplicates included), `4xx` is terminal, `5xx` should be retried.
* **Creator provisioning** is a synchronous, idempotent call (`POST /partner/stores/{storeId}/publishers`) keyed on the partner's own user id. It returns the creator's share link.
* **Earnings** are pulled per creator with a bearer key that is separate from the signing secret, so a leaked read key cannot forge events. The payout counter is cumulative and never decreases; the partner credits the difference since its last checkpoint.
* **Eligibility** comes from the partner: each order carries an authoritative first-time-buyer flag and a buyer id used for the self-referral check.

The frozen wire is versioned separately and shared with the partner directly. Store-level reporting for partners is the [Partner analytics API](https://docs.higherfrequency.io/developers/partner-analytics-api.md).

### Partner tenants are not providers

A partner that brings its own users and frontend (for example, a partner app signing users in with its own Clerk instance) is a **tenant**, not a provider. Its brands still connect a Shopify store; its users call the application API directly from the browser. See [Architecture: authentication and tenants](https://docs.higherfrequency.io/developers/architecture.md#authentication-and-tenants).

## How a new provider plugs in

1. **Pick a provider key** and implement the ports for the capabilities you can honestly support. External product and order ids are **opaque strings**; never coerce them to numbers.
2. **Register stores** through `StoreConnectionService.registerProviderStore` with your key. Membership, invitations, entitlement and the store switcher work unchanged.
3. **Ingest orders.** Your inbound adapter (webhook receiver, poller or checkout endpoint) verifies and normalises payloads into the neutral `CommerceOrderEvent` union (`ORDER_PLACED`, `ORDER_REFUNDED`, `ORDER_FULFILLED`, `DISPUTE_CHANGED`) and submits them to `OrderIngestionService`. The inbox gives you de-duplication, claim and backoff retries, dead-lettering and replay.
4. **Process events** in your `OrderEventProcessor`. Return `processed`, `retryable` or `terminal`, derived from persisted state, never from "the promise resolved". The `reason` is a stable, low-cardinality slug with no raw provider text and no personal data.
5. **Delegate the money work** to the shared `OrderLifecycleEngine`:

```ts theme={null}
processOnChainConversion(orderId, {
  clickIdCandidates: ['clk_...'],               // newest first
  lineItems: [{ externalProductId: '123', quantity: 1, unitPriceMinor: 2500 }],
  occurredAt: new Date(),
});
processOnChainReturn(orderId, refund);           // full or partial
processDisputeChanged(orderRowId, dispute);      // OPEN | WON | LOST
processFulfillment(orderId, trackingUrl);
```

`unitPriceMinor` (net of discounts, excluding tax) on each line is optional. Commission is always a percentage of the order total, so line prices do not change the payout. If an order used a platform discount code, persist it on the order row at ingest; the neutral input deliberately carries no codes.
6\. **Add the provider** to `commerce-providers/commerce-provider-composition.ts`, behind a flag until it is proven.

### Rules beyond the interface

* **Idempotency.** Duplicate deliveries must end `processed`, not double-applied. Escrow and ledger writes are idempotency-keyed by store, campaign and order.
* **No personal data in the inbox envelope.** It is stored and replayed.
* **Fail closed.** A click or code minted for one store is rejected at another store's checkout. Unresolvable attribution ends `UNATTRIBUTED`; it is never guessed.
* **Opaque ids** everywhere.

The test-shop provider under `commerce-providers/fern/` is the smallest complete example (catalogue, discount codes, order events and engine delegation). Start there.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.