A Spring Boot application that simulates an NCR ATM terminal communicating with an NCR ATM host over the NDC (NCR Direct Connect) protocol. Used as a testing tool to simulate real user operations — card authentication, PIN entry, cash withdrawal, and more — against a live or simulated host.
- Java 21
- Maven (or use the included
mvnwwrapper — no installation needed)
./mvnw clean packageThis produces a self-contained JAR at:
target/atmsimulator-0.0.1-SNAPSHOT.jar
./mvnw spring-boot:runjava -jar target/atmsimulator-0.0.1-SNAPSHOT.jarjava -jar target/atmsimulator-0.0.1-SNAPSHOT.jar \
--atm.host.simulated=false \
--atm.host.address=192.168.1.100 \
--atm.host.port=4000The server starts on http://localhost:8080.
By default, atm.host.simulated=true in application.properties, so no real NCR host is needed. To connect to a real host, see Connecting to a Real NCR ATM Host below.
./mvnw testRun a single test class:
./mvnw test -Dtest=AtmSimulatorApplicationTestsThree scenario endpoints simulate complete ATM sessions with separate operation classes (Login, Balance, Transfer, Withdraw, Logout) wired together by scenario services.
Each scenario response contains:
scenario— scenario namesuccess—trueonly if every step succeededoperations[]— ordered per-step results (operation name, status, balance, authorizationCode, NDC messages)fullNdcTrace[]— every NDC message exchanged during the session in chronological order
Simulated balance: every card starts at $1,000.00. Each approved transfer or withdrawal deducts from it. Balance is reset when the server restarts.
Flow: Login → Balance Inquiry → Logout
Request
{
"cardNumber": "4111111111111111",
"expiryDate": "2812",
"pin": "1234",
"accountType": "CHECKING"
}Response
{
"scenario": "BALANCE_CHECK",
"success": true,
"operations": [
{ "operation": "LOGIN", "success": true, "status": "APPROVED", "message": "Card accepted — PIN entry requested" },
{ "operation": "BALANCE", "success": true, "status": "APPROVED", "balance": 1000.00, "message": "BALANCE RETRIEVED" },
{ "operation": "LOGOUT", "success": true, "status": "APPROVED", "message": "Card ejected — session ended" }
],
"fullNdcTrace": [ ... ]
}Flow: Login → Balance → Transfer → Balance → Logout
Request
{
"cardNumber": "4111111111111111",
"expiryDate": "2812",
"pin": "1234",
"accountType": "CHECKING",
"toAccountNumber": "9876543210",
"transferAmount": 50.00
}Response
{
"scenario": "TRANSFER_AND_BALANCE",
"success": true,
"operations": [
{ "operation": "LOGIN", "success": true, "status": "APPROVED" },
{ "operation": "BALANCE", "success": true, "balance": 1000.00 },
{ "operation": "TRANSFER", "success": true, "status": "APPROVED", "authorizationCode": "A1B2C3", "processedAmount": 50.00 },
{ "operation": "BALANCE", "success": true, "balance": 950.00 },
{ "operation": "LOGOUT", "success": true, "status": "APPROVED" }
],
"fullNdcTrace": [ ... ]
}Flow: Login → Balance → Transfer → Withdraw → Balance → Logout
Request
{
"cardNumber": "4111111111111111",
"expiryDate": "2812",
"pin": "1234",
"accountType": "CHECKING",
"toAccountNumber": "9876543210",
"transferAmount": 50.00,
"withdrawAmount": 100.00
}Response
{
"scenario": "FULL_TRANSACTION",
"success": true,
"operations": [
{ "operation": "LOGIN", "success": true, "status": "APPROVED" },
{ "operation": "BALANCE", "success": true, "balance": 1000.00 },
{ "operation": "TRANSFER", "success": true, "status": "APPROVED", "authorizationCode": "D4E5F6", "processedAmount": 50.00 },
{ "operation": "WITHDRAW", "success": true, "status": "APPROVED", "authorizationCode": "G7H8I9", "processedAmount": 100.00 },
{ "operation": "BALANCE", "success": true, "balance": 850.00 },
{ "operation": "LOGOUT", "success": true, "status": "APPROVED" }
],
"fullNdcTrace": [ ... ]
}| ScenarioRequest Field | Required for | Type |
|---|---|---|
cardNumber |
All scenarios | String |
expiryDate |
All scenarios | String (YYMM) |
pin |
All scenarios | String |
accountType |
All scenarios | CHECKING | SAVINGS | CREDIT |
toAccountNumber |
Scenarios 2 & 3 | String |
transferAmount |
Scenarios 2 & 3 | Number |
withdrawAmount |
Scenario 3 only | Number |
Simulates a complete ATM withdrawal: terminal connect → card insert → PIN entry → host authorization → cash dispense.
POST http://localhost:8080/api/atm/withdraw
Content-Type: application/json{
"cardNumber": "4111111111111111",
"expiryDate": "2812",
"pin": "1234",
"operation": "WITHDRAW",
"amount": 100.00,
"accountType": "CHECKING"
}| Field | Type | Description |
|---|---|---|
cardNumber |
String | 13–19 digit PAN (Primary Account Number) |
expiryDate |
String | Card expiry in YYMM format (e.g. 2812 = Dec 2028) |
pin |
String | Cardholder PIN (encoded as ISO 9564 Format 0 PIN block internally) |
operation |
Enum | WITHDRAW | BALANCE_INQUIRY | TRANSFER | DEPOSIT |
amount |
Number | Transaction amount in dollars |
accountType |
Enum | CHECKING | SAVINGS | CREDIT |
{
"success": true,
"status": "APPROVED",
"authorizationCode": "FEB870",
"dispensedAmount": 100.00,
"message": "APPROVED",
"ndcTrace": [
{
"messageClass": "SOLICITED",
"messageSubClass": "F",
"direction": "TERMINAL->HOST",
"rawMessage": "2\u001cF\u001c00000001\u001c0\u001c000",
"readableMessage": "2<FS>F<FS>00000001<FS>0<FS>000",
"timestamp": "2026-03-08T02:16:54.283Z"
},
{
"messageClass": "SOLICITED",
"messageSubClass": "F",
"direction": "HOST->TERMINAL",
"rawMessage": "2\u001cF\u001c00000000\u001c0",
"readableMessage": "2<FS>F<FS>00000000<FS>0",
"timestamp": "2026-03-08T02:16:54.289Z"
},
{
"messageClass": "UNSOLICITED",
"messageSubClass": "E",
"direction": "TERMINAL->HOST",
"rawMessage": "1\u001cE\u001c00000001\u001c0001\u001d;4111111111111111=2812?",
"readableMessage": "1<FS>E<FS>00000001<FS>0001<GS>;4111111111111111=2812?",
"timestamp": "2026-03-08T02:16:54.291Z"
},
{
"messageClass": "HOST_COMMAND",
"messageSubClass": "8",
"direction": "HOST->TERMINAL",
"rawMessage": "3\u001c8\u001c024",
"readableMessage": "3<FS>8<FS>024",
"timestamp": "2026-03-08T02:16:54.291Z"
},
{
"messageClass": "UNSOLICITED",
"messageSubClass": "T",
"direction": "TERMINAL->HOST",
"rawMessage": "1\u001cT\u001c00000001\u001c0001\u001d020000\u001d000000010000\u001d10\u001d4111111111111111\u001d041225EEEEEEEEEE",
"readableMessage": "1<FS>T<FS>00000001<FS>0001<GS>020000<GS>000000010000<GS>10<GS>4111111111111111<GS>041225EEEEEEEEEE",
"timestamp": "2026-03-08T02:16:54.291Z"
},
{
"messageClass": "HOST_DATA",
"messageSubClass": "A",
"direction": "HOST->TERMINAL",
"rawMessage": "4\u001cPENDING",
"readableMessage": "4<FS>PENDING",
"timestamp": "2026-03-08T02:16:54.303Z"
}
]
}{
"success": false,
"status": "DECLINED",
"authorizationCode": null,
"dispensedAmount": 0,
"message": "DECLINED - EXCEEDS LIMIT",
"ndcTrace": [ ... ]
}Every response includes an ndcTrace array showing the full NDC protocol exchange in order. Each entry has:
direction—TERMINAL->HOSTorHOST->TERMINALmessageClass—SOLICITED,UNSOLICITED,HOST_COMMAND, orHOST_DATArawMessage— exact on-wire bytes (NDC control characters appear as Unicode escapes in JSON)readableMessage— human-readable form with<FS>,<GS>,<RS>,<US>tokens in place of control characters
The 6-message exchange for a withdrawal:
| # | Direction | Class | Sub-class | Description |
|---|---|---|---|---|
| 1 | TERMINAL→HOST | SOLICITED | F | Terminal Solicited Ready |
| 2 | HOST→TERMINAL | SOLICITED | F | Host Ready Acknowledgement |
| 3 | TERMINAL→HOST | UNSOLICITED | E | Card Data (Track 2) |
| 4 | HOST→TERMINAL | HOST_COMMAND | 8 | Enter PIN command (state 024) |
| 5 | TERMINAL→HOST | UNSOLICITED | T | Transaction Request (txnCode + amount + accountType + PAN + PIN block) |
| 6 | HOST→TERMINAL | HOST_DATA | A | Authorization Response |
1<FS>T<FS>00000001<FS>0001<GS>020000<GS>000000010000<GS>10<GS>4111111111111111<GS>041225EEEEEEEEEE
│ │ │ │ │ │ │ │ │
│ │ │ │ │ │ │ │ └─ PIN block (ISO 9564 Format 0)
│ │ │ │ │ │ │ └─ PAN (card number)
│ │ │ │ │ │ └─ Account type (10 = CHECKING/none)
│ │ │ │ │ └─ Amount in cents, 12 digits ($100.00 = 000000010000)
│ │ │ │ └─ Transaction code (02=withdrawal, 0000=flags)
│ │ │ └─ Institution ID
│ │ └─ Terminal ID
│ └─ Sub-class T = Transaction Request
└─ Class 1 = UNSOLICITED
src/main/resources/application.properties:
# true = simulated host (default, no network required)
# false = live TCP connection to NCR host
atm.host.simulated=true
atm.host.address=localhost
atm.host.port=4000
atm.terminal.id=00000001
atm.institution.id=0001When atm.host.simulated=true, the built-in SimulatedHostGateway is used:
| Amount | Result |
|---|---|
| ≤ $500.00 | APPROVED (random 6-char auth code) |
| > $500.00 | DECLINED — EXCEEDS LIMIT |
Follow these steps to switch the simulator from the built-in simulation to a live NCR ATM host.
# Switch to real host
atm.host.simulated=false
# IP address or hostname of the NCR ATM host
atm.host.address=192.168.1.100
# TCP port the host listens on (commonly 4000 or 17000 for NDC)
atm.host.port=4000
# 8-digit terminal ID registered with the host/acquirer
atm.terminal.id=00000001
# 4-digit institution/bank ID assigned by the host
atm.institution.id=0001RealNdcHostGateway manages the raw TCP socket. Call openConnection() before sending any messages and closeConnection() when the session is complete. Currently AtmSimulationService delegates all messaging through the AtmHostGateway interface — wire openConnection() / closeConnection() into the service around the transaction steps:
// in AtmSimulationService.processWithdrawal(), before Step 1:
if (hostGateway instanceof RealNdcHostGateway real) {
real.openConnection();
}
// after Step 5 (card ejected):
if (hostGateway instanceof RealNdcHostGateway real) {
real.closeConnection();
}In simulation mode the PIN block is sent as clear-text ISO 9564 Format 0. A real NCR host requires the PIN block to be 3DES-encrypted under the Terminal Working Key (TWK) before transmission.
What needs to change in PinBlockUtil.java:
| Step | Current (simulation) | Required (production) |
|---|---|---|
| Build format block | 0 + len + PIN + FFFF… |
same |
| Build PAN block | 0000 + rightmost 12 PAN digits |
same |
| XOR blocks | clear-text result | same |
| Encrypt | not done | 3DES-encrypt result under TWK |
| Send | clear-text PIN block | encrypted PIN block |
The TWK is loaded from the HSM (Hardware Security Module) or injected at terminal key-load time. Add the encryption step in PinBlockUtil.buildPinBlock() or in NdcMessageBuilder.buildTransactionRequest() once you have the TWK available.
The NCR host maintains a terminal table. Before the simulator can exchange messages, the atm.terminal.id and atm.institution.id must be pre-registered on the host side. Contact your host/acquirer team for the values assigned to the test terminal.
Ensure the machine running the simulator can reach the host on the configured port:
# Test connectivity (replace IP and port with your values)
telnet 192.168.1.100 4000When atm.host.simulated=false, Spring Boot loads RealNdcHostGateway instead of SimulatedHostGateway — no code changes needed. The same REST endpoint and NDC message format are used; only the transport layer changes from in-memory to TCP.
Simulated (true) |
Real (false) |
|
|---|---|---|
| Transport | In-memory method calls | TCP/IP socket |
| Authorization | Amount ≤ $500 → approve | Real host decision |
| PIN block | Clear-text | Must be 3DES-encrypted under TWK |
| Auth code | Random 6 chars | Issued by host |
| Terminal registration | Not required | Required on host |
src/main/java/atm/terminal/atmsimulator/
├── controller/ AtmController REST entry point (withdraw + 3 scenarios)
├── model/
│ ├── request/ AtmRequest JSON input for /withdraw
│ │ ScenarioRequest JSON input for scenario endpoints
│ └── response/ AtmResponse JSON output for /withdraw
│ ScenarioResponse JSON output for scenarios
│ OperationResult Per-step result within a scenario
├── domain/ OperationType, AccountType, TerminalState
│ TransactionResult, AtmSession
├── protocol/ NdcMessage, NdcMessageClass, NdcDelimiter, PinBlockUtil
└── service/
├── NdcMessageBuilder Builds all outbound NDC messages
├── AtmSimulationService Orchestrates single withdrawal flow
├── operation/ LoginOperation Card insert + PIN entry command
│ BalanceInquiryOperation txnCode 010000
│ TransferOperation txnCode 030000
│ WithdrawOperation txnCode 020000
│ LogoutOperation Card ejected notification
├── scenario/ BalanceCheckScenario Scenario 1
│ TransferAndBalanceScenario Scenario 2
│ FullTransactionScenario Scenario 3
└── gateway/
├── AtmHostGateway Interface
├── SimulatedHostGateway In-memory host (default)
└── RealNdcHostGateway Live TCP to NCR host