Appearance
Routing Engine - How It Works
This document is a complete walkthrough of Fyralynx's routing engine: how it generates candidates for a given URL, how each candidate is scored, how the winner is selected, and how the final redirect URL is constructed. It is the engineering companion to the EPC Model specification.
Design Constraints
The routing engine was designed under strict constraints that shape every decision:
| Constraint | Value | Rationale |
|---|---|---|
| Median routing decision time | < 40 ms | Imperceptible to end users; preserves click-through behaviour |
| P95 routing decision time | < 150 ms | SLO for the Cloudflare Worker fleet |
| Data dependencies | KV reads only | Zero live affiliate network API calls in the routing path |
| Maximum candidates per click | 7 | Bounds compute time; prevents exponential scoring cost |
| Worst-case behaviour | Original URL served unchanged | Commission loss is always preferable to user experience failure |
| Bot/rate-limit behaviour | Original URL served unchanged | Preserves user experience even when routing is bypassed |
Architecture Overview
The routing engine runs in two contexts:
Browser extension (popup): Triggered when the creator clicks the extension on a merchant product page. Calls the edge redirector API to get the routing decision, then caches the result locally for the CRC display.
Cloudflare Workers edge redirector: Triggered on every click when a real visitor follows a Fyralynx routing URL (
go.fyralynx.com/r/…). Runs the full routing pipeline and serves a sub-50 ms HTTP 302.
Both contexts run the same routing logic from @fyralynx/routing-engine. The edge context additionally handles geo-resolution (visitor country from CF-IPCountry), rate limiting, bot detection, and telemetry emission.
Step 1: Merchant Identification
When a URL arrives at the routing engine, the first step is to identify the merchant:
hostname = new URL(originalUrl).hostname
// e.g. "www.amazon.com" → strip "www." → "amazon.com"
merchantInfo = storefronts[hostname]
// storefronts.json is a flat map: "amazon.com" → { merchantId: "amazon", region: "US" }storefronts.json covers all hostnames used by the 35 supported merchants across all regions. For example, amazon.co.uk, amazon.de, amazon.in, amazon.co.jp all map to { merchantId: "amazon", ... } with their respective region codes.
If the hostname is not in storefronts.json: The original URL is returned unchanged (passthrough). No routing attempt is made. This is the safe default for unsupported merchants.
If the passthrough flag is set on the link: The original URL is returned unchanged. Click counting still occurs. No other processing runs.
Step 2: Context Resolution
Before generating candidates, the engine resolves contextual signals that affect scoring:
typescript
const visitorCountry: string = request.cf?.country // Cloudflare CF-IPCountry
?? params.get('country') // Explicit override (simulation)
?? 'UNKNOWN';
const deviceType: DeviceType = detectDevice(request.headers.get('user-agent') ?? '');
// Returns: 'ios' | 'android' | 'desktop'
// Uses User-Agent pattern matching. Defaults to 'desktop' if ambiguous.Geo-resolution note: Cloudflare provides visitor country at the edge with high accuracy. In the browser extension context, the creator can specify a country explicitly (for routing simulation) or it defaults to the creator's account country for purpose of scoring link previews.
Step 3: KV Reads (Parallel)
All KV data needed for routing is loaded in a single parallel batch to minimise latency:
typescript
const [storefronts, fxRates, deepLinkCache, offerAlerts, creatorProfile] =
await Promise.all([
env.FYRALYNX_CONFIG.get('storefronts', { type: 'json' }),
env.FYRALYNX_CONFIG.get('fx_rates', { type: 'json' }),
env.FYRALYNX_CONFIG.get('deep_link_cache', { type: 'json' }),
env.FYRALYNX_CONFIG.get(`offer_alerts:${merchantId}`, { type: 'json' }),
creatorId
? env.FYRALYNX_CONFIG.get(`creator:${creatorId}`, { type: 'json' })
: Promise.resolve(null),
]);KV read latency at the Cloudflare edge is typically < 5 ms per key. Because reads are parallel, total KV overhead is bounded by the slowest single read.
KV failure handling: If any KV read fails, the engine falls back to cached in-memory data (populated at Worker startup) or returns the original URL unchanged. Routing never fails to serve a redirect.
Step 4: Candidate Generation
Candidates are (merchant, region, network) tuples that represent possible routes for the visitor. Generation follows strict rules:
Rule 1 - Network account validation (hard invariant)
The creator must have a declared active account with a network before any candidate using that network is generated. If the creator has not declared a CJ account, zero CJ candidates are generated - regardless of how good the EPC model says CJ would be for this route.
This rule is enforced at candidate generation, before scoring. It is non-negotiable - routing to an undeclared network would constitute commission fraud.
Rule 2 - Non-zero epcHint required
A candidate is generated only if epcHints[networkId] > 0.0 in the merchant region profile. A zero hint means this network is not available for this merchant/region combination.
Rule 3 - Regional candidate expansion
If the visitor's country differs from the origin URL's storefront country, the engine also generates candidates for the matching local storefront. For example:
- Original URL:
amazon.com(US) - Visitor country:
GB - Additional candidate generated:
amazon.co.uk(GB)
This ensures the visitor is offered the best route for their actual location, not just for the URL's origin country.
Rule 4 - Original URL as fallback candidate
The original URL is always included as a candidate with a conservative EPC estimate (typically 20-30% below the best known EPC hint for this merchant/network). This guarantees at least one candidate survives even if all network-routed candidates fail scoring.
Rule 5 - Cap at 7 candidates
If more than 7 candidates are generated (possible for merchants with many regions × networks), the engine ranks them by epcHints descending and takes the top 6 plus the original URL fallback. This bounds compute to a predictable maximum.
Rule 6 - Unknown domains
If no merchant is found for the URL's hostname, the function returns immediately with the original URL. No candidates are generated.
Rule 7 - Passthrough links
If the link has passthrough: true, no candidates are generated. The original URL is returned in ≤ 1 KV read.
Example candidate set
For amazon.com/dp/B0CRMZHDG4 with a UK creator (declared networks: CJ, Awin, Impact) and a Nigerian visitor:
| # | Merchant | Region | Network | epcHint (USD) |
|---|---|---|---|---|
| 1 | Amazon | US | Impact | $0.061 |
| 2 | Amazon | US | CJ | $0.052 |
| 3 | Amazon | GB | Awin | $0.048 |
| 4 | Amazon | GB | Impact | $0.044 |
| 5 | Jumia | NG | Jumia Affiliate | $0.027 |
| 6 | Original URL fallback | - | - | conservative |
Step 5: EPC Scoring
Each candidate is scored in full by the @fyralynx/epc-engine. The scoring is synchronous and CPU-bound (no I/O after the initial KV reads).
typescript
const scoredCandidates = candidates.map(candidate => {
const inputs = buildEpcInputs(
candidate, merchantProfile, fxRates, deepLinkCache,
offerAlerts, deviceType, visitorCountry, creatorProfile
);
const score = computeRouteScore(inputs);
return { candidate, score, inputs };
});buildEpcInputs assembles all 16 signal values from the loaded data. computeRouteScore applies the formula and returns { intermediateScore, confidenceFactor, finalScore, confidenceDeltas, signalValues }.
For a full description of every signal, cap, and formula, see the EPC Model specification.
Step 6: Winner Selection
Candidates are sorted descending by finalScore (= ROUTE_SCORE_r = S_r × C_r):
typescript
scoredCandidates.sort((a, b) => b.score.finalScore - a.score.finalScore);
const winner = scoredCandidates[0];Tie-breaking: In the rare case of identical finalScore, the candidate with higher confidenceFactor is preferred (higher confidence in the estimate).
Emergency fallback: If the winner's finalScore is 0.0 or the winner is the original URL fallback, the engine logs a routing_fallback telemetry event and returns the original URL. This happens when all real candidates score zero - typically because the creator has no declared network accounts and the original URL is the only candidate.
Trust score emergency threshold: If the winning merchant's trust score is < 30 (emergency threshold), the engine forces a hard fallback to the original URL and shows a CRC warning: "This merchant's trust score is critically low. Routing is paused to protect your attribution."
Step 7: Redirect URL Construction
The final redirect URL is assembled from the winning candidate:
1. Start with the target storefront
The storefront hostname for the winning candidate's region. For Amazon GB, this is amazon.co.uk.
2. Reconstruct the product path
The product path is not the original URL's path verbatim (which may contain session tokens, ad referrers, or stale params). Instead, it is reconstructed from the extracted product ID:
- Amazon:
/dp/{ASIN}- extracted via regex\/dp\/([A-Z0-9]{10})/ - Flipkart:
/product/p/{productId}- extracted via regex - eBay:
/itm/{itemId}- extracted from path oritmquery param - Generic: the path is sanitized using the merchant's
canonicalizationrules from the profile
3. Inject affiliate tracking parameters
The creator's affiliate credentials for the winning network are injected as query parameters:
| Network | Parameter injected |
|---|---|
| CJ | cjevent={creatorCJClickId} |
| Awin | awc={creatorAwinId} |
| Impact | clickid={creatorImpactId}&irgwc=1 |
| Admitad | admitad_uid={creatorAdmitadId} |
| Cuelinks | cued={creatorCuelinksCued} |
| Rakuten | ranMID={merchantRanMID}&ranEAID={creatorEAID}&ranSiteID={creatorSiteID} |
| Involve Asia | aff_id={creatorInvolveAffId}&offer_id={merchantOfferId} |
| Amazon Associates | tag={creatorAmazonTag} |
For most direct programs (Flipkart, Noon, Jumia), the creator's affiliate tag is already embedded in the URL by their dashboard - Fyralynx preserves it via the preserveParams list. For Amazon Associates, Fyralynx injects the creator's own Associates tag directly into the canonical Amazon URL.
4. Strip unwanted parameters
Parameters in the merchant's stripParams list are removed. Universal strip list (applied to all merchants):
gclid,fbclid,msclkid,ttclid,twclid(ad network click IDs)sessionid,sessid(session tokens)ref,source(generic referrer junk, unless inpreserveParams)
5. Inject UTM parameters
If utmPreservation: true for this merchant and the link is not in passthrough mode, UTM parameters from the creator's active preset are appended:
&utm_source=youtube&utm_medium=video&utm_campaign=channelIf utmPreservation: false (the merchant's app deep link strips UTMs), UTMs are applied only to the web fallback URL, not the deep link. A CRC note is shown.
6. Final validation
The constructed URL is validated:
- Must be a valid HTTPS URL.
- Hostname must match the target storefront.
- Must not contain any Fyralynx internal parameters.
Step 8: Background Tasks (Non-blocking)
After serving the 302 redirect, the edge worker enqueues background tasks via waitUntil:
Deep-link re-probe
If the winning candidate used a deep-link pattern and its cache entry has expired, the deep-link validator is re-probed asynchronously:
typescript
async function probeDeepLink(pattern: string, env: WorkersEnv): Promise<void> {
const testUrl = pattern.replace(/{[^}]+}/g, 'test123');
const resp = await fetch(testUrl, {
method: 'HEAD',
redirect: 'manual',
signal: AbortSignal.timeout(3000),
});
const valid = resp.status >= 200 && resp.status < 400;
await env.FYRALYNX_CONFIG.put(
`dl:${pattern}`,
JSON.stringify({ valid, probedAt: Date.now() }),
{ expirationTtl: valid ? 259200 : 86400 } // 72h valid, 24h invalid
);
}The probe uses HEAD (not GET) to minimise bandwidth and avoid triggering analytics pixels. A 3-second timeout prevents the background task from running indefinitely.
Telemetry emit (opt-in only)
If the creator has opted in to server-side analytics, a minimal telemetry event is emitted:
typescript
{
merchantId: "amazon",
networkId: "Impact",
region: "US",
country: "NG", // visitor country
device: "android",
normalizedEpcUsd: 0.037,
confidence: 87,
deepLinkUsed: false,
redirectDepth: 1,
timestamp: 1747660800000,
// No creator ID - only a rotating anonymous session token
}Zero PII is included. The creatorId is never present in telemetry. The anonymous session token rotates every 24 hours.
Routing Presets
Three presets adjust the weighting of signals in the EPC formula without changing the formula's structure:
| Preset | Description | Key weight adjustments |
|---|---|---|
auto | Default/balanced - equal weight across all signals | Baseline weights (1.0×) |
link-in-bio | Mobile-first - lower redirect tolerance, faster geo-fallback | device_bias ×1.5, link_health_penalty ×1.3, region_match ×1.2 |
long-form | EPC-aggressive - prioritises deep-link bonus and raw EPC | deeplink_bonus ×1.4, epc_base ×1.2, link_health_penalty ×0.8 |
The preset auto-suggest feature in the extension analyses contextual signals (current tab URL, referrer, User-Agent) and suggests the most appropriate preset. This suggestion is advisory only - the creator always has final control.
The Candidate Filter (Creator Network Validation)
The candidate filter is the routing engine's single hardest invariant. Before scoring, every candidate passes through:
typescript
function filterCandidates(
candidates: Candidate[],
creatorNetworkAccounts: NetworkAccount[],
): Candidate[] {
const declaredNetworkIds = new Set(
creatorNetworkAccounts
.filter(a => a.status === 'active')
.map(a => a.networkId)
);
return candidates.filter(c =>
c.networkId === null // original URL fallback - always allowed
|| declaredNetworkIds.has(c.networkId) // declared + active network
|| isSupportedDirectProgram(c.merchantId, c.networkId) // direct program
);
}A candidate is included only if:
- It is the original URL fallback (no network required).
- The creator has an active declared account for the network.
- It is a direct program (Amazon Associates, Flipkart, Noon, Jumia) that the creator has declared.
If all network candidates are filtered out (the creator has no declared accounts), the original URL is returned with a CRC advisory: "No network accounts declared. Add your affiliate network accounts in Settings to enable intelligent routing."
Fallback Chain
The engine never fails to serve a redirect. The complete fallback chain:
| Condition | Action |
|---|---|
| Routing succeeds | HTTP 302 → optimised URL |
| Merchant not found in storefronts | HTTP 302 → original URL unchanged |
| All candidates filtered (no declared networks) | HTTP 302 → original URL + CRC advisory |
| KV read failure | HTTP 302 → original URL unchanged |
| Trust score < 30 (emergency) | HTTP 302 → original URL + CRC critical warning |
| Passthrough flag set | HTTP 302 → original URL unchanged (click still counted) |
| Any uncaught error | HTTP 302 → original URL unchanged |
A failed redirect that drops the user is never acceptable. Commission loss is preferable to user experience failure.
SLOs
| Metric | Target |
|---|---|
| P50 routing decision time | < 40 ms |
| P95 routing decision time | < 150 ms |
| P99 routing decision time | < 400 ms |
| Worker error rate | < 0.05% |
| Monthly availability | ≥ 99.9% |
| Fallback success rate | 100% |
These are measured via Cloudflare Analytics Engine and tracked on the Fyralynx admin dashboard.
Complete Pseudocode Reference
The full edge routing handler (from blueprint Section 10.3):
typescript
async function handleClick(request: Request, env: WorkersEnv): Promise<Response> {
const params = new URL(request.url).searchParams;
const originalUrl = params.get('url');
const creatorId = params.get('cid') ?? null;
if (!originalUrl) return Response.redirect('/', 302);
// Passthrough check
const linkMeta = creatorId
? await env.FYRALYNX_LINKS.get(linkKey(creatorId, originalUrl), { type: 'json' })
: null;
if (linkMeta?.passthrough === true) {
env.ctx.waitUntil(emitTelemetry(env, { type: 'passthrough', creatorId, url: originalUrl }));
return Response.redirect(originalUrl, 302);
}
// Context resolution
const visitorCountry = request.cf?.country ?? params.get('country') ?? 'UNKNOWN';
const deviceType = detectDevice(request.headers.get('user-agent') ?? '');
// Parallel KV reads
const [storefronts, fxRates, deepLinkCache, creatorProfile] = await Promise.all([
env.FYRALYNX_CONFIG.get('storefronts', { type: 'json' }),
env.FYRALYNX_CONFIG.get('fx_rates', { type: 'json' }),
env.FYRALYNX_CONFIG.get('deep_link_cache', { type: 'json' }),
creatorId
? env.FYRALYNX_CONFIG.get(`creator:${creatorId}`, { type: 'json' })
: Promise.resolve(null),
]);
// Merchant identification
const hostname = new URL(originalUrl).hostname.replace(/^www\./, '');
const merchantInfo = storefronts?.[hostname] ?? null;
if (!merchantInfo) return Response.redirect(originalUrl, 302);
const merchantProfile = await env.FYRALYNX_CONFIG.get(
`merchant:${merchantInfo.merchantId}`, { type: 'json' }
);
if (!merchantProfile) return Response.redirect(originalUrl, 302);
// Candidate generation
const candidates = generateCandidates(
originalUrl, merchantInfo, merchantProfile,
visitorCountry, creatorProfile?.networkAccounts ?? [],
creatorProfile?.preferences ?? {}
);
// Scoring
const scoredCandidates = candidates.map(candidate => ({
candidate,
score: computeRouteScore(buildEpcInputs(
candidate, merchantProfile, fxRates, deepLinkCache,
deviceType, visitorCountry, creatorProfile
)),
}));
// Selection
scoredCandidates.sort((a, b) => b.score.finalScore - a.score.finalScore);
const winner = scoredCandidates[0]!;
// URL construction
const redirectUrl = buildRedirectUrl(winner.candidate, originalUrl, creatorProfile);
// Background tasks
if (winner.candidate.deepLinkPattern && isDeepLinkTtlExpired(deepLinkCache, winner.candidate)) {
env.ctx.waitUntil(probeDeepLink(winner.candidate.deepLinkPattern, env));
}
if (creatorProfile?.analyticsOptIn) {
env.ctx.waitUntil(emitTelemetry(env, { ...winner.score, visitorCountry, deviceType }));
}
return Response.redirect(redirectUrl, 302);
}See Also
- EPC Model - Exhaustive Specification - all 16 signals, formula, confidence
- Choice Pages - presenting multiple merchant options
- WordPress Plugin - auto-monetise CMS content