1. Minting a click
Every creator link resolves through the public redirect, served without the/api prefix:
Each request:
- Looks up the campaign destination. Unknown codes and campaigns return
404. - Mints a click id of the form
clk_<uuid v4>. - 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). - Builds the destination URL with tracking parameters:
uam_click=<click id>(the primary carrier)utm_source=uam,utm_medium=affiliateutm_term=<click id>(a copy that survives Shopify’s journey data, which strips non-UTM parameters)
- Optionally allocates a discount code (below), then responds
302.
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 shapedUAM-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_clickfrom the URL and accepts it only if it matchesclk_<uuid v4>; - overwrites the cart attribute
uam_clickwith it (this reaches the order asnote_attributes); - injects a hidden line-item property
_uam_clickinto product forms, so the click is stamped on each line at add-to-cart and on unbranded “Buy it Now”; - mirrors the click to
localStorageand re-applies it if the cart is reset.
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:
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:
UNATTRIBUTEDwith the reason of the newest one (CAMPAIGN_NOT_ACTIVE,STORE_MISMATCHorPRODUCT_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 isUNATTRIBUTED (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:
- A platform discount code stored on the order row, if any.
- Each entry of
clickIdCandidates, in order. A candidate from another store is skipped. - Nothing left:
UNATTRIBUTED.
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.
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.
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 isfloor(hold × 2000 / 10000)in minor units and the last-click creator gets the remainder.
8. Order outcomes and reasons
Not-paid orders carry a stable reason code. Readers must tolerate codes added later and
null on older rows.
The authoritative list is
ATTRIBUTION_REASON_CODES in src/modules/commerce/domain/attribution-reason.ts.