Skip to main content

Outgoing offers and evaluation

Outgoing proposals use deterministic scoring before staging an offer for human approval. Use this page to understand package ranking and the difference between an approved receipt and an ESPN submission.

Which evaluation do you need?​

EvaluationInputsOutput
POST /v1/trade/evaluate; MCP evaluate_proposed_tradeInternal incoming/outgoing IDs, exact season/weekRaw VORP totals and missing IDs
Trade scoreESPN IDs, current ownership, horizon and schedulesOptimal starter impact, G*, C* and coverage
incoming_total = sum(available incoming VORP)
outgoing_total = sum(available outgoing VORP)
delta_vorp = incoming_total − outgoing_total

Duplicate IDs and IDs on both sides are rejected. Absent rows contribute nothing and appear in missing_player_ids; raw evaluation does not simulate slots, schedules or drops.

Which generated packages qualify?​

The worker targets up to three deficit teams and asks for at most six packages. Our outgoing side must include the target's deficit position.

GateRequirement
IdentityDistinct, correctly owned ESPN IDs; no side overlap
CoverageAt least one scored week; no unmapped traded players
Our gainRaw lineup gain >0
Their gainRaw lineup change ≥0
CapacityNeither side exceeds its roster limit
Final score/horizonPositive weighted score and complete horizon are not required

No eligible package means no staged offer. Unavailable scoring can fail the run rather than stage an unscored claim.

How does the worker rank packages?​

PriorityRule
1. Score tierNumeric final_score packages precede unweighted our_lineup_gain packages
2. ValueChosen score divided by scored-week count, descending
3. Tie-breaksMore scored weeks, complete horizon, larger counterparty gain, then canonical package key

Incoming inbox ranking uses total final score instead. The outgoing ranking keeps weighted and unweighted values in separate tiers.

Pitch, staged evidence and offer identity
EvidenceContents
PitchDeterministic score facts with cited evidence IDs
proposal-evaluation-v1Exact request/response, target (deficit fields only for deficit-sourced targets), ranking basis/value/counts and pitch evidence paths
Pitch pathsJSON Pointers into the stored score
Invalid/absent evaluationUnavailable panel; no inference from LLM text
GET /v1/trade/offers IDsESPN IDs even in incoming_player_ids/outgoing_player_ids
offer_keyDigest of identity, player sides, exact message and related transaction ID for counters
Thread rootTracing metadata outside the digest

Editing the message at approval creates a new key over the exact edited text. Core checks configured league/team, player sides and source; conflicting staged copies fail closed.

What happens when you approve?​

RuleBehavior
RoutePOST /v1/trade/offers/{offer_key}/approve
ApproverNonblank self-reported approved_by audit label
Optional messagemessage: nonblank after whitespace check; changed text is limited to 1000 characters; exact text is preserved
AuthenticationNo application-layer auth; deployment trust boundary applies
LockRead all rows for key under PostgreSQL advisory lock
Eligible stateawaiting_approval
Blocking historyPending, submitting or submitted row prevents new publication
Committed before sendExact payload, approver, time, tracking ID and bot_decisions audit
New confirmed publication202, approved
Duplicate approval200, original receipt, no send

An omitted or null message, or text exactly equal to the staged message, keeps the original key and approval behavior. For changed text, core creates a replacement row, marks all staged copies of the original key superseded, and commits the replacement claim and audit in the same transaction. The published payload contains the edited message and approval for the new key; the response's offer_key is that new key. An existing offer under the edited key returns 409 rather than merging histories.

How is an offer resolved?​

Core cannot see ESPN, so the worker reports the end state of a sent offer with POST /v1/trade/offers/{offer_key}/resolution (X-API-Key required):

{"outcome": "declined", "resolved_at": "2026-10-22T03:00:00Z", "espn_transaction_id": "tx-9"}

outcome is accepted, declined, expired, withdrawn, vetoed or cancelled; resolved_at and espn_transaction_id are optional. The marker is stored at outcome_details.resolution on every sent row for the key and the row's status is unchanged. Repeating the same outcome returns the stored marker with duplicate: true; a different outcome is 409, as is an offer that was never approved. An unknown key is 404. GET /v1/trade/offers returns it as resolution (null while pending).

Until resolved, the offer's outgoing players are committed (see trade context) and are not sent in other trades.

GET /v1/trade/offers?status=approved exposes drafted_message (the original generated text, which can be null) and message_edited: true on edited rows. Unedited rows return drafted_message: null, message_edited: false and edited_by: null. Edited rows expose edited_by, the self-reported label of the person who changed the message. approval.approved_by identifies the latest successful approver; after a proven failed publish, a different person may re-approve without changing the editor. evaluation.pitch remains the generated, evidence-backed draft. Human edits are not validated against score evidence and do not change the stored evaluation.

Repeating the same edited approval through either the original or new key returns 200 with duplicate: true, the new key and the original receipt. Omitting the message on either key also returns that receipt. A different message after approval returns 409. Pending or ambiguous publication remains blocked. A proven pre-publication failure can retry the same edited offer through either key; the original draft stays superseded. Echoing the original draft through the original key also returns the replacement receipt, even when that draft exceeds 1000 characters. Confirmed duplicate receipts through the original key do not require the edited-key lock, so a worker claim does not block them. A busy edited-key lock still returns retryable 409 when a new publication is needed. If a later scan stages the original key again, its live staged row takes precedence over the historical redirect and can be approved independently.

The 1000-character limit is a fallback pending the ESPN request capture in fantasy-browser-worker#124. Blank or oversized text returns 422 before changing any rows.

An approved receipt is a broker receipt

Worker enable, dry-run, approval and execution gates still apply. For an ambiguous send or crash, reconcile broker, worker and ESPN evidence before releasing the indefinitely pending claim.