Check Pipeline

An end-to-end remote deposit capture system that processes check images through an automated pipeline and produces X9.100-187 Image Cash Letter files.

Work in progress

Overview

A remote deposit capture (RDC) system that takes a photograph of a check and processes it through an automated pipeline, ultimately producing a valid X9.100-187 Image Cash Letter file for clearing against a receiving bank.

The system demonstrates the full lifecycle of financial check processing: image capture, quality validation, MICR line recognition, amount verification, duplicate detection, cash letter assembly, and submission to a receiving institution simulator. Each stage runs as an independent worker consuming from a message queue, allowing per-stage scaling and retry semantics.

All check images are synthetically generated — no real financial data is used anywhere in the project.

Architecture

The system uses an event-driven microservice architecture with SQS queues connecting independent processing stages:

React Frontend (Vite + TypeScript)
    |
    v
Gin REST API  (Go)
    |           \
    v            v
PostgreSQL    AWS S3 (KMS-encrypted images)
    ^
    |
SQS Queues → Stage Workers (IQA → MICR → CAR/LAR → Dupe → Cash Letter)
    |
    v
ICL Receiver (simulated clearing bank)
                

Event-driven stages. Each processing stage (image quality, MICR recognition, amount verification, etc.) runs as an independent worker process consuming from its own SQS queue. When a stage completes, it publishes to the next queue in the pipeline. This decoupling means stages can be scaled independently, retried without affecting other stages, and monitored individually via dead-letter queues.

Single worker binary. All stage workers compile into one Go binary (icl-worker) with subcommands per stage (e.g., icl-worker iqa, icl-worker micr). This simplifies the container image while still allowing Kubernetes to scale each stage independently by running different subcommands in separate pods.

LocalStack for development. The same code path targets LocalStack (local) and real AWS (production) via environment variables only. S3, SQS, and KMS all run locally in Docker Compose with zero AWS costs during development.

Processing Pipeline

Each check item progresses through a defined state machine. Every transition is recorded as an immutable, hash-chained audit entry:

captured → iqa_passed → micr_read → amount_read → dupe_checked → bundled → sent → posted | returned | rejected
                
Stage Purpose Status
Image Capture Accept front/back check images, upload to S3, create deposit record Complete
Image Quality Analysis Validate dimensions, brightness, contrast, skew, and corner content Complete
MICR Recognition Parse E-13B MICR line: routing number, account number, check number In progress
Amount Recognition CAR/LAR (courtesy and legal amount recognition) with confidence scoring Planned
Duplicate Detection Exact match and perceptual image hashing to catch resubmissions Planned
Cash Letter Assembly Bundle approved items into X9.100-187 files using moov-io/imagecashletter Planned
Receiving Simulator Validate incoming cash letters, simulate returns with X9.100-188 codes Planned

Items that fail any stage are routed to an exception queue for manual review. The exception handling UI lets operators inspect failed items, view failure reasons, and manually key corrected values.

API Design

The REST API manages deposits, items, exceptions, and cash letters:

Group Endpoints Purpose
/deposits POST / — create deposit with images
GET / — list deposits
GET /:id — deposit details with items
Capture sessions. Each deposit contains one or more check items with front/back image pairs.
/items GET / — list items
GET /:id — item details with audit trail
Individual check items with recognized fields, images, and processing state.
/items/:id/audit GET / — audit chain with hash verification Returns the full hash-chained audit trail for an item, with tamper detection.
/exceptions GET / — list exceptions
POST /:id/resolve — resolve exception
Items that failed automated processing and need human review.
/cashletters GET / — list cash letters Assembled X9.100-187 files ready for submission.

All responses follow the standard envelope: { "result": bool, "data": T, "error": string }.

Audit Trail

Every state transition for every check item is recorded in an append-only item_states table with cryptographic hash chaining:

hash = SHA256(prev_hash || item_id || state || payload || timestamp)

Each record links to the previous via its hash, forming an immutable chain. The verification endpoint walks the entire chain for an item and recomputes hashes to detect any tampering. If a single record has been modified, every subsequent hash will fail verification.

This is a critical compliance feature in financial systems — regulators require provable audit trails for every check that enters the clearing process. The hash chain provides cryptographic proof that no record has been altered after the fact.

The frontend includes an audit trail viewer that displays the chain for any item, with visual indicators for verified and broken links.

Image Quality Analysis

The IQA stage validates check images using pure Go image processing — no OpenCV or external dependencies. Five tests run against each image:

Test Criteria Why It Matters
Dimensions Aspect ratio 1.5–4.0, minimum 200×80 px Rejects non-check images (selfies, screenshots)
Darkness Average brightness 30–240 Catches under/overexposed captures
Contrast Standard deviation > 15 Ensures text is distinguishable from background
Skew < 5° rotation Excessive tilt prevents reliable MICR reading
Corners All four corners have content Detects cropped or partially captured checks

Results are stored with measured values and pass/fail thresholds. Failed items are routed to the exception queue with specific failure reasons, so operators know exactly what went wrong.

Data Model

PostgreSQL with versioned migrations managed by golang-migrate. Seven migrations define the schema:

Table Purpose
deposits Capture sessions with user, item count, declared total, and status
items Individual checks with front/back images, MICR fields, amounts, and processing state
item_states Append-only audit chain with hash-linked state transitions
iqa_results Image quality test results with measured values and pass/fail thresholds
exceptions Items needing human review with failure reasons and resolution notes
cash_letters Assembled X9.100-187 files with bundle/item counts and S3 keys
accounts / returns Receiving institution simulator data

Infrastructure

Local development uses Docker Compose with PostgreSQL and LocalStack (S3, SQS, KMS). No AWS account needed during development.

AWS resources are defined in Terraform:

  • S3 bucket with versioning, object lock (governance mode), and 90-day Glacier lifecycle
  • KMS customer-managed key for envelope encryption of check images
  • SQS queues per processing stage, each with its own dead-letter queue
  • IAM roles with least-privilege access policies

Production deployment targets Kubernetes (k3s) on a DigitalOcean droplet with Traefik ingress and Let's Encrypt TLS.

Tech Stack

Language
Go
Web Framework
Gin
Database
PostgreSQL
Message Queue
AWS SQS (LocalStack in dev)
Object Storage
AWS S3 with KMS encryption
ICL Standard
moov-io/imagecashletter (X9.100-187)
Routing Validation
moov-io/fed (ABA routing numbers)
Migrations
golang-migrate
Frontend
React 19 + TypeScript + Vite
Linting
Oxlint
Infrastructure
Docker Compose, Terraform, Kubernetes (k3s)
Image Processing
Pure Go (no OpenCV dependencies)

GitHub

View the source code

check-pipeline