Custom Code Rules
Write TypeScript authorization logic that compiles to WebAssembly and runs in Lithic's Rules Engine with deterministic execution for backtesting.
Custom Code Rules is a premium feature included in the Lithic Fraud Command suite. To enable Fraud Command and access this feature, contact your Customer Success Manager.
Overview
Custom Code Rules (TYPESCRIPT_CODE rule type) let you write custom logic in TypeScript that runs directly inside Lithic's Rules Engine. Unlike CONDITIONAL_ACTION rules, which evaluate a fixed set of attribute conditions, Custom Code Rules give you the full expressiveness of a programming language: arithmetic on multiple features, string manipulation, pattern matching, multi-factor scoring, and any other logic you can express in TypeScript.
Your TypeScript code is executed in a sandboxed environment with deterministic inputs and the execution is fully reproducible. This means Custom Code Rules benefit from the same shadow mode testing, backtesting, and performance reporting available to all other rule types.
Custom Code Rules are available on the following event streams:
| Event Stream | Description |
|---|---|
AUTHORIZATION | Card transaction authorization requests |
THREE_DS_AUTHENTICATION | 3DS authentication requests |
TOKENIZATION | Digital wallet and merchant tokenization requests |
ACH_CREDIT_RECEIPT | Incoming ACH credit transactions |
ACH_DEBIT_RECEIPT | Incoming ACH debit transactions |
When to Use Custom Code Rules
Use CONDITIONAL_ACTION rules when your logic can be expressed as a set of attribute checks (e.g., block transactions from specific countries or merchant categories). Use VELOCITY_LIMIT rules for straightforward spend or count limits over time periods.
Use Custom Code Rules when your logic requires:
- Arithmetic across multiple signals: scoring models that combine weighted risk factors
- String manipulation: fuzzy matching on merchant descriptors or cardholder names
- Conditional thresholds: different limits based on card metadata, account attributes, or computed values
- Complex branching: logic that cannot be reduced to AND/OR conditions on a flat set of attributes
- External libraries: timezone conversions, pattern matching, or other utility functions
How Custom Code Rules Work
At a high level, the lifecycle of a custom code rule is:
- Write: You define a
rule()function in TypeScript that accepts typed feature inputs and returns an array of actions - Compile: When you create or update a draft, Lithic validates your Typescript code and wraps it in a WebAssembly module. This process may take up to 10 seconds. The draft is a
PENDINGstate during compilation. - Shadow: Once compilation succeeds, the draft moves to
SHADOWINGstate and begins evaluating against live events without affecting outcomes - Promote: After validating shadow results, you promote the draft to make it active
%%{init: {
"theme": "base",
"themeVariables": {
"primaryColor": "#FF6600",
"primaryTextColor": "#FFFFFF",
"primaryBorderColor": "#993D00",
"secondaryColor": "#00CC88",
"tertiaryColor": "#9944FF",
"background": "#FFFFFF",
"mainBkg": "#FF6600",
"secondBkg": "#00CC88",
"lineColor": "#000000",
"textColor": "#000000"
}
}}%%
flowchart LR
A[Write TypeScript] --> B[Create Draft]
B --> C[PENDING]
C --> D[SHADOWING]
D --> E[Promote]
E --> F[ACTIVE]
C -->|Compilation<br/>Error| G[ERROR]
G -->|Fix & Redraft| B
If compilation fails, the draft enters an ERROR state with a diagnostic message describing the issue. You can fix the code and create a new draft to try again.
The easiest way to create and manage Custom Code Rules is through the built-in code editor in the Lithic Dashboard, which provides syntax highlighting, inline type errors, and one-click promotion. Custom Code Rules are also available through the Auth Rules API — pass your TypeScript source in the code parameter and declare inputs in features.
For details on the full rule management lifecycle — including shadow mode, promotion, and rollback — see Authorization Rules.
Writing a Custom Code Rule
The Rule Function
Every custom code rule must define a rule() function. This function receives features as positional arguments and returns an array of actions. If the function returns an empty array, the rule takes no action on the event.
import { Authorization, AuthorizationAction as Action } from './types';
function rule(authorization: Authorization): Action[] {
if (authorization.amount > 100_00 && authorization.merchant.country !== 'USA') {
return [Action.Decline("UNAUTHORIZED", "High-value foreign transaction")];
}
return [];
}The './types' module is auto-generated by Lithic and contains TypeScript type definitions for all available features and actions. You do not need to create this file. It is bundled automatically during compilation.
Features (Inputs)
Features are the data inputs available to your rule at evaluation time. You declare which features your rule needs when creating it, and they are passed as positional arguments to the rule() function in the order you declare them.
Each feature has a name (which becomes the parameter name in your function signature) and a type (which determines what data it contains). The available feature types depend on the event stream:
| Feature Type | TypeScript Type | Available On |
|---|---|---|
AUTHORIZATION | Authorization | AUTHORIZATION |
AUTHENTICATION | Authentication | THREE_DS_AUTHENTICATION |
TOKENIZATION | Tokenization | TOKENIZATION |
ACH_RECEIPT | ACHReceipt | ACH_CREDIT_RECEIPT, ACH_DEBIT_RECEIPT |
CARD | Card | AUTHORIZATION, THREE_DS_AUTHENTICATION |
ACCOUNT_HOLDER | AccountHolder | AUTHORIZATION, THREE_DS_AUTHENTICATION |
IP_METADATA | IpMetadata | THREE_DS_AUTHENTICATION |
SPEND_VELOCITY | SpendVelocityOutput | AUTHORIZATION |
A rule can declare multiple features. For example, a rule that needs both the authorization event and the card data would declare two features:
import { Authorization, Card, AuthorizationAction as Action } from './types';
function rule(authorization: Authorization, card: Card): Action[] {
// Access both authorization event data and card attributes
const isHighRisk = authorization.amount > 50_00 && card.state === 'OPEN';
// ...
return [];
}The
SPEND_VELOCITYfeature type provides access to spend aggregations over configurable time periods. When declaring aSPEND_VELOCITYfeature, you must also specify thescope,period, and optionalfiltersparameters. See the API specification for the full configuration options.
Actions (Outputs)
The rule() function returns an array of action objects. The available actions depend on the event stream:
| Event Stream | Available Actions | Description |
|---|---|---|
AUTHORIZATION | Action.Decline(code, explanation?) | Decline the transaction with a result code |
Action.Challenge(explanation?) | Trigger an SMS step-up challenge. See Authorization Challenges | |
Action.PartialApprove(approved_amount) | Approve a partial amount | |
THREE_DS_AUTHENTICATION | Action.Decline(code, explanation?) | Decline the authentication |
Action.Challenge(explanation?) | Trigger a 3DS challenge | |
TOKENIZATION | Action.Approve(explanation?) | Approve the tokenization |
Action.RequireTfa(reason?, explanation?) | Require two-factor authentication | |
Action.Decline(reason?, explanation?) | Decline the tokenization | |
ACH_CREDIT_RECEIPT / ACH_DEBIT_RECEIPT | Action.Approve(explanation?) | Approve the ACH transaction |
Action.Return(code, explanation?) | Return the ACH transaction with a NACHA return code |
The code parameter on Decline and Return actions is a typed enum (e.g., AuthorizationDeclineCode, ACHReturnCode), not a free-form string. Your code must use a valid enum value or it will fail type checking during compilation. See the API specification for the full list of valid codes per event stream.
The optional explanation string on each action is recorded alongside rule results for auditing purposes. Use it to describe the reason for the action.
import { Authorization, AuthorizationAction as Action } from './types';
function rule(authorization: Authorization): Action[] {
if (authorization.merchant.mcc === '7995') {
return [Action.Decline("UNAUTHORIZED", "Gambling merchant blocked")];
}
return [];
}Importing Libraries
Custom Code Rules support URL imports from esm.sh CDN. This lets you use any open-source TypeScript or JavaScript library without a local build step. Lithic handles bundling and compilation behind the scenes. All URL imports are resolved and bundled into a single module during compilation. Type definitions are fetched automatically from CDN type headers, so full type checking is available for imported libraries.
import { toZonedTime } from 'https://esm.sh/[email protected]';
import { getDay, getHours } from 'https://esm.sh/[email protected]';
import { Authorization, AuthorizationAction as Action } from './types';
function rule(authorization: Authorization): Action[] {
const zonedDate = toZonedTime(authorization.created, 'America/New_York');
const isFriday = getDay(zonedDate) === 5;
const hour = getHours(zonedDate);
// Block transactions outside business hours on Fridays
if (isFriday && (hour < 9 || hour >= 17)) {
return [Action.Decline("UNAUTHORIZED", "Outside business hours")];
}
return [];
}Code Examples
See the following examples to get started:
- Risk Scoring (Authorization) - assign risk points based on multiple signals and decline when the cumulative score exceeds a threshold
- Fuzzy Matching (3DS Authentication) - use an external fuzzy matching library to compare cardholder names during 3DS authentication
- Spend Tiers (Authorization) - use a
SPEND_VELOCITYfeature (configured withCARDscope andDAYperiod) to enforce different daily limits based on card metadata
import { Authorization, Card, AuthorizationAction as Action } from './types';
function rule(authorization: Authorization, card: Card): Action[] {
let riskScore = 0;
// Foreign transaction
if (authorization.merchant.country !== 'USA') {
riskScore += 2;
}
// High amount relative to typical spend
if (authorization.amount > 500_00) {
riskScore += 3;
}
// Card-not-present transaction
if (authorization.pos.entry_mode === 'PAN_MANUAL_ENTRY') {
riskScore += 2;
}
// No liability shift from 3DS
if (authorization.cardholder_authentication?.liability_shift !== 'THREE_DS') {
riskScore += 3;
}
if (riskScore >= 8) {
return [Action.Decline("UNAUTHORIZED", `Risk score ${riskScore} exceeds threshold`)];
}
return [];
}import fuzz from 'https://esm.sh/fuzzball@2';
import {
Authentication,
AccountHolder,
AuthenticationAction as Action,
} from './types';
function rule(
authentication: Authentication,
account_holder: AccountHolder
): Action[] {
const cardholderName = authentication.cardholder.name;
const nameOnFile =
`${account_holder.first_name} ${account_holder.last_name}`.trim();
// Challenge if cardholder name is missing
if (!cardholderName) {
return [Action.Challenge("Cardholder name missing from 3DS request")];
}
// Challenge if name does not match using fuzzy comparison
const score = fuzz.token_sort_ratio(
cardholderName.toLowerCase(),
nameOnFile.toLowerCase()
);
if (score < 80) {
return [
Action.Challenge(
`Name mismatch: '${cardholderName}' vs '${nameOnFile}' (score: ${score})`
),
];
}
return [];
}import {
Authorization,
Card,
SpendVelocityOutput,
AuthorizationAction as Action,
} from './types';
function rule(
authorization: Authorization,
card: Card,
velocity: SpendVelocityOutput
): Action[] {
// Determine daily limit from card memo field
const tier = card.memo ?? 'standard';
const dailyLimitCents: Record<string, number> = {
executive: 10_000_00,
manager: 5_000_00,
standard: 1_000_00,
};
const limit = dailyLimitCents[tier] ?? dailyLimitCents['standard'];
const projectedSpend = velocity.amount + authorization.amount;
if (projectedSpend > limit) {
return [
Action.Decline(
"ACCOUNT_DAILY_SPEND_LIMIT_EXCEEDED",
`Projected daily spend ${projectedSpend} exceeds ${tier} limit ${limit}`
),
];
}
return [];
}Constraints and Limitations
- Execution time: Each rule must complete within 10 milliseconds. If a rule exceeds the time limit or throws an uncaught exception, it is treated as an evaluation error. In case of decisioning event streams that approve or decline, the transaction will be declined. The issue will be visible in the dashboard and also in the rule evaluation results.
- Bundle size: The total bundled size of your code and its dependencies must not exceed 1 MB. Rules that exceed this limit will fail to compile.
- No external calls: Rules cannot make network requests, access the filesystem, or reach any resources outside their declared features.
- Synchronous only: The
rule()function must be synchronous;async/awaitis not supported. - URL imports only: Only URL imports from esm.sh are supported;
npm install-style dependency management is not available. - Deterministic execution: All rule execution is fully deterministic. If your code reads the current time, it receives the event timestamp, not wall clock time. Random number generation is seeded from the event. This is what enables backtesting — the same rule always produces the same result when replayed against the same event.
Next Steps
- Review the Authorization Rules guide for the full rule lifecycle, including shadow mode, promotion, and performance reports
- See Backtesting Authorization Rules to learn how to test custom code rules against historical transactions
- Use the Lithic Dashboard to create and manage rules through a visual interface
- Consult the Auth Rules API specification for complete endpoint documentation and parameter details
- Learn about Authorization Intelligence for the broader vision of programmable decisioning across your payment stack
Updated about 3 hours ago
