> ## 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.

# Attribution engine

> Attribution answers one question for every order: which creator's click earned this sale?

Attribution answers one question for every order: **which creator's click earned this sale?** This page explains how a click is recorded, how it travels to checkout, how an order is matched back to it, and what happens when no click qualifies.

For the plain-language version, see [How credit is decided](https://docs.higherfrequency.io/how-it-works/attribution.md).

> [!WARNING]
>
> **The Freeq Growth Network is last-click.** The most recent eligible click before the purchase wins. We identify both the first click and the last click: the last click earns the credit, and the first click is recorded for display (see [First-touch display](#7-first-touch-display)).

## 1. Minting a click

Every creator link resolves through the public redirect, served without the `/api` prefix:

| Link shape | Example | Notes |
| - | - | - |
| Short link (default) | `/go/<17-character code>` | A 17-character Crockford Base32 code (16 random characters plus a check character). Hides the campaign id and the creator's address. |
| Long link (legacy) | `/go/campaign_<uuid>/0x<creator address>` | Still resolves. Adds `utm_campaign` and `utm_content` to the destination. |

Each request:

1. Looks up the campaign destination. Unknown codes and campaigns return `404`.
2. Mints a click id of the form `clk_<uuid v4>`.
3. Writes one **click record** (`UserClickProfile`): click id, campaign, creator address (lower-cased) and a derived pseudo-address (a hash of the click id, used as the "user" on-chain).
4. Builds the destination URL with tracking parameters:
   * `uam_click=<click id>` (the primary carrier)
   * `utm_source=uam`, `utm_medium=affiliate`
   * `utm_term=<click id>` (a copy that survives Shopify's journey data, which strips non-UTM parameters)
5. Optionally allocates a discount code (below), then responds `302`.

**Nothing is written on-chain at click time.** The redirect is rate limited per IP with a ceiling far above human traffic.

### Per-click discount codes

When a brand enables codes for a campaign and the store's provider supports writing discounts, the redirect mints a single-use code shaped `UAM-XXXX-XXXX-XXXX` for **this click**, valid for a short time (45 minutes by default). The redirect is wrapped in the provider's apply-and-redirect URL so the code is already in the cart. These are not vanity codes; each one maps to exactly one click.

If the code cannot be issued (budget or cap exhausted, campaign ended), the shopper is still redirected with the click recorded, plus `uam_coupon=unavailable` so the storefront can show a message. If code issuance errors or times out, the redirect fails open to the plain tracked URL.

### Landing-path override

Some partner providers allow a link to land on a specific page: `/go/<code>?to=/<relative path>`. The path must be relative, same-origin and inside that provider's allowlist of path prefixes. Anything else falls back to the campaign destination without error. Attribution is unaffected either way, because the click is minted before the landing path is resolved.

## 2. Carrying the click to checkout (Shopify)

URL parameters are fragile: Shopify normalises landing pages and may drop unknown parameters. The **theme app embed** installed with the Shopify app makes the click durable. On every page load it:

* reads `uam_click` from the URL and accepts it only if it matches `clk_<uuid v4>`;
* **overwrites** the cart attribute `uam_click` with it (this reaches the order as `note_attributes`);
* injects a hidden line-item property `_uam_click` into product forms, so the click is stamped on each line at add-to-cart and on unbranded "Buy it Now";
* mirrors the click to `localStorage` and re-applies it if the cart is reset.

The overwrite is what makes the system last-click: if a shopper lands from creator A and later from creator B, the cart now carries B. Items already in the cart keep their earlier line stamp, so one order can carry several clicks; the resolver handles that (below).

The embed is fire-and-forget and never blocks or alters the storefront. One permanent gap: branded wallet buttons (Shop Pay, PayPal, Apple Pay) render in frames the embed cannot reach, so a branded "Buy it Now" carries no line stamp and relies on the cart attribute or the weaker rungs.

## 3. Resolving a Shopify order

An order webhook is **accepted** if it carries any carrier the platform attached: `uam_click` in the landing URL, in `note_attributes`, in a line-item property, or a `UAM-` discount code. Orders with none are kept only when they came from the online store (`source_name = web`), as recovery candidates; everything else (POS, drafts, API orders) is ignored.

Resolution is a ladder that **stops at the first rung that yields a click**:

| Rung | Signal | Runs for |
| - | - | - |
| 1 | A `UAM-` discount code applied to the order (for online-store orders, also recovered from the Shopify Admin API if the webhook omitted it) | Every accepted order |
| 2 | Durable carriers: the cart attribute and every line-item stamp, from the webhook and the Admin API | Every accepted order |
| 3 | Shopify customer journey, **last** visit | Online-store orders only |
| 4 | Landing / referring URL | Online-store orders only |

Merchant-owned codes (for example `SUMMER20`) are not a signal and never block the lower rungs. A customer's email is **not** a rung; it is associated with the click only after resolution.

### Rung 2: newest eligible click wins

All candidate click ids are collected and de-duplicated. A candidate is **eligible** when its campaign exists, belongs to this order's store, and promotes a product that is in the order. The winner is the eligible click with the newest creation time (ties go to the click in the cart attribute, then the lower id).

* No candidates: fall through to rungs 3 and 4.
* Candidates exist but none are known: `UNATTRIBUTED` (`CLICK_NOT_FOUND`).
* Known but none eligible: `UNATTRIBUTED` with the reason of the newest one (`CAMPAIGN_NOT_ACTIVE`, `STORE_MISMATCH` or `PRODUCT_NOT_IN_CAMPAIGN`).
* Admin API data incomplete: no winner is picked; the order stays pending and is retried.
* Implausibly many distinct clicks: `UNATTRIBUTED` (`DURABLE_CANDIDATES_EXCEEDED`). A winner is never chosen from a truncated set.

### Rung 4: URL freshness

The landing URL can be stale (a browser restoring a days-old page). An optional freshness gate accepts a URL click only if it was minted shortly before the order (60 minutes by default) and not after it; otherwise the order is `UNATTRIBUTED` (`CLICK_TOO_OLD`).

## 4. Other providers: the neutral path

Non-Shopify providers do not send Shopify payloads. Their adapters normalise each order to a neutral input (`clickIdCandidates`, line items with external product ids, `occurredAt`, and optional buyer identity) and the engine resolves it without any journey lookup or URL parsing:

1. A platform discount code stored on the order row, if any.
2. Each entry of `clickIdCandidates`, in order. A candidate from another store is skipped.
3. Nothing left: `UNATTRIBUTED`.

Every resolution then passes a central store guard. See [Commerce providers](https://docs.higherfrequency.io/developers/commerce-providers.md).

## 5. After a click is chosen

The same checks run for both paths before any money moves:

* **Product match.** The campaign's product must be in the order (store-wide campaigns excepted).
* **Self-referral.** If the provider supplies a buyer identity and it resolves to the very creator being credited: `SELF_REFERRAL`, no commission.
* **Returning customer.** A buyer whose normalised email already produced a paid order in the same campaign is tracked but not paid: `INELIGIBLE_RETURNING_BUYER`. Providers can send their own authoritative first-time-buyer flag instead.
* **Budget and membership.** The campaign must cover the commission and the creator must be a campaign member on-chain.

The commission base is frozen on the order before anything is sent. It is the order total (after discounts, including tax and shipping) for every campaign. For a single-product campaign the product only decides whether the order qualifies.

## 6. On-chain writes and retries

Only after attribution resolves does the backend write the chain, in order: `recordUserMovement` (the winning click) then `registerConversion`. Each conversion goes through one durable submission record: intent before sending, the broadcast hash after, and a reconcile step on every retry. That prevents a crash from paying the same sale twice. Escrow and ledger entries follow a successful conversion. See [Money path](https://docs.higherfrequency.io/developers/money-path.md).

A **recovery poller** sweeps orders left pending: never started, stuck mid-flight, or waiting for Shopify's journey data to become ready. Orders still pending after **24 hours** expire as `UNATTRIBUTED` (`NOT_CONFIRMED_IN_24H`). Orders with an in-flight chain submission are never expired.

## 7. First-touch display

For Shopify online-store orders, a background job later reads the journey's **first** visit and, if it carries a click, stores it for display ("first touch"). It typically appears some minutes after the order, once Shopify's journey data is ready.

First touch never changes **attribution**: the last eligible click always wins the order, and no attribution, escrow, ledger, settlement or on-chain path reads the display fields. Separately, the campaign's **reward mode** (`rewardMode`, locked when the campaign is activated) decides how the winning order's commission is paid:

* `LAST_CLICK_ONLY`: the last-click creator receives the whole order commission.
* `FIRST_LAST_80_20` (Shopify only): if another eligible creator first introduced the shopper, they receive 20% of the order commission and the last-click creator the remaining 80%. The discovery resolver turns the first touch into a frozen per-order decision once; the money path reads that decision, never the display fields. The 20% share is `floor(hold × 2000 / 10000)` in minor units and the last-click creator gets the remainder.

The resolver also records a **first-touch point** (internally a discovery point) for the first-touch creator in either mode. Points are a ranking signal only, never money.

## 8. Order outcomes and reasons

| Status | Paid? | Meaning |
| - | - | - |
| `PENDING` | Not yet | Accepted, still being resolved or retried. |
| `CONVERTED` | Held in escrow | Attributed and recorded on-chain. |
| `SETTLED` | Released | Hold released to the creator. |
| `RETURNED` / `PARTIAL_RETURN` / `CHARGED_BACK` | Reversed in whole or part | See [Money path](https://docs.higherfrequency.io/developers/money-path.md). |
| `UNATTRIBUTED`, `FAILED` | No | No qualifying click, or a technical failure. |
| `INELIGIBLE_RETURNING_BUYER`, `SELF_REFERRAL` | No | Attributed, but not commissionable. |

Not-paid orders carry a stable reason code. Readers must tolerate codes added later and `null` on older rows.

| Code | Meaning |
| - | - |
| `CLICK_NOT_FOUND` | No usable click, or the click id is unknown. |
| `CLICK_TOO_OLD` | URL click failed the freshness gate. |
| `STORE_MISMATCH` | The click belongs to another store. |
| `CAMPAIGN_NOT_ACTIVE` | Campaign missing, not active, or past its late-conversion cutoff. |
| `PRODUCT_NOT_IN_CAMPAIGN` | None of the order's products are in the campaign. |
| `BUDGET_EXHAUSTED` | The budget could not cover the commission. |
| `PUBLISHER_NOT_IN_CAMPAIGN` / `PUBLISHER_NOT_FOUND` | The creator is not a member, or their account is missing. |
| `RETURNING_CUSTOMER` | The buyer already bought through this campaign. |
| `SELF_REFERRAL` | The buyer is the credited creator. |
| `NOT_CONFIRMED_IN_24H` | Still unresolved after the recovery window. |
| `DURABLE_CANDIDATES_EXCEEDED` | Too many distinct clicks on one order. |
| `TECHNICAL_ERROR` | An internal or on-chain failure, not a business rule. |

The authoritative list is `ATTRIBUTION_REASON_CODES` in `src/modules/commerce/domain/attribution-reason.ts`.


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