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
ARCOS reporting data flow from ledger dispositions and acquisitions to a submitted fixed-width file On the left, the append-only audit ledger emits two streams of committed events: acquisitions and dispositions. Both feed a schedule filter that keeps only ARCOS-scope Schedule I to II and selected III substances, discarding out-of-scope and internal-move events to a sidelined stream. The retained events enter the ARCOS record assembler, which resolves the NDC, transaction code, quantity, associated registrant, and date for each. The assembler emits fixed-width transaction records that are concatenated into a batch file, over which a SHA-256 audit hash is computed. The hashed file is submitted to the DEA ARCOS processor, which returns an acknowledgment; a rejected batch loops back to the assembler for a regenerated file rather than a hand edit. Audit Ledger acquisitions dispositions Schedule filter ARCOS scope I–II + sel. III Out of scope IV–V · internal moves Record assembler NDC · code · qty assoc. reg. · date Fixed-width file batch of records SHA-256 hash DEA ARCOS batch submission acknowledgment rejected → regenerate

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:

  1. Snapshot — freeze the ledger up to the reporting-period boundary so the input set cannot drift mid-run.
  2. Schedule filter — keep only substances in ARCOS scope, discarding Schedule IV–V and non-controlled items.
  3. Action filter — keep only acquisitions and dispositions, discarding internal moves that do not change total on-hand.
  4. Map — resolve each event’s transaction code, action indicator, normalized NDC, quantity, associated registrant, and transaction date.
  5. Sort — order records deterministically (by sequence) so the emitted file is reproducible.
  6. Assemble — render each record and concatenate the batch.
  7. 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.

python
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:

  1. 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.
  2. 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.
  3. 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.

Explore deeper

Related topics