Chain Signals and Prefetch
Use this guide to understand how chain signals, rule evaluation, and prefetch work together to enable cross-event personalization.
When to use
- You want to personalize a visitor's experience based on their behavioral history across multiple page views.
- You need to understand how signal-based rules differ from static routing rules.
- You are integrating the chain signals API to drive dynamic content decisions.
Concept overview
Chain signals extend the platform's personalization beyond single-click routing. Instead of deciding the redirect destination at click time only, chain signals let you:
- Collect behavioral signals as a visitor interacts with your content.
- Accumulate a behavioral profile in a time-windowed signal ledger.
- Evaluate rules that react to accumulated signals rather than just current-click context.
- Prefetch decisions for multiple trigger events in a single request.
This enables scenarios like: "If the visitor viewed the pricing page and spent 30 seconds on the features page, show the enterprise CTA on their next visit."
Signal model
A signal is a discrete behavioral observation about a visitor within a session:
| Field | Type | Description |
|---|---|---|
signalKey | String | Category of the observation (e.g., intent, category, conversion) |
signalValue | String (max 256 chars) | Observed value (e.g., rings, pricing-viewed) |
confidence | Number (0.0–1.0) | Certainty of the observation |
source | String (optional) | Where the signal came from |
metadata | Object (optional) | Additional context (page URL, click ID) |
Signals are stored in the Signal Ledger — an append-only, time-windowed DynamoDB table with automatic TTL cleanup.
Session identity
All signals within a session share a sessionId with the format:
sig_{handle}_{timestamp}_{randomId}
A session can accumulate up to 200 signals. Signal write rate is limited to 60 signals per session per minute (120 per IP per minute for public endpoints).
Consent and retention
| Consent mode | Signal retention | Description |
|---|---|---|
full | 30 days | Full cross-session behavioral tracking |
basic | 24 hours | Short-term tracking only |
denied | None | No signals stored |
Chain rules
A chain rule defines when accumulated signals should trigger an action. Rules are evaluated in priority order (lower number = higher priority), and the first match wins.
Rule structure
{
"ruleId": "early-bird-expired",
"enabled": true,
"priority": 100,
"triggerEvent": "early_bird_deadline",
"when": {
"countryIn": ["US", "GB"],
"deviceIn": ["MOBILE", "DESKTOP"]
},
"chainConditions": {
"require": [
{ "signalKey": "early.bird.deadline", "operator": "equals", "signalValue": "expired" }
],
"exclude": [
{ "signalKey": "already_purchased", "operator": "exists" }
],
"minChainDepth": 1,
"maxChainAgeMinutes": 1440
},
"action": {
"type": "redirect",
"destinationUrl": "https://example.com/regular-pricing"
}
}
Condition operators
| Operator | Semantics | Example |
|---|---|---|
exists | Signal key has any value | Signal "intent" has been recorded |
not_exists | Signal key has no values | No "conversion" signal yet |
equals | Exact value match (case-insensitive) | Intent equals "rings" |
contains | Substring match (case-insensitive) | Preference contains "diamond" |
count_gte | At least N signals with this key | At least 2 "category" signals |
any_of | Value matches any in a list | Intent is "rings" or "watches" |
Window scoping
Conditions can optionally be scoped:
| Parameter | Effect |
|---|---|
withinLastN | Only consider the N most recent signals |
minConfidence | Only consider signals with confidence >= threshold |
Chain constraints
| Parameter | Effect |
|---|---|
minChainDepth | Require at least N signals in the session |
maxChainAgeMinutes | Reject if the first signal is older than N minutes |
Rule evaluation flow
1. Load all enabled rules for handle + triggerEvent
2. Sort by priority (ascending), then by ruleId
3. For each rule:
a. Check basic conditions (device, country, source, campaign)
b. Check chain depth constraint
c. Check chain age constraint
d. Evaluate ALL "require" conditions (must all match)
e. Evaluate ALL "exclude" conditions (must all NOT match)
f. First rule where all checks pass → return its action
4. If no rule matches → return { matched: false }
Action types
When a rule matches, it returns an action for the client to execute:
| Action type | Description |
|---|---|
redirect | Navigate visitor to a new URL |
show_popup | Display a popup/modal (client handles rendering) |
show_banner | Display a banner (client handles rendering) |
swap_cta | Replace a CTA element (client handles rendering) |
fire_event | Trigger an analytics event |
add_to_cart | Add an item to cart (client handles implementation) |
no_action | Explicitly do nothing (useful as a catch-all) |
Actions support variable interpolation: signal values can be inserted into action fields using {{signalKey}} syntax.
Prefetch
Prefetch lets you pre-compute decisions for multiple trigger events in a single request. This eliminates round-trips when the client needs to know what to do for several possible events.
POST /v2/public/chain/prefetch
{
"handle": "myhandle",
"sessionId": "sig_myhandle_1234_abc",
"triggerEvents": ["exit_intent", "idle_30s", "add_to_cart"],
"currentContext": {
"device": "mobile",
"country": "US",
"source": "email",
"pageUrl": "https://example.com/product"
}
}
Response:
{
"decisions": {
"exit_intent": { "matched": true, "ruleId": "offer_popup", "action": { ... } },
"idle_30s": { "matched": false, "ruleId": null, "action": null },
"add_to_cart": { "matched": true, "ruleId": "cross_sell", "action": { ... } }
},
"validUntil": "2026-02-25T12:35:56.789Z"
}
Prefetch results are cached server-side for 60 seconds per session + trigger event + context hash. Up to 10 trigger events per prefetch request.
Segment token flow
When signals include segment hints (via metadata.intentSegments), the server:
- Validates segment labels (regex:
^[a-z0-9][a-z0-9_-]{0,29}$, max 5 segments). - Signs them into an HMAC token.
- Returns a
segmentTokenin the response. - The client stores the token as the
lp_segcookie.
This token is then available to personalization rules that use segmentIn conditions during redirects.
Required auth
Public endpoints (no auth required)
| Endpoint | Rate limit |
|---|---|
POST /v2/public/chain/signals | 120/IP/min |
POST /v2/public/chain/resolve | 60/IP/min |
POST /v2/public/chain/prefetch | 20/IP/min |
Handle-scoped endpoints (JWT or PAT)
| Endpoint | Auth | Rate limit |
|---|---|---|
POST /v2/handles/{handle}/chain/signals | CREATOR+ or PAT chain.signal.write | 60/session/min |
POST /v2/handles/{handle}/chain/resolve | CREATOR+ or PAT chain.resolve.read | 30/session/min |
POST /v2/handles/{handle}/chain/prefetch | CREATOR+ or PAT chain.resolve.read | 10/session/min |
GET /v2/handles/{handle}/chain/{sessionId} | CREATOR+ or PAT chain.read | 10/handle/min |
Limits
| Resource | Limit |
|---|---|
| Chain rules per handle | 100 |
| Signals per write request | 5 |
| Signals per session | 200 |
| Signal value length | 256 characters |
| Trigger events per prefetch | 10 |
| Decision cache TTL | 60 seconds |
Common errors
| Code | Error | Cause |
|---|---|---|
| 400 | invalid_session_id | Session ID does not match expected format |
| 400 | invalid_signal_key | Signal key contains invalid characters |
| 429 | rate_limited | Signal or resolve rate limit exceeded |
Related: