Full fraud-prevention suite for Vendure. Every placed order is risk-scored server-side the moment it lands — no storefront integration required — and what happens next is up to your per-channel policy: log it, hold it for manual review, or hold it and tell the customer it's being verified.
Plugin page & pricing: https://huloglobal.com/vendure-plugins/fraud-prevention/
Each fired signal adds weighted points (all weights overridable per channel):
| Signal | Default points |
|---|---|
| Blocklisted email / domain | 60 / 45 |
| Blocklisted IP / IP in CIDR range | 55 |
| Disposable email domain | 50 |
| Failed payments from IP (1h) | 45 |
| IP order velocity (hour / day) | 40 / 30 |
| High-risk country | 40 |
| Card AVS: postcode mismatch (issuer verdict) | 35 |
| Email order velocity (24h) | 35 |
| Email daily value ceiling | 30 |
| Order value ceiling | 25 |
| Card AVS: street address mismatch (issuer verdict) | 20 |
| First order + high value | 18 |
Plus-addressed email (x+7@gmail) | 12 |
| Billing / shipping postcode differ (typed) | 8 |
Emails are canonicalised before velocity counting — x+1@gmail.com,
x+2@gmail.com and x.y@gmail.com all count as one identity. Allowlisted
identities bypass everything.
AVS (Address Verification Service) is the card issuer's own verdict on the
billing postcode and street number the customer typed — the strongest
address signal there is, because it comes from the bank rather than the
customer. Only an explicit fail scores; unavailable / unchecked
(issuer doesn't support it, or the field wasn't sent) is silent.
Where the verdict comes from, in order:
avsResolver plugin option — any gateway. Called once per placed
order with the Vendure Order (payments loaded) and the request
context; return { postalCode, line1, source } using
pass | fail | unavailable | unchecked, or null when unknown.
Payment metadata — a custom payment handler can store the result on
the Payment as { avs: { postalCode: 'fail', line1: 'pass' } }
(Stripe-style checks: { address_postal_code_check, address_line1_check }
and flat keys are recognised too; Y/N/U, match/no_match and booleans
are all understood).
Stripe, automatically — for orders paid through Vendure's
StripePlugin the PaymentIntent is fetched with expand[]=latest_charge
using the payment method's own API key and
charge.payment_method_details.card.checks is read. One GET per placed
order, 5 s timeout, fails open. Switch it off per channel with
Check card AVS with Stripe under Rules → Signals.
Stripe only runs AVS when your checkout sends the billing address.
The Payment Element does not collect the street address by itself, and
many integrations never pass a postcode either — in which case both
checks come back null and nothing scores. Send the order's billing
address in confirmPayment
(confirmParams.payment_method_data.billing_details.address, with the
element created using fields: { billingDetails: { address: 'never' } })
and the issuer verifies postcode and street number on every card
payment. Wallet payments (Apple Pay, Google Pay, Link) are verified by
the wallet provider instead and carry no AVS checks.
A typed billing vs shipping postcode difference (same country, both present) is scored separately and weakly — gifts and office deliveries do this legitimately, but it compounds with the other signals. All three weights are overridable per channel like every other signal.
Daily sync (03:00) into the blocklist: FireHOL Level 1, Spamhaus DROP (CIDR ranges are matched properly), Tor exit nodes, and the community disposable-email-domains list. Manual allow/block entries ride on top.
Admin UI (multi-tab dashboard: Overview, Rules, Review queue, Lists, Simulate, Activity, Settings):
The plugin marks orders as held; your fulfilment path asks before shipping:
POST /fraud-prevention/check { email, orderValuePence, channelId } →
{ allowed, riskLevel } — rate-limited, minimal response shape by design.
Without a licence key the plugin runs in the free tier: monitor mode, manual lists and simulate. Enforce mode, review-queue holds, threat-feed sync and email alerts require a licence from https://huloglobal.com/vendure-plugins/fraud-prevention/.
Vendure >=3.5 <4. MariaDB/MySQL. Tables are created/migrated automatically
on boot; upgrading from the pre-plugin HULO implementation preserves all
existing config, log and list data.
AGPL-3.0-or-later — commercial licences available from HULO Global.
Pick monthly or annual on the plugin's admin banner and click Start 14-day free trial (card required, nothing charged until day 15, cancel any time), or choose lifetime — checkout opens in a new tab and the key installs itself within a minute. You can also buy at https://elite.charity/licence/buy/vendure-plugin-fraud-prevention and paste the emailed key into the admin.
All notable changes to @huloglobal/vendure-plugin-fraud-prevention are
documented here. The format follows
Keep a Changelog and this project
adheres to semantic versioning.
GET /fraud-prevention/log accepts signal=avs.GET /fraud-prevention/cases accepts signal=avs (any signal-key prefix).Stripe-Version header the request ran at the account's default version, and on accounts created before November 2022 expand[]=latest_charge is rejected with HTTP 400 — the lookup silently produced nothing. The GET is now pinned (read-only, so it never changes the account), and the unreachable charges.data fallback is gone.postcode_mismatch no longer fires when one postcode is a partial form of the other (5-digit ZIP vs ZIP+4, UK outward code vs full postcode).avs_postcode_fail (35) and avs_address_fail (20) fire when the card issuer's AVS verdict on the billing postcode / street address is an explicit fail; postcode_mismatch (8) fires when the typed billing and shipping postcodes differ within the same country. AVS verdicts come from a host avsResolver option (any gateway), from Payment.metadata (avs: { postalCode, line1 }, Stripe-style checks, or flat keys), or — for orders paid through Vendure's StripePlugin — automatically, by fetching the PaymentIntent's latest charge with the payment method's own API key. Fails open; unavailable / unchecked never score.avsLookup, default on).AvsCheck, AvsResult, avsFromMetadata, avsFromStripeCharge, fetchStripeAvs, normalisePostcode, parseAvsCheck, postcodesDiffer.confirmPayment → confirmParams.payment_method_data.billing_details — see README → Postcode / AVS. Wallet payments (Apple Pay, Google Pay, Link) carry no AVS checks.AssessInput gains billingPostalCode, shippingPostalCode and avs; the order guard fills them from the order's addresses and FraudPreventionService.resolveAvsForOrder().hulo-global-logo.svg is now the HG wordmark on the HULO black rounded square (the same mark as the huloglobal.com header), with explicit -light and -dark variants alongside the auto-switching default..env edit, no restart. Renewed subscription keys are picked up automatically too. New admin endpoints licence/purchase-link and licence/claim-status.@huloglobal/vendure-licence-sdk ^0.12.0.dbConnectionOptions use.
Verified against PostgreSQL 17. MySQL/MariaDB installs are unaffected
(byte-identical passthrough).# comments
ignored), typed as IP / CIDR range / email / email-domain. They sync
nightly alongside the built-ins and are matched identically (CIDR
included), with per-feed enable/disable, on-demand sync, and last
sync count / error surfaced. Licensed feature.GET/POST /fraud-prevention/feeds/custom,
POST /feeds/custom/:id (edit / {sync:true}), DELETE /feeds/custom/:id.POST /fraud-prevention/check returned Nest's default 201 for
a POST; it's a read-only risk check, so it now returns 200.@vendure/testing, real MariaDB dialect): admin
auth gating, public-check shape + rate limiting, and the assessment
engine (disposable email, order value, returning-customer trust credit,
blocklist incl. CIDR ranges, allowlist bypass, unlicensed enforce
downgrade). 11 e2e tests.[ngModel]="caseNotify(id)") which
Angular re-evaluated every change-detection pass. The per-case ticks
are now seeded from the global defaults when cases load and bound to
plain state. The Settings integrations list likewise iterates a stable
array instead of one rebuilt each cycle.@ escape was corrupted by the release tooling (sed &
back-reference) and still shipped a bare @ — admin builds kept
failing. Repaired and verified: no bare @ remains in any template
text node.@BotFather in the Telegram help text is now the @ entity.X-Hulo-Signature) — every held, approved,
rejected and auto-released case pings all configured channels. Each
transport fails independently.{{orderCode}}, {{firstName}}, {{supportEmail}} and
{{reviewHours}} variables, live preview and one-click reset. Bodies
are plain text (blank lines = paragraphs) so tone is editable without
HTML foot-guns. Defaults rewritten to be honest without being alarming
— a held order is "a quick security check", never an accusation, and a
rejection includes a human-appeal path.reviewHours promise surfaced in the templates.fraud_ip_intel,
fails open): VPN/proxy and datacentre-IP signals — the blockVpnProxy
toggle finally does something — plus IP vs billing-country mismatch.First release as a standalone plugin — a full rebuild of the fraud tooling that previously lived inside the HULO host project, with the piece that implementation never had: real enforcement.
OrderPlacedEvent is assessed — the
old /check endpoint required the storefront to call it, and nothing did.ipInCidr verification (unit-tested).pendingOrderIds()); approve releases + emails the customer, reject
cancels + emails. All decisions audited.fraud_config, fraud_log, fraud_blocked_orders, fraud_blocklist,
fraud_whitelist, fraud_notification_config) — upgrading preserves all
live data; new columns are added automatically.ReadCatalog / UpdateCatalog) — previously these endpoints were
mounted without any auth guard. The public /check endpoint is
rate-limited and returns a minimal shape (no signal internals to probe).