ARCOS Transaction Reporting
Assemble DEA ARCOS acquisition and disposition records from an immutable ledger — fixed-width layout, transaction codes, reporting cadence, SHA-256 hashing
The Automated Records and Consolidated Orders System (ARCOS) is the DEA’s periodic transaction-reporting program, and for manufacturers and distributors it is the single most demanding recurring filing a controlled-substance system produces. Under 21 CFR § 1304.33, registrants that manufacture or distribute Schedule I–II substances — and specified Schedule III materials — must report each acquisition and disposition transaction to the DEA on a defined cadence, in a fixed-width record format the ARCOS processor ingests without tolerance for structural error. This reference sits within the broader Regulatory Reporting & DEA Submissions area and applies its central rule to ARCOS specifically: the ARCOS file is a deterministic projection of the append-only ledger, never a hand-built export, so a resubmission or an inspector’s regeneration is byte-identical to the original.
Regulatory Context & Compliance Boundaries
ARCOS reporting is a strict-liability recordkeeping obligation, and the boundary between “the ledger” and “the report” is where compliance is won or lost. The pipeline must encode each governing requirement as a structural property of the generator, not as a manual check performed before submission.
| Regulation | Reporting requirement | Implementation control |
|---|---|---|
21 CFR § 1304.33 |
Periodic acquisition/disposition reporting for Schedule I–II (and some III) | Deterministic record assembler over ledger acquisition/disposition events |
21 CFR § 1304.11 |
Reported transactions must reconcile to the perpetual inventory | ARCOS records selected from the same append-only ledger that drives inventory |
21 CFR § 1304.04 |
Two-year retrievable retention of the filing | Signed, SHA-256 hashed ARCOS artifact in WORM storage |
HIPAA 45 CFR § 164.312(b) |
Tamper-evident provenance of the submission | Audit hash binding ledger event set to emitted file |
The decisive principle is that an ARCOS transaction is not authored at reporting time — it already exists as a committed ledger event the moment a substance is acquired or disposed. The generator’s only job is to select the reportable events and format them; it never invents a transaction. Reportable events are keyed by their National Drug Code, which must arrive already normalized per the NDC-11 vs NDC-10 Parsing Standards, and each event’s schedule is resolved through the DEA Schedule II–V Classification Mapping so that only in-scope substances are reported.
Which Transactions Are Reportable
ARCOS reports movement of controlled substances into and out of the registrant’s inventory. Not every ledger event qualifies, and misjudging the scope is a common source of over- and under-reporting.
- Acquisitions — receipts from a supplier, returns received, and inventory adjustments that increase on-hand for an in-scope substance.
- Dispositions — sales or transfers to another registrant, disposal/destruction, theft or loss, and adjustments that decrease on-hand.
- In scope by schedule — all Schedule I and II substances, plus the specific Schedule III substances and selected products the DEA designates as reportable. Schedule IV–V and non-controlled items are excluded.
- Excluded — internal moves that do not change the registrant’s total on-hand (for example, a bin-to-bin relocation) generate ledger events but are not ARCOS transactions.
Selection therefore has two filters: a schedule filter (is this substance in ARCOS scope?) and an action filter (is this action an acquisition or disposition, not an internal move?). Both filters read only committed ledger fields, so the selection is reproducible.
The ARCOS Transaction Record Layout
ARCOS transaction records are fixed-width: each field occupies a defined column range, numeric fields are zero-padded and right-justified, and text fields are space-padded and left-justified. The record identifies the reporting registrant, the transaction type, the drug, the quantity, the associated registrant, and the date. The essential fields are mapped below; the byte-exact serialization — widths, padding direction, and truncation hazards — is the subject of the child recipe, Generating ARCOS Fixed-Width Transaction Files.
| Field | Content | Format |
|---|---|---|
| Reporter DEA registration | The filing registrant’s DEA number | 9 chars, left-justified text |
| Transaction code | Acquisition/disposition type (e.g. purchase, sale, disposal, loss) | 1–2 digit code |
| Action indicator | Acquisition (A) vs disposition (D) |
1 char |
| NDC / drug code | 11-digit NDC identifying the product | 11 digits, zero-padded |
| Quantity | Units transacted, implied-decimal | numeric, zero-padded right-justified |
| Associated registrant | DEA number of the counterparty | 9 chars, left-justified text |
| Transaction date | Date the transaction occurred | YYYYMMDD |
Reporting Frequency and Batch Submission
ARCOS reporting cadence depends on the registrant’s designated reporting frequency; distributors and manufacturers typically report on a monthly or quarterly basis, with each period’s transactions assembled into a batch. The generator does not decide the cadence — it accepts a reporting-period boundary and selects the committed events whose transaction date falls inside it. Because selection is by committed event, a period never overlaps or double-counts: an event belongs to exactly one period, determined by its immutable occurred_at.
Batch submission concatenates the period’s transaction records into a single fixed-width file, computes a SHA-256 digest over the file bytes for the audit trail, and transmits the batch through the DEA’s ARCOS submission channel. The acknowledgment is retained alongside the file’s hash so the filing’s provenance — which events, which bytes, which acknowledgment — is fully reconstructable.
Deterministic Assembly Workflow
The pipeline drives each reporting run through explicit, ordered stages so the output depends only on the frozen input, never on iteration order or wall-clock timing:
- Snapshot — freeze the ledger up to the reporting-period boundary so the input set cannot drift mid-run.
- Schedule filter — keep only substances in ARCOS scope, discarding Schedule IV–V and non-controlled items.
- Action filter — keep only acquisitions and dispositions, discarding internal moves that do not change total on-hand.
- Map — resolve each event’s transaction code, action indicator, normalized NDC, quantity, associated registrant, and transaction date.
- Sort — order records deterministically (by sequence) so the emitted file is reproducible.
- Assemble — render each record and concatenate the batch.
- Hash & sign — compute the SHA-256 digest over the file bytes and bind it to the ledger event set for the audit trail.
Production Python Implementation
The assembler below selects reportable acquisition/disposition events from a frozen ledger snapshot, maps each to an ARCOS transaction record, and emits the batch with a SHA-256 audit hash and PHI-free structured logging. It focuses on selection and record assembly; the byte-exact fixed-width serialization is deferred to the child recipe. The code realizes the workflow above and satisfies the reproducibility standard behind 21 CFR § 1304.33.
from __future__ import annotations
import hashlib
import logging
from dataclasses import dataclass
from datetime import date, datetime, timezone
from enum import Enum
from typing import Iterable, Sequence
# Structured logging only — transaction codes and hashes, never patient data.
logger = logging.getLogger("pharmacy.reporting.arcos")
# Substances the DEA designates in ARCOS scope, by schedule. Selected III included.
ARCOS_SCOPE_SCHEDULES = {"I", "II", "III_SELECTED"}
class ArcosAction(str, Enum):
ACQUISITION = "A"
DISPOSITION = "D"
# Ledger action -> (ARCOS action indicator, ARCOS transaction code).
_TXN_CODE = {
"RECEIVING": (ArcosAction.ACQUISITION, "P"), # purchase / receipt
"RETURN_IN": (ArcosAction.ACQUISITION, "R"), # return received
"SALE": (ArcosAction.DISPOSITION, "S"), # sale / transfer out
"DESTRUCTION": (ArcosAction.DISPOSITION, "Y"), # disposal
"LOSS": (ArcosAction.DISPOSITION, "Z"), # theft / loss
}
# Internal moves are intentionally absent: they are not ARCOS transactions.
@dataclass(frozen=True)
class LedgerEvent:
"""A committed, immutable ledger event. The report never mutates it."""
sequence: int
ndc_11: str
schedule: str
action: str
quantity: float
occurred_at: datetime # UTC, tz-aware
associated_dea: str # counterparty DEA number
chain_hash: str
@dataclass(frozen=True)
class ArcosRecord:
"""One assembled ARCOS transaction, pre-serialization."""
reporter_dea: str
action_indicator: str
transaction_code: str
ndc_11: str
quantity: float
associated_dea: str
transaction_date: date
def _in_period(event: LedgerEvent, start: date, end: date) -> bool:
d = event.occurred_at.date()
return start <= d <= end
def select_reportable(
events: Sequence[LedgerEvent], start: date, end: date
) -> list[LedgerEvent]:
"""Filter to ARCOS-scope acquisition/disposition events in the reporting period."""
selected = [
e for e in events
if e.schedule in ARCOS_SCOPE_SCHEDULES
and e.action in _TXN_CODE
and _in_period(e, start, end)
]
# Deterministic order so the emitted batch is reproducible.
return sorted(selected, key=lambda e: e.sequence)
def assemble_records(
events: Iterable[LedgerEvent], reporter_dea: str
) -> list[ArcosRecord]:
"""Map each reportable ledger event to an ARCOS transaction record."""
records: list[ArcosRecord] = []
for e in events:
action, code = _TXN_CODE[e.action]
records.append(
ArcosRecord(
reporter_dea=reporter_dea,
action_indicator=action.value,
transaction_code=code,
ndc_11=e.ndc_11,
quantity=e.quantity,
associated_dea=e.associated_dea,
transaction_date=e.occurred_at.date(),
)
)
return records
def _serialize(record: ArcosRecord) -> str:
"""Reference field ordering. Byte-exact fixed-width widths live in the child recipe."""
q = int(round(record.quantity))
return (
f"{record.reporter_dea}|{record.action_indicator}|{record.transaction_code}|"
f"{record.ndc_11}|{q}|{record.associated_dea}|"
f"{record.transaction_date:%Y%m%d}"
)
def build_batch(
events: Sequence[LedgerEvent], reporter_dea: str, start: date, end: date
) -> tuple[bytes, str]:
"""Return the ARCOS batch bytes and a SHA-256 audit hash over them."""
reportable = select_reportable(events, start, end)
records = assemble_records(reportable, reporter_dea)
body = ("\n".join(_serialize(r) for r in records) + "\n").encode("utf-8") if records else b""
audit_hash = hashlib.sha256(body).hexdigest()
logger.info(
"arcos_batch_built reporter=%s period=%s/%s records=%d audit_hash=%s",
reporter_dea, start.isoformat(), end.isoformat(), len(records), audit_hash,
)
return body, audit_hash
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
now = datetime(2026, 7, 10, 14, 0, tzinfo=timezone.utc)
events = [
LedgerEvent(1, "00093-0058-01", "II", "RECEIVING", 500, now, "PM9988776", "h1" * 32),
LedgerEvent(2, "00093-0058-01", "II", "SALE", 120, now, "PB5544332", "h2" * 32),
LedgerEvent(3, "00527-1355-01", "IV", "SALE", 90, now, "PB5544332", "h3" * 32), # out of scope
]
body, audit_hash = build_batch(
events, reporter_dea="PM1234567", start=date(2026, 7, 1), end=date(2026, 7, 31)
)
print(body.decode())
print("audit_hash:", audit_hash)
# The Schedule IV sale is filtered out; re-running yields the identical hash.
_, again = build_batch(events, "PM1234567", date(2026, 7, 1), date(2026, 7, 31))
assert again == audit_hash
The assembler keeps two properties visible: the Schedule IV sale never appears because it falls outside ARCOS scope, and the final assertion proves that re-running against the same events reproduces the audit hash — the reproducibility that makes the filing defensible.
Compliance Mapping & Audit Boundaries
The ARCOS file is a derived artifact; the ledger is the legal record. Every reported transaction binds a committed ledger event — its normalized NDC, quantity, counterparty, and date — into a fixed-width line, and the batch as a whole is bound to its input set by the SHA-256 audit hash. That hash is retained with the DEA acknowledgment, so the filing inherits the same access rules the platform’s audit boundary definition establishes: field-level RBAC, encryption at rest, and immutable access logs. Nothing in an ARCOS record is authored at reporting time; a correction is a new forward-chained ledger event that regenerates a corrected batch, never an in-place edit of a filed file.
Error Handling & Offline Resilience
ARCOS pipelines fail in a small set of predictable ways, and each has a deterministic destination:
- Out-of-scope inclusion — a Schedule IV item slips into a batch because a schedule was misclassified upstream. The fix is to correct the classification and regenerate; the reproducible pipeline guarantees the corrected batch is complete.
- Malformed record rejection — the ARCOS processor rejects a batch for a field-width or transaction-code violation. Because the file is a projection, the mapping is corrected and the batch regenerated rather than hand-edited, then resubmitted.
- Directory or connectivity outage — when the ARCOS submission channel or an NDC directory lookup is unreachable at reporting time, the batch is built against a cached reference snapshot and held for deferred submission; the raw ledger events are already durable, so nothing is lost. The deferred-submission and replay policy follows the Fallback Routing for Offline Sync strategy, and every retry is logged.
- Duplicate submission — a re-sent batch is deduplicated by its SHA-256 audit hash, so an idempotent resubmission after a timeout never double-reports.
Downstream Integration
Once assembled, the ARCOS artifact feeds the submission and retention subsystems that act on it:
- Batch serialization — the assembled records are rendered to byte-exact fixed-width form and hashed for transmission; the widths, padding, and truncation rules are the subject of Generating ARCOS Fixed-Width Transaction Files.
- Retention — the signed, hashed batch and its acknowledgment are written to WORM storage to satisfy the two-year retrieval standard of
21 CFR § 1304.04. - Reconciliation — reported acquisitions and dispositions are cross-checked against the perpetual-inventory projection so that a discrepancy surfaces as a reconciliation exception rather than an ARCOS anomaly discovered by the DEA.
By selecting reportable events from an immutable ledger, mapping them deterministically, and hashing the batch, the ARCOS pipeline converts committed transactions into a filing that reconciles to inventory and reproduces on demand — satisfying 21 CFR § 1304.33 without a single hand-authored line.
Frequently Asked Questions
Which substances must be reported to ARCOS?
ARCOS covers all Schedule I and II controlled substances, plus the specific Schedule III substances and selected products the DEA designates as reportable. Schedule IV–V and non-controlled items are out of scope. The generator applies a schedule filter that reads each event’s classification, so an item’s inclusion is determined by committed ledger data rather than a reporting-time judgment call.
How often are ARCOS transactions reported?
The cadence depends on the registrant’s designated reporting frequency — commonly monthly or quarterly — and each period’s transactions are assembled into one batch. The generator selects events by their immutable transaction date, so every event belongs to exactly one period and periods never overlap or double-count.
Why assemble ARCOS records from the ledger instead of a database export?
Because a report must be reproducible to be defensible. A database export reflects a mutable table that can drift between filing and inspection, whereas a projection of the append-only ledger folds an immutable, hash-chained input into a byte-identical output every time. Re-running the assembler in front of an inspector reproduces the filed batch exactly.
What happens when the ARCOS processor rejects a batch?
The rejection identifies a malformed record or an invalid transaction code. Because the batch is a projection, the fix is to correct the mapping or classification and regenerate the whole file, then resubmit; the file is never hand-edited. Deduplication by the batch’s SHA-256 audit hash ensures an idempotent resubmission does not double-report.
Related
- Regulatory Reporting & DEA Submissions — parent area this reporting pipeline operates within
- Generating ARCOS Fixed-Width Transaction Files — byte-exact serialization of the assembled records
- NDC-11 vs NDC-10 Parsing Standards — normalization of the NDC that keys each ARCOS record
- DEA Schedule II–V Classification Mapping — schedule resolution that drives the ARCOS scope filter
- Fallback Routing for Offline Sync — deferred submission and replay when the channel is unreachable