Model a transaction — a trade, a free-agent signing, or an RFA offer sheet — and run it through the league's real validation rules.
The simulator and the league's transaction system run the same rubric: the page calls POST /api/validate/sign (or /api/validate/offer_sheet), which shares its validator with POST /api/transactions. A "legal" result here is what the office will accept. These endpoints are read-only — nothing entered here is submitted, recorded, or applied to a roster.
The declared method has to actually be available. Cap space is measured against Team Salary with cap holds included (§ 3.10) — an unrenounced hold occupies room until it's cleared, so a team that has renounced on the league sheet but not in the ledger will read as having no room here. NTMLE/TMLE/Room Exception are checked against the remaining season balance, and a generic "MLE" resolves to whichever bucket the team is already locked into. The NTMLE is unavailable at or above the 1st Apron (§ 3.3); the Room Exception requires Team Salary to have been more than a full NTMLE below the Cap at the start of the league year (§ 3.2). Bird Rights carries no dollar ceiling of its own, but is checked against real tenure (§ 3.8, below); minimum is checked by § 3.12.
The declared tier is checked against continuous service derived from the transaction ledger. Rights accrue from the most recent free-agent signing and carry through trades — § 6.2 recognises holding "Bird Rights with the team via trade" — while re-signing your own free agent continues the clock rather than restarting it. Signing with a different team, or a release, resets it. A drafted player's clock starts at their draft.
Only over-declaration is blocked, and only when tenure is definite. Claiming a higher tier than the ledger supports is an error; claiming a lower one is allowed. Where the record starts with a trade and no earlier history, the result is a lower bound — the acquiring team inherits accrual we can't see — so that only ever warns. Unverifiable tenure warns too, and is never treated as Non-Bird: a player with no record is usually a long-tenured one.
Retired players can't be signed, and neither can players already under contract to another team — acquire them by trade instead. A player carrying a UFA or RFA cap hold is a free agent (§ 3.10): their team holds the rights, so re-signing them is exactly what a signing is for. The player dropdown is filtered to match, and an RFA offer sheet only lists genuine restricted free agents, since under § 3.15 nobody else can receive one.
An offer sheet is validated against whichever team ends up signing the player: the retaining team if the outcome is a match, the offering team if not. A match keeps the player on a roster spot they already occupy, so it doesn't add to the roster count; a non-match does. Everything else about § 3.15 — the 48-hour match clock, good-faith fit, Gilbert Arenas eligibility — stays manual review and is not checked here.
Not modeled: § 3.7 DPE (no exception type exists). Tenure derivation currently resolves 483 of 487 rostered players; where it can't, you get a warning rather than a verdict.
The page calls POST /api/validate/extension, which shares its validator with POST /api/transactions. An extension is fundamentally different from a signing: it adds years on top of a live contract rather than replacing a current-season figure, so this does not reuse the signing rubric — an earlier attempt at that reported a team getting $18.9M cheaper for extending a player. Only the incumbent may extend (§ 6.2 — no rival bid), so the team is derived from the roster once you pick a player.
The existing contract must be at least 3 years, and the player must be in its final year, or Year 4 of a 5-year deal. Contract start is derived from the transaction ledger the same way § 3.8 Bird tenure is — including a drafted player with no signing record, whose clock starts at the draft. Where the ledger has no record at all, the team's own attestation of when the deal began is used instead, always at warning severity (never trusted as fact) unless it contradicts other partial history the player does have, which errors.
New money must start the season after the existing deal's final guaranteed year — an option year (player or team) never counts as guaranteed, and is treated as declined outright by the act of extending, so the extension supersedes it. Submitting a different first season is an error naming the mismatch, since it corrupts every downstream cap figure.
Measured in the first extended season, not the current one — every extension targets 27-28 or later. Salary Cap / apron / hard-cap thresholds for those seasons are still $0 in Cap Settings as of this writing, so this check correctly reports cannot evaluate rather than a silent pass, until the committee enters real figures.
Not yet built: the § 4.5 six-month trade-freeze check (belongs in the trade validator, not here), and § 6.3's submission-window deadlines beyond the expiring-veteran June 30 date, which need a regular-season start date the system doesn't track yet.
The simulator and the league's transaction system run the same rubric: the page calls POST /api/validate/trade, which shares its validator with POST /api/transactions. A "legal" result here is what the office will accept. Picks are tracked but carry no cap or roster weight. With 3+ teams in the deal, each selected asset needs an explicit destination — with only 2, the destination is implied and there's nothing extra to pick.
A team's tier is set by its salary before the trade. Incoming ≤ outgoing always matches. In a multi-team deal, each team's own outgoing/incoming totals (across all its legs) are what's matched — not pairwise per partner.
Pick an exception under a team's selector to absorb that team's incoming salary against its remaining season balance instead of matching outgoing salary — outgoing sent back doesn't matter under this path. Eligibility mirrors free-agent use of the same exception: NTMLE is blocked at/above the 1st Apron, TMLE at/above the 2nd Apron, the BAE at/above the 1st Apron (and unavailable in consecutive seasons, or alongside cap space/Room Exception use), and the Room Exception requires Team Salary more than a full NTMLE below the Cap. Using the NTMLE, TMLE, or BAE this way triggers the same hard-cap-at-apron effect as a signing.
If a team has an unexpired TPE on file, it appears as an extra option in the same selector ("TPE: $X remaining"). A TPE covers incoming salary up to its remaining balance with no outgoing salary required — but it cannot be combined with another exception, or with any outgoing salary from that team, in the same trade.
Checking a Cap Hold player (a current free-agent hold — UFA/RFA — the team is carrying) reveals a contract-terms form: years (3–4), a per-year salary schedule, Bird-rights tier, and how the new deal is funded. Submitting with this filled in validates it as a sign-and-trade: contract length, the 5% raise cap (Bird's 8% doesn't apply here), that the MLE doesn't fund any part of it, that the signing team hasn't already used the Taxpayer MLE this season, Base Year Compensation where it applies, and that the receiving team ends up below the 1st Apron.