alpacahq/alpaca-broker-reconciliation-idempotency
Keep local state correct against the Alpaca Broker API — idempotency keys, ID-keyed upserts, event snapshotting and dedup, polling rails that have no events, nightly reconciliation/heal jobs, status state-machine mapping, and handling eventual consistency and corrections. Use when designing the data-correctness layer of any Alpaca integration in any language.
npx skills add https://github.com/alpacahq/alpaca-skills --skill alpaca-broker-reconciliation-idempotency
This is the skill that separates a demo from production. Alpaca is an asynchronous, eventually-consistent system: writes settle later, events can be missed or replayed, some rails emit no events at all, and "executed" can still be reversed. Your job is to make your local database a faithful, self-healing mirror of Alpaca's state.
> Read alpaca-broker-integration, alpaca-broker-sse-events, and the relevant domain skills first. This skill is the architecture that ties them together.
> Treat Alpaca as the source of truth and your DB as a cache that must converge to it. Every write is a request, not a fact. Every event is a hint, not a guarantee. Correctness comes from *idempotent* processing plus a *reconciliation* loop — never from assuming any single call or event succeeded exactly once.
Layer 1 — Idempotent writes : never create a duplicate when you retry
Layer 2 — Idempotent event intake: never double-process a replayed/duplicate event
Layer 3 — Reconciliation sweep : re-pull authoritative state and fix any drift
You need all three. Layer 1+2 keep you correct in the happy/retry case; Layer 3 catches everything that still slips through (downtime, bugs, missing events, corrections).
Every money/order write must be safe to retry, because you can't tell a timeout apart from a success.
client_order_id (≤128 chars) derived from your transaction ID. On a lost response, look the order up via orders:by_client_order_id before retrying.Idempotency-Key header. Same key + same body returns the original journal; same key + different body → 422. (See alpaca-broker-journals.)account_id, order_id, journal_id, transfer_id). It is your only correlation key for events and reconciliation.Events are at-least-once: replay cursors, reconnects, and corrections all cause the same event to arrive more than once.
event_id. Insert the raw event with upsert / skip-on-duplicate on event_id (a ULID). A duplicate becomes a no-op. This single unique constraint is your dedup boundary.SELECT … FOR UPDATE or your engine's equivalent) so concurrent events for one record serialize.A scheduled job that re-pulls authoritative state from Alpaca and upserts it locally. This is what makes the system self-healing.
Canonical nightly heal (lesson):
GET /v1/accounts/activities, paginated) for the last *N* days (e.g. 3) — a moving window that re-covers recent days so anything missed by SSE gets backfilled.GET /v1/journals) and transfers for the same window.alpaca-broker-rate-limits-resilience).Why a *window* and not just "since last run": it absorbs corrections, late settlements, and any events dropped during a deploy — without rescanning all history every night.
Not everything emits SSE. Where there's no event, you must poll.
GET /v1beta/.../funding_wallet/transfers/{id} on a schedule.:00, another at :30) to spread API load.status IN (pending, processing, …); once a record reaches a terminal status, drop it from the polling set. This bounds the work and prevents re-notifying.GET /v1/accounts/{id}/transfers?direction=OUTGOING (a *list*) and match the ID client-side; cache the list per account within a run.Lesson: polling implies latency. Document the expected lag (e.g. "withdrawal status updates within ~1h") so product/support set the right expectations.
Alpaca exposes several status enums (account, order, journal, transfer, funding-wallet) — each with its own vocabulary. Don't scatter raw Alpaca strings through your app.
executed/COMPLETE → COMPLETED; rejected/canceled/returned/failed → CANCELLED).TRD activity type that consumers had to filter out).executed/COMPLETE is not always final — journals can be reversed by cashiering; transfers can be RETURNED after appearing done. Keep reconciling past the "happy" terminal state for a window.correct cancels the original and issues a *new* journal ID carrying the real funds. Reconciliation keyed on event snapshots + Alpaca IDs handles this; logic that mutates the original record in place does not.200 means accepted, not settled. Never confirm money moved to a user off the create response — confirm off the terminal event/poll.alpaca-broker-sse-events §5). Idempotency absorbs them.since_id cursor → silently drops every event during the gap. (Fix: persist + replay the cursor.)executed as irreversible → broken books when a reversal/correction lands.confirmed set instead of a seen set → judge/reject churn re-processes forever. Dedup on *everything seen*, key on the Alpaca/event ID.WRITE: local intent row (idempotency key) → Alpaca call → store Alpaca ID
LIVE: SSE consumer (cursor-replay, snapshot-keyed dedup, status-guarded upsert)
POLL: schedulers for rails with no events (non-terminal records only)
HEAL: nightly window re-pull of activities/journals/transfers → upsert
MAP: Alpaca status → internal status, terminal-aware, unknown-tolerant
Related skills: event consumption mechanics → alpaca-broker-sse-events; idempotency keys per domain → alpaca-broker-journals, alpaca-broker-trading-orders; polling rails → alpaca-broker-funding-transfers; backoff & rate limits in heal jobs → alpaca-broker-rate-limits-resilience.
Take alpacahq/alpaca-broker-reconciliation-idempotency from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.