Signals Explained
7 min read
A signal is a single contribution to a trust score. Each detection module emits its own signal — sometimes multiple — for every scored customer. The sum of all signals, plus the base score of 50, plus the account-age bonus, equals the final trust score. This guide is the reference for every signal TrustLens emits: what it means, what triggers it, what magnitude it has, and what to do when you see it.
The Signal Object #
Every signal is recorded as a row in the {prefix}trustlens_signals table and shown in the Signal Breakdown panel on the Customer Detail page. The fields:
| Field | Type | Description |
|---|---|---|
module |
string | Module ID that emitted the signal — returns, orders, coupons, categories, chargebacks, linked_accounts, shipping_anomalies, card_testing, or account_age, plus system for insufficient-data notes |
score |
integer | Positive or negative contribution. Typical range -50 to +15. |
reason |
string | Human-readable explanation, often containing the specific number that triggered the signal |
Signals are re-computed and re-written on every recalculation. The signal log shows the current state, not history; historical movement is shown on the score trend chart.
Returns Module Signals #
The Returns module emits up to three concurrent signals per customer based on refund behavior. Configurable thresholds in Settings → Modules → Returns determine the tier boundaries.
Return Rate Tier #
The dominant signal. Mutually exclusive — only one of these fires per customer.
| Trigger | Score | Reason Text |
|---|---|---|
| Return rate ≥ critical threshold (default 60%) | -40 | “Very high return rate: 67%” |
| Return rate ≥ high threshold (default 40%) | -25 | “High return rate: 48%” |
| Return rate ≥ 25% | -10 | “Elevated return rate: 31%” |
| Return rate ≤ 5% with 5+ orders | +10 | “Excellent return history” |
Wardrobing (Full-Refund Ratio) #
Fires alongside the return rate signal when applicable.
| Trigger | Score | Reason Text |
|---|---|---|
| 3+ refunds and ≥90% are full refunds | -10 | “90%+ full refunds (wardrobing risk)” |
High Refund Value #
Penalty based on absolute lifetime refund value.
| Trigger | Score | Reason Text |
|---|---|---|
| Total refund value ≥ $2,000 | -10 | “High refund value: $2,340” |
| Total refund value ≥ $1,000 | -5 | (no reason text) |
Orders Module Signals #
Tracks completion patterns. The Orders module uses clean orders (total orders minus refunds) as the basis for loyalty bonuses.
| Trigger | Score | Reason Text |
|---|---|---|
| 10+ clean orders (total − refunds) | +15 | “10 orders without issues” |
| 5+ clean orders | +10 | “5 orders without issues” |
| 3+ clean orders | +5 | (no reason text) |
| Net customer value (total − refund value) ≥ $1,000 | +5 | “High customer value: $X” |
| 3+ cancellations and rate ≥ 50% | -15 | “High cancellation rate: 60%” |
| 3+ cancellations and rate ≥ 30% | -10 | “Elevated cancellation rate: 35%” |
The cancellation signals are gated by an absolute count of 3+ cancelled orders (not just a percentage) to suppress noise from low-volume customers.
Coupons Module Signals #
Catches coupon abuse patterns. The dominant signal is coupon-then-refund cycles — placing an order with a coupon, then refunding it.
| Trigger | Score | Reason Text |
|---|---|---|
| 3+ coupon-then-refund cycles | -25 | “3 coupon orders refunded (abuse pattern)” |
| 2 coupon-then-refund cycles | -15 | “2 coupon orders refunded” |
| 1 coupon-then-refund cycle | -5 | (no reason text) |
| first_order_coupons > 0 AND coupon_then_refund > 0 | -10 (additional) | “First-order coupon abuse pattern” |
| 5+ orders with coupon usage rate ≥ 80% | -10 | “High coupon usage: 85% of orders” |
| 3+ coupons used with 0 refund cycles | +5 | “Legitimate coupon user” |
The coupon-then-refund cycle is the strongest single signal of bad-faith abuse. The first-order-abuse penalty stacks on top of the cycle tier when both first_order_coupons and coupon_then_refund are non-zero — a customer with 3+ refund cycles where at least one used a first-order coupon sees -25 + -10 = -35 from this module alone.
Category-Aware Risk Signals #
Fires when a customer’s return rate concentrates in specific product categories. The module computes a per-category return rate and weights it against the category’s store-wide return rate.
| Trigger | Score | Reason Text |
|---|---|---|
| Category return rate ≥ 50% (any weight) | -15 to -20 | “50%+ returns in Outerwear” |
| Category return rate ≥ 30% with weight ≥ 1.5x store baseline | -10 to -15 | “Elevated category return rate (Footwear)” |
The weight factor is key: a 40% return rate in a category where store-wide returns are 35% is normal; a 40% return rate where store-wide is 8% is a specific behavioral pattern. The module only penalizes the latter.
Linked Accounts Signals #
Detects accounts sharing fingerprints — shipping address hash, billing address hash, phone number, IP, payment method fingerprint, or device user-agent fingerprint.
| Trigger | Score | Reason Text |
|---|---|---|
| Linked to 3+ other accounts | -30 | “Linked to 4 other accounts” |
| Linked to ≥1 risky/critical account | -25 | “Linked to 1 high-risk accounts” |
| Linked to 1–2 normal accounts | -5 to -10 | “Linked to 2 other accounts” |
The detection is one-way at signal time but bidirectional in the data — both accounts in a link see their counterpart in the linked_accounts list on the customer detail page.
Shipping Anomalies Signals #
| Trigger | Score | Reason Text |
|---|---|---|
| 3+ distinct shipping addresses in 30-day window | -10 | “Address hopping (4 addresses in 30 days)” |
| Billing/shipping country mismatch | -5 | “Country mismatch (US billing → DE shipping)” |
| Severe country mismatch (high-risk corridor) | -15 | “High-risk country corridor” |
| Address change velocity ≥ 1 per week sustained | -10 | “Rapid address changes” |
Pro adds diversity-trend detection and enhanced country-mismatch severity for more nuanced shipping-fraud signals.
Chargebacks Signals #
The strongest signals in the system. A single chargeback can move a customer from Trusted to Critical.
| Trigger | Score | Reason Text |
|---|---|---|
| 3+ lost disputes | -50 | “3 lost disputes” |
| 2 lost disputes | -40 | “2 lost disputes” |
| 1 lost dispute | -30 | “Dispute lost” |
| Pending dispute | -20 | “Active dispute” |
| 1+ disputes (any outcome) with recent activity | -5 to -15 | “Recent dispute history” |
| 10+ orders with clean chargeback history | +10 | “Clean chargeback history” |
Won disputes (“dispute reversed in our favor”) count significantly less than lost ones — there’s still a small penalty because the dispute was filed, but the magnitude is much lower than a lost dispute.
Card-Testing Signals #
Fires when a customer’s email is associated with a device fingerprint that has been involved in card-testing velocity events.
| Trigger | Typical Score | Reason Text |
|---|---|---|
| Customer fingerprint matched recent attack | -20 to -40 | “Fingerprint tied to recent card-testing attack” |
| Customer fingerprint matched historical attack | -10 | “Historical card-testing fingerprint match” |
This is the bridge between checkout-time velocity detection and the customer trust score: if a fingerprint that hammered your gateway with stolen-card attempts later completes a successful order, that customer inherits the signal.
Account Age Bonus #
Not strictly a module signal — added after module signals are collected. Mutually exclusive tiers.
| Tenure | Score | Reason Text |
|---|---|---|
| 1+ year (365+ days since first order) | +15 | “Long-term customer (1+ year)” |
| 6+ months (180+ days) | +10 | “Established customer (6+ months)” |
| 3+ months (90+ days) | +5 | “Regular customer (3+ months)” |
| < 3 months | 0 | (no signal recorded) |
See Account Age Loyalty Bonus for the full design.
System Signals #
The Returns/Orders/Coupons/etc. modules sometimes return a zero-score signal that exists only to convey state — typically “not enough data to evaluate.” TrustLens drops these unless they have a reason text.
| Trigger | Score | Reason Text |
|---|---|---|
| Customer below minimum-orders threshold | 0 | “Insufficient data (1/3 orders)” |
| Module disabled in settings | — | (module returns nothing; no signal recorded) |
| Allowlisted | — | (short-circuit; no signals computed) |
Reading the Signal Breakdown #
The Signal Breakdown panel on the Customer Detail page is a complete audit of how the score was calculated. To verify a score manually:
- Open the customer’s profile
- Note every signal in the breakdown with its score and reason
- Sum all signal scores (positive and negative)
- Add 50 (base score)
- Clamp to 0–100 if needed
- The result should match the displayed score exactly
If the numbers don’t reconcile, the customer is either currently being recalculated (a stale UI displaying the old score with new signals) or there’s a custom filter modifying the score via trustlens/trust_score.
Signal Strength Comparisons #
To build intuition for signal magnitudes:
| Comparison | Magnitudes |
|---|---|
| “Worst” single negative | 3+ lost disputes: -50 |
| “Worst” common negative | Very high return rate: -40 |
| “Best” single positive | 20+ loyal orders: +15; 1+ year tenure: +15 |
| Typical first negative signal | Elevated return rate (25%): -10 |
| Typical first positive signal | 5+ orders: +5 |
| Maximum theoretical sum of positives | ~+50 (loyalty + clean history + age bonus + low return rate) |
| Maximum theoretical sum of negatives | Unbounded below 0 (clamped) — compound signals can stack arbitrarily |
Asymmetry: positive signals are bounded, negative signals are unbounded (clamped at 0). This is intentional — a customer with multiple severe abuse patterns should read as Critical, not just “below average.”
Filtering Signals Programmatically #
Developers can modify the signals list before the final sum using the trustlens/score_signals filter:
add_filter( 'trustlens/score_signals', function( $signals, $email_hash ) {
// Add a custom signal from external data
$external_score = my_loyalty_program_score( $email_hash );
if ( $external_score > 0 ) {
$signals[] = array(
'module' => 'loyalty_program',
'score' => min( 10, $external_score ),
'reason' => 'Loyalty program member',
);
}
return $signals;
}, 10, 2 );
The custom signal will appear in the signal breakdown alongside built-in ones, with whatever module ID you provide.