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 StreamDescription
AUTHORIZATIONCard transaction authorization requests
THREE_DS_AUTHENTICATION3DS authentication requests
TOKENIZATIONDigital wallet and merchant tokenization requests
ACH_CREDIT_RECEIPTIncoming ACH credit transactions
ACH_DEBIT_RECEIPTIncoming 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:

  1. Write: You define a rule() function in TypeScript that accepts typed feature inputs and returns an array of actions
  2. 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 PENDING state during compilation.
  3. Shadow: Once compilation succeeds, the draft moves to SHADOWING state and begins evaluating against live events without affecting outcomes
  4. 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 TypeTypeScript TypeAvailable On
AUTHORIZATIONAuthorizationAUTHORIZATION
AUTHENTICATIONAuthenticationTHREE_DS_AUTHENTICATION
TOKENIZATIONTokenizationTOKENIZATION
ACH_RECEIPTACHReceiptACH_CREDIT_RECEIPT, ACH_DEBIT_RECEIPT
CARDCardAUTHORIZATION, THREE_DS_AUTHENTICATION
ACCOUNT_HOLDERAccountHolderAUTHORIZATION, THREE_DS_AUTHENTICATION
IP_METADATAIpMetadataTHREE_DS_AUTHENTICATION
SPEND_VELOCITYSpendVelocityOutputAUTHORIZATION

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_VELOCITY feature type provides access to spend aggregations over configurable time periods. When declaring a SPEND_VELOCITY feature, you must also specify the scope, period, and optional filters parameters. 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 StreamAvailable ActionsDescription
AUTHORIZATIONAction.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_AUTHENTICATIONAction.Decline(code, explanation?)Decline the authentication
Action.Challenge(explanation?)Trigger a 3DS challenge
TOKENIZATIONAction.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_RECEIPTAction.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:

  1. Risk Scoring (Authorization) - assign risk points based on multiple signals and decline when the cumulative score exceeds a threshold
  2. Fuzzy Matching (3DS Authentication) - use an external fuzzy matching library to compare cardholder names during 3DS authentication
  3. Spend Tiers (Authorization) - use a SPEND_VELOCITY feature (configured with CARD scope and DAY period) 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/await is 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