The floor node and the machines
Read this first
- The machines are planned until one is connected. The machine lane is built and contracted, and only software has ever driven it.
- NCR’s reference manual for these machines has not been read. Every protocol value the scaffold carries is the Lab’s reading of publicly circulating descriptions of NDC, recorded as assumptions A-46 to A-48 and overridable machine by machine.
- The wire address is a placeholder. Nothing is listening on it today, and no listener is deployed anywhere.
What this is. The Lab has five NCR Atleos machines on its floor. This document says how
they are joined to the Virtual Financial Ecosystem Simulation, what is still unknown about
them, and the order the work goes in.
What this is not. It is not a statement of how an NCR Atleos works. **NCR's reference
manual for these machines has not been read**, no NCR document was consulted, and every
protocol value the scaffold carries is the Lab's reading of publicly circulating descriptions
of NDC - recorded as assumptions A-46, A-47 and A-48 in
[../spec/assumptions.md](../spec/assumptions.md) and overridable, machine by machine, from
nodes/floor/<machine>.yaml. Nothing here should be taken as describing what the machines on
the floor actually do until the inventory below is done.
Why a floor node exists at all
A real NCR self-service terminal does not decide anything. Its state tables say *"in this
state, send a transaction request and wait"*, and what happens next - which screen, which
device action, how much comes out of the cassettes - is decided by a host that owns those
screens and answers over NDC (NCR Direct Connect), on a TCP link.
The simulated ecosystem lives on Cloud Run, and Cloud Run cannot accept raw TCP. A Cloud
Run service is reached over HTTP(S) through Google's front end on the one port the container
declares; there is no second listener a machine can open a socket to, and nothing in the
service's configuration can publish one. The same paragraph is already in
[../deploy/README.md](../deploy/README.md) about the ISO 8583 listener, for the same reason.
So the Lab runs the same image twice:
| Where | What it is | |
|---|---|---|
| The cloud gateway | Cloud Run, ecosystem-gateway | HTTP, the console, every lane, every team's world. No TCP listener, and none is set. |
| The floor node | a small PC on the Lab LAN, on the machines' own segment | FLOOR_NODE=1. Holds the NDC and ISO 8583 TCP ports the machines dial, and forwards what they ask for to the cloud's machine lane with its own team key. |
The floor node is a participant of the cloud world, not a second world. It keeps no
ledger, settles nothing, and holds no state a restart would lose except the day's journal it
has not handed over yet. Everything a machine asks for is answered by the simulated switch in
the cloud, over /v1/atm/**, exactly as an integration calling those endpoints from a laptop
would be answered. The floor node adds a protocol, not a capability.
The research checklist
Nothing below is known yet. Each line is a question with an owner (Carlos), and each one
blocks something specific - which is why the list is a table and not a wish.
| # | To find out | What it blocks |
|---|---|---|
| 1 | Exact model of each of the five machines, and the software version on it. Known (2026-09-06): the flagship is an NCR Atleos SelfServ 62 (interior lobby pocket cash recycler on the Scalable Recycler, 15" touch screen, 18.5" upper panel) running NCR Atleos NDC Enterprise on Windows 11; NCR presents NDC Enterprise as the multi-vendor successor to Advance NDC. Still pending: the other four models, and the NDC Enterprise version on each | everything; a profile cannot be written for a machine nobody has identified |
| 2 | NDC version the software speaks, and the XFS version underneath it | the whole dialect: framing, message classes, field order (A-46, A-47) |
| 3 | The reference manual for that model and version | replaces every assumed value in packages/ndc-host/src/dialect.ts with a read one |
| 4 | Supervisor access - the mode, the credential, and who holds it | reading the configuration, the journal and the counters off the machine at all |
| 5 | EPP key state - keys loaded, zeroed, or never loaded; and whether the Lab can load its own | chip and PIN, step four. Nothing before it needs a key, which is why nothing before it waits |
| 6 | Cards: what exists, and whether the Lab may emboss or encode its own in the Lab BIN range beginning 9 | the card-present flows. The Lab's PAN prefix is deliberately outside the assigned ranges |
| 7 | CA keys for chip - which certification authority public keys the machines hold, and whether Lab test keys can be loaded | EMV, step four |
| 8 | Cash test mode - whether it exists on these machines and how it is entered | dispensing without moving real notes, step three |
| 9 | The electronic journal - its format, where it is held, and how it is read off the machine | replaces the Lab journal line (A-48) with the machine's own |
| 11 | Who owns the screens and the flow. NCR's page says NDC Enterprise lets an operator "manage the transaction flow and user interface through the enterprise server". Whether that server is installed on the Lab's machines, and whether the host (the floor node) still downloads state tables and screens the classic NDC way or the enterprise server does, decides how much of the host's customization-data role is ours to play | the customization-data messages of the host (A-46); possibly the whole balance between host and enterprise server |
| 10 | The VLAN - which segment the machines are on, what else can reach it, and whether the floor node can be the only route out | the security boundary of the whole arrangement. See below |
The VLAN is the security boundary, and that is a decision
There is no credential on the NDC link, and that is stated rather than omitted. An NDC
terminal has no field to carry a team key in and no notion of presenting one, so a listener
that demanded one would be a listener no machine can talk to.
What protects the link is the network it is on. The machines and the floor node sit on a
VLAN of their own; the floor node is the only thing on that VLAN that can reach the
internet; and the team key lives on the floor node, is presented by the floor node to the
cloud lane, and never travels on the machine link in either direction. Anything that can
reach the NDC port can drive a machine's screens, so the answer to "who may reach the port"
is "the machines, and nothing else".
On any link that leaves the floor, set NDC_TLS_CERT and NDC_TLS_KEY (and the ISO 8583
pair beside them). Plain TCP is the default because floor hardware often cannot do better and
a listener that refuses the machine is not a listener.
Assets the Lab already holds
3D renders and animations of the SelfServ 62, a Blender model of it, and realistic photographs exist and can be provided on request (Carlos, 2026-09-06). They are not in this repository. They serve the films, the public simulation page and a physical-layout rehearsal of the floor; they are not needed for the host, which only ever sees the machine's messages.
The four steps
The wire and NDC ride the same floor node. One box, one team key, two ports: NDC_TCP_PORT
for the machines that speak NDC and ISO8583_TCP_PORT for a machine or a POS on the same floor
that speaks ISO 8583 instead. That is not a convenience, it is the arrangement — both listeners
need raw TCP, Cloud Run carries neither, and a second box would mean a second team key, a second
journal to reconcile and two places to get the VLAN wrong. The ISO 8583 half has its own profile
([../spec/iso8583-wire.md](../spec/iso8583-wire.md)), its own sign-on and its own conformance
pack (npm run wire:conformance); everything below about the VLAN, the key file and the service
recipes applies to it word for word. The same pairing is what the Compute Engine recipe in
[../deploy/README.md](../deploy/README.md) stands up when the link has to leave the floor.
1. Inventory. Work the checklist above, machine by machine, and write what is found into
nodes/floor/<machine>.yaml - the dialect block for what the manual says, the pending list
for what is still open. No machine is put on the network in this step.
2. Floor node and host, driven by the virtual ATM and the virtual switch. *(This is what
exists today.)* The floor node runs, both listeners are up, and each is exercised by a
client-shaped test double that dials a real socket and sends real frames: NDC by
packages/ndc-host/test/virtual-atm.ts - which carries out the function id it is given and
reports back - and ISO 8583 by packages/iso8583-wire/test/virtual-switch.ts, which signs on,
authorizes, reverses, withdraws with a card and without one, and is answered by the same world
the REST lanes answer from. Both are proved before a machine exists, which is the only way to
arrive at step three with something worth plugging in.
3. The first no-cash transaction, on ONE machine. One machine, on the VLAN, dialling the
floor node. Balance inquiry only: it reads an account, prints a figure, moves nothing and
dispenses nothing. It is the safest transaction that exists and the right first thing to run
on a machine whose supervisor access and journal have only just been understood. What is
being proved here is the link and the dialect, not the ecosystem - the ecosystem has been
answering these operations over REST for months.
4a. Cardless withdrawal with test notes. The one-time code the holder's phone was given
is keyed at the machine, the switch spends the order exactly once, and the notes come out -
in cash test mode if the machines have it, and with test notes if they do not. No card,
no PIN, no EMV, which is what makes this runnable before a single key is loaded. The
electronic journal is read off the machine at the end of the run and reconciled through
POST /v1/atm/journal.
4b. Chip and PIN, after key loading. Only after items 5, 6 and 7 of the checklist are
settled. It needs an EPP with keys, cards in the Lab BIN range, CA keys the machines accept,
and a dialect that has been read rather than assumed. Nothing in the scaffold computes a
PIN block, holds a key or verifies a certificate today, and nothing should until this step.
Running a floor node
The floor node is the ordinary image with floor-node environment. It needs no build of its
own and no code branch.
docker run -d --name finlab-floor \
--restart unless-stopped \
-p 8583:8583 -p 9100:9100 -p 8787:8787 \
-e FLOOR_NODE=1 \
-e NDC_TCP_PORT=9100 \
-e ISO8583_TCP_PORT=8583 \
-e CLOUD_GATEWAY_URL=https://ecosystem.financial \
-e FLOOR_TEAM_KEY_FILE=/run/secrets/floor-team-key \
-v /etc/finlab/floor-team-key:/run/secrets/floor-team-key:ro \
-v /etc/finlab/nodes/floor:/app/nodes/floor:ro \
ecosystem-gateway:latest
The environment
| Variable | Default | What it is |
|---|---|---|
FLOOR_NODE | empty | 1 turns floor-node mode on. Empty means this process is not a floor node, which is what every cloud instance is |
NDC_TCP_PORT | (required in floor mode) | the port the machines dial for NDC |
NDC_TCP_HOST | 0.0.0.0 | the interface the listener binds |
CLOUD_GATEWAY_URL | (required in floor mode) | the cloud gateway machine operations are forwarded to, e.g. https://ecosystem.financial |
FLOOR_TEAM_KEY_FILE | empty | a file holding the floor's team key. Preferred: an environment variable is readable by anything that can list the process, and a floor node sits where somebody has physical access |
FLOOR_TEAM_KEY | empty | the same key in the environment, when a file is impractical. One of the two is required |
FLOOR_NETWORK_ID | atm-network | the id of the ATM-network administrator in the cloud world, for the one operation the machine lane does not name a route for |
FLOOR_PROFILES | nodes/floor | where the machine profiles are read from |
FLOOR_UNKNOWN_MACHINE | refuse | what a machine whose LUNO is in no profile gets. refuse closes the connection - the right default on a floor. default speaks the Lab default dialect, which is what a bench wants |
FLOOR_TIMEOUT_MS | 15000 | before a machine is told the host could not reach its world |
NDC_MAX_CONNECTIONS | 16 | machines held at once; one over is refused at the socket rather than queued |
NDC_IDLE_TIMEOUT_SECONDS | 300 | silence before the listener closes a connection |
NDC_TLS_CERT / NDC_TLS_KEY | empty | PEM paths. Both or neither: one without the other stops the floor node coming up rather than quietly starting a plain-text listener somebody believed was encrypted |
ISO8583_TCP_PORT | empty | the ISO 8583 listener, for a machine or a POS on the same floor that speaks it instead. Read by the gateway itself; see [../deploy/README.md](../deploy/README.md) |
**A floor node started without a key, or without a gateway to forward to, refuses to come
up.** That is the right severity: a box that accepts a machine's socket and then fails every
transaction on it is worse for the person at the machine than a box that never answered.
As a systemd service (Linux floor PC)
/etc/systemd/system/finlab-floor.service:
[Unit]
Description=FinLab floor node - NDC and ISO 8583 listeners for the Lab machines
After=network-online.target docker.service
Requires=docker.service
[Service]
Type=simple
Restart=always
RestartSec=5
# the key is a file the service user can read and nothing else can: 0400, owned by root
ExecStartPre=-/usr/bin/docker rm -f finlab-floor
ExecStart=/usr/bin/docker run --rm --name finlab-floor \
-p 9100:9100 -p 8583:8583 -p 8787:8787 \
-e FLOOR_NODE=1 -e NDC_TCP_PORT=9100 -e ISO8583_TCP_PORT=8583 \
-e CLOUD_GATEWAY_URL=https://ecosystem.financial \
-e FLOOR_TEAM_KEY_FILE=/run/secrets/floor-team-key \
-v /etc/finlab/floor-team-key:/run/secrets/floor-team-key:ro \
-v /etc/finlab/nodes/floor:/app/nodes/floor:ro \
ecosystem-gateway:latest
ExecStop=/usr/bin/docker stop finlab-floor
[Install]
WantedBy=multi-user.target
sudo install -m 0400 -o root -g root floor-team-key /etc/finlab/floor-team-key
sudo systemctl daemon-reload && sudo systemctl enable --now finlab-floor
journalctl -u finlab-floor -f
As a Windows service (the machines are Windows-based, and so may the floor PC be)
Windows has no native "run this as a service" wrapper, so use one. With
[NSSM](https://nssm.cc/) and Node 22 installed, running the repository directly rather than
the container:
# the key, readable by the service account and nobody else
icacls C:\finlab\floor-team-key /inheritance:r /grant:r "NT SERVICE\finlab-floor:(R)"
nssm install finlab-floor "C:\Program Files\nodejs\npm.cmd" "run gateway"
nssm set finlab-floor AppDirectory C:\finlab\finlab-ecosystem
nssm set finlab-floor AppEnvironmentExtra ^
FLOOR_NODE=1 ^
NDC_TCP_PORT=9100 ^
ISO8583_TCP_PORT=8583 ^
CLOUD_GATEWAY_URL=https://ecosystem.financial ^
FLOOR_TEAM_KEY_FILE=C:\finlab\floor-team-key
nssm set finlab-floor AppStdout C:\finlab\logs\floor.log
nssm set finlab-floor AppStderr C:\finlab\logs\floor.log
nssm set finlab-floor Start SERVICE_AUTO_START
nssm start finlab-floor
Open the two ports to the machines' VLAN and to nothing else:
New-NetFirewallRule -DisplayName "FinLab NDC" -Direction Inbound -Protocol TCP `
-LocalPort 9100 -RemoteAddress 10.20.30.0/24 -Action Allow
New-NetFirewallRule -DisplayName "FinLab ISO 8583" -Direction Inbound -Protocol TCP `
-LocalPort 8583 -RemoteAddress 10.20.30.0/24 -Action Allow
The two flows the host answers today
Both are mapped onto operations the simulated switch already publishes
(packages/atm-network/src/network.ts) and endpoints the machine lane already carries
(/v1/atm/**). The host adds no capability of its own.
Balance inquiry
machine -> Transaction Request opcode buffer = the balance-inquiry code (A-47)
general-purpose buffer B = the account number
host -> /v1/atm/balance-inquiries { terminalId, issuerId, accountNumber }
host -> Transaction Reply function id = print only, screen = the balance screen,
the figure in the screen update and on the receipt
machine -> Solicited Status the reply was carried out
Nothing moves and nothing comes out. The function id is print only on every path through
this flow.
Cardless withdrawal by code
(phone) -> /v1/atm/cardless/orders the holder's OWN institution mints the one-time code.
A machine never raises an order.
machine -> Transaction Request opcode buffer = the cardless-withdrawal code (A-47)
general-purpose buffer B = the one-time code
host -> /v1/atm/cardless/redemptions { terminalId, code }
host -> Transaction Reply approved: function id = dispense and print, and the
amount is THE ORDER'S, never the machine's
declined: function id = print only, the decline screen,
and the ISO 8583 DE039 on the receipt
machine -> Solicited Status the notes went out, or they did not
host -> a journal line, completed by that status
Four properties hold, and each one is asserted in packages/ndc-host/test/flows.test.ts:
- Every request gets a reply. A machine that receives silence prints nothing, times out, and teaches a customer that the Lab's machines hang.
- A decline dispenses nothing. The function id is set from the outcome, and there is no path where a non-approval reaches dispense and print.
- The amount is the switch's. What the customer keyed at the machine is recorded and then ignored: the order is the authority, and a machine that could name its own amount would be a machine that could pay itself.
- Nothing is dispensed twice. The switch spends the order before it authorizes, and the host sends the same reply to a repeated coordination number rather than starting a second transaction - which is what a machine that missed a reply and re-sent actually needs.
Reconciling the electronic journal
Every machine keeps its own record of the day. The switch keeps its own. Reconciliation is
finding where they disagree, and the interesting direction is the machine's: a journal
line the switch has no record of is a payout nobody is going to settle, and it is the shape of
a dispense that happened after the link dropped. The other direction gets found anyway, when
the cassettes are counted.
curl -sS https://ecosystem.financial/v1/atm/journal \
-H "x-lab-api-key: $FLOOR_TEAM_KEY" \
-H 'content-type: application/json' \
-d '{"journal":"2026-09-06T13:00:00.000Z|TERM-LAB-0001|cardless-withdrawal|ATM-atm-network-00001|200000|DOP|00|A"}'
The report says what matched (on the reference, or - for a line with no reference - on the
amount and the terminal), what a matched pair disagrees about, what is in the journal only,
what is at the switch only, and the two totals with their difference.
The line format is the Lab's own, and it is a placeholder (assumption A-48). The format
of these machines' journals is item 9 of the checklist. When it arrives, a parser for it
produces the same JournalLine objects and nothing downstream changes - not the matcher, not
the report, not the endpoint. That is what makes this a scaffold rather than a guess: the
shape of the answer is settled and the reading of the bytes is not.
Where the code is
packages/ndc-host/
├── src/dialect.ts the dialect table - READ THIS FIRST. A-46, A-47, all profile-driven
├── src/messages.ts the six NDC message families as objects
├── src/codec.ts bytes to messages and back, and the TCP stream framer
├── src/profile.ts nodes/floor/<machine>.yaml, and the dialect overrides in it
├── src/lane.ts the three switch operations, over HTTP or in-process
├── src/host.ts the state machine: the two flows above
├── src/server.ts the TCP listener, one host per connected machine
├── src/floor.ts FLOOR_NODE and the environment
├── src/journal.ts the journal line and the matcher
└── test/virtual-atm.ts the machine that does not exist, on a socket that does
How a partner switch connects, and the conformance pack
How a partner switch connects
The address is wire.ecosystem.financial:8583, and it is a PLACEHOLDER. It is written here
so an integration has one name to configure and one place to change when a link is actually
stood up. Nothing is listening on it today, and no listener is deployed anywhere. Saying
otherwise would be the easiest false claim in this repository to make and the most expensive
to have made.
Why there is nothing to dial yet, plainly:
The listener needs a TCP-capable host, and the deployed gateway is not one. The ecosystem's gateway runs on Cloud Run, which is reached over HTTP(S) through Google's front end on the one port the container declares; there is no second listener a switch can open a socket to, and no configuration that publishes one. So the listener runs where raw TCP is possible, which today means one of exactly two places: the Lab floor node - this same image on a small PC on the machines' own network segment - or a small Compute Engine VM the deployer starts, running the same image with
ISO8583_TCP_PORTset. Both are recipes a person follows; neither is running.docs/deploy/README.mdcarries the VM recipe as written-outgcloudcommands, and they have not been run.
What a partner does, in order:
- Ask for a team key. The wire sign-on takes a team key - the same value the REST lanes take in
x-lab-api-key, fromFINLAB_API_KEYS. A service identity is an HTTP credential and is not accepted here: its scope is expressed in HTTP methods and lanes, which a socket does not have. - Say which addresses the switch dials from. They go in
ISO8583_ALLOWED_CIDRSon the listener. A peer outside it is destroyed at the socket without an answer - no refusal to read, no connection slot spent, nothing to learn from. On a floor VLAN the list may be left empty and the network is the boundary; on any link that leaves the floor it is not optional. - Take TLS.
ISO8583_TLS_CERTandISO8583_TLS_KEY, both or neither. Remember what is inside the message: DE052 carries a Lab PIN in the clear - the simulation holds no keys and computes no PIN block, and §3 says so in the table - so the transport is the only thing protecting it. Simulated PIN or not, a link that leaks one teaches a bad habit. The Lab's floor certificate is self-signed; a partner should pin it rather than turn verification off. - Run the conformance pack (§9) against the listener, from the switch's own network, and keep the report.
- Sign on, and stay signed on. One connection, one world, one credential presented once. The link is expected to hold the socket and echo (
0800DE070301) rather than reconnect per transaction.
The environment the listener reads
Every one of these is read by the gateway process that holds the port. Empty means the
listener does not exist; that is what every Cloud Run instance uses.
| Variable | Default | What it does |
|---|---|---|
ISO8583_TCP_PORT | empty | the port. Empty means no listener at all |
ISO8583_TCP_HOST | 0.0.0.0 | the interface it binds |
ISO8583_ALLOWED_CIDRS | empty | comma- or newline-separated addresses and CIDR blocks that may connect: 203.0.113.0/24, 10.20.0.5, 2001:db8::/32. Empty means every peer may connect. An IPv4 client on a dual-stack socket (::ffff:203.0.113.7) is matched against the IPv4 rules as the IPv4 address it is. A malformed entry stops the process coming up rather than being skipped |
ISO8583_TLS_CERT / ISO8583_TLS_KEY | empty | PEM paths. Both or neither: one without the other stops the gateway coming up rather than quietly starting a plain-text listener somebody believed was encrypted |
ISO8583_MAX_CONNECTIONS | 32 | connections held at once; one over is refused at the socket rather than queued |
ISO8583_MAX_MESSAGES_PER_MINUTE | 600 | per connection, on a continuously refilled token bucket. Over it, 96 with the retry in DE126 — the connection is not closed |
ISO8583_IDLE_TIMEOUT_SECONDS | 180 | silence before the listener closes a connection |
ISO8583_MAX_SIGN_ON_ATTEMPTS | 3 | wrong keys before the connection is closed, so the socket is not a key oracle |
Every message is in the world's trace
A signed-on connection writes into the team's own trace, under the actor iso8583-wire: an
event for the sign-on, then a message and a reply per exchange, each carrying the
connection's name (iso8583-0007), the peer, the team, and an **eight-hex fingerprint of the
key** — enough to tell a floor node's credential from a bench key six weeks later, useless as
a credential itself. The rendering is the same one the log uses, so **DE052 and DE127 are
dropped before a character is written**. That is what makes a wire withdrawal and a REST
withdrawal distinguishable after the fact, which is the question a reconciliation asks.
The conformance pack
The list of what a switch has to get right, and a runner for it:
npm run wire:conformance -- --host=127.0.0.1 --port=8583 --key=<team key> \
--gateway=http://127.0.0.1:8787
npm run wire:conformance -- --host=wire.example --port=8583 --key=… --tls --json
It dials the listener with packages/iso8583-wire/test/virtual-switch.ts — the same virtual
card switch the test suite drives, which is the point: **the pack is not a description of the
tests, it is the tests, pointed somewhere else.** Without --gateway the cases that need Lab
material (a card, an acceptor, a terminal, a cardless order) report SKIP with the reason
and are never counted as passes; the link discipline runs against anything. The exit code is
0 when nothing failed, 1 when anything did, and --json prints the whole report.
**A pass says the listener speaks this profile. It says nothing about any card scheme's
certification, and a run is never a substitute for one.**
| Case | Messages | What a switch must do |
|---|---|---|
sign-on<br>sign on with the team key in DE127 | 0800 0810 | an 0800 with DE070 001 and the key in DE127 is answered 0810 DE039 00, and the response carries no DE127: a credential that comes back is a credential in every capture of the link. |
sign-on-refused<br>a key the listener does not hold is refused, and the socket is not an oracle | 0800 0810 | a wrong key is answered 57 and the connection is closed after the configured number of attempts, so the link cannot be used to guess one. |
unsigned-refused<br>a financial message before the sign-on is refused | 0200 0210 | any message that is not the sign-on, on a connection that has not signed on, is answered 57 with DE126 saying to sign on first. It is never carried out. |
credential-once<br>DE127 on any message but the sign-on is refused outright | 0800 0100 0200 | a credential on a financial message is refused with 57 rather than ignored: a terminal that presents its key on every message is a terminal whose key is in every packet capture. |
echo<br>echo test | 0800 0810 | an 0800 with DE070 301 is answered 00 while the link is up. It is what a switch polls with and the cheapest way to tell a dead link from a quiet one. |
authorization<br>a purchase at an acceptor | 0100 0110 | a 0100 carrying DE002, DE042 and DE004 is answered 0110 with DE039, and on an approval with DE037 and DE038. The PAN comes back MASKED: the response is the projection, and the projection masks it.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
authorization-declined<br>a decline is a response | 0100 0110 | an authorization the issuer will not approve arrives as a well-formed 0110 with its own DE039 - never as a dropped connection. A machine that receives silence prints nothing and retries.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
reversal<br>the reversal of an authorization, by its own reference | 0400 0410 | a 0400 carrying the DE037 the 0110 came back with is answered 0410 00, and DE090 points at the message being reversed. A 0400 with no DE037 is answered 30.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
withdrawal-cardless<br>a cardless withdrawal at a machine | 0200 0210 | a 0200 with DE003 010000, NO DE002 and the one-time code in DE102 is answered 0210. The order is spent exactly once: a second redemption of the same code is refused.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
withdrawal-card<br>a withdrawal with a card and its Lab PIN | 0200 0210 | a 0200 with DE003 010000, DE002, DE041, DE100 and DE052 is answered 0210, and DE052 is never echoed and never logged.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
deposit<br>a deposit at a machine | 0200 0210 | a 0200 with DE003 210000, the declared amount in DE004 and DE002 or DE102 is answered 0210 with the outcome the switch reached, recognition included.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
balance-inquiry<br>a balance inquiry, which moves nothing | 0200 0210 | a 0200 with DE003 310000, DE100 and DE102 is answered 0210 00 with the figure in DE126. It posts nothing: DE054 is not in this profile and no transaction is recorded.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
clearing<br>an authorization raised on the wire clears and settles net | 0100 0110 | THE PROFILE CARRIES NO CLEARING MESSAGE - no 0320, no 0500, no 0600. What must hold is that a transaction authorized on the wire is the same transaction the acquirer's clearing cycle picks up and settles net, which the Lab proves through POST /v1/cards/cycles rather than through an MTI.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
wire-rest-parity<br>the wire and the REST lane agree, on the outcome and on the postings | 0100 0110 | the same operation sent over the wire and over the REST card lane, in the same world, reaches the same DE039 and moves the same money. The response IS the REST projection, so a difference here is a difference in the ecosystem and not in the link.<br><em>needs Lab material (a card, an acceptor, a terminal): pass --gateway.</em> |
unknown-mti<br>a message family the profile does not carry is refused, not dropped | 0420 0500 | an MTI outside 0100/0200/0400/0800 - a 0420 reversal advice, a 0500 reconciliation - is answered 12 with DE126 naming the four families. The connection survives. |
format-error<br>a frame that is not a message is answered, and the link survives | 0810 | bytes that do not decode are answered 30 - on the request's own response MTI when the MTI was readable, otherwise on an 0810 - and the connection stays up. |
oversized-frame<br>a frame past the profile's maximum ends the connection | 0810 | a length header declaring more than 4096 bytes, or zero, is answered 30 once and the connection is closed: there is no delimiter in this protocol to resynchronize a stream on. |
rate-limit<br>a flood is slowed, not disconnected | 0800 0810 | past the per-connection budget a message is answered 96 with the retry in DE126, and the connection is NOT closed: a terminal that is merely fast should slow down, not fall off the network.<br><em>a limit case: pass --include-slow.</em> |
sign-off<br>sign off, and the listener closes | 0800 0810 | an 0800 with DE070 002 is answered 00 and the connection is closed by the listener, so a switch that has finished does not hold a slot. |
_19 cases, 10 of which run against any listener with nothing but a host, a port and a key. Generated from packages/iso8583-wire/src/conformance.ts by npm run spec:wire._
There is deliberately **no clearing message in the pack, because there is none in the
profile (§2: no 0320, no 0500, no 0600). Clearing and settlement here are a cycle** the
acquirer runs, so the clearing case proves that a transaction authorized on the wire is
the one that cycle picks up and settles net — through POST /v1/cards/cycles, not through an
invented MTI. For the same reason a 0420 reversal advice is in the pack only as something a
switch must refuse with 12: the profile's reversal is 0400/0410.