Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/alpacahq-alpaca-broker-reconciliation-idempotency/SKILL.md| Model | Eval pass | Runs |
|---|---|---|
| gemini-3.6-flash | 100% | 6 |
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | 108% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 81% | 0% |
| case-20 | ✓→✗ | ▼ Worse | 62% | 0% |
| case-09 | ✓→✓ | = Same ✓ | 37% | 0% |
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 driftYou 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-tolerantRelated 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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-09 | pass→pass | 13,330 | 7,110 | -47% | 1 | 1 | 0% | 2,370 | 3,254 | +37% | 0 | 0 | — |
case-01 | fail→fail | 26,609 | 26,873 | +1% | 1 | 1 | 0% | 6,235 | 8,025 | +29% | 0 | 0 | — |
case-02 | pass→pass | 30,479 | 25,992 | -15% | 1 | 1 | 0% | 6,217 | 7,436 | +20% | 0 | 0 | — |
case-16 | pass→pass | 9,915 | 5,667 | -43% | 1 | 1 | 0% | 1,692 | 2,883 | +70% | 0 | 0 | — |
case-03 | pass→pass | 10,477 | 5,482 | -48% | 1 | 1 | 0% | 2,332 | 3,004 | +29% | 0 | 0 | — |
case-04 | pass→pass | 12,393 | 9,244 | -25% | 1 | 1 | 0% | 2,229 | 3,767 | +69% | 0 | 0 | — |
case-05 | pass→pass | 10,694 | 8,249 | -23% | 1 | 1 | 0% | 2,054 | 3,450 | +68% | 0 | 0 | — |
case-06 | fail→pass | 29,471 | 7,467 | -75% | 1 | 1 | 0% | 1,677 | 3,496 | +108% | 0 | 0 | — |
case-07 | pass→pass | 16,920 | 18,701 | +11% | 1 | 1 | 0% | 3,260 | 4,214 | +29% | 0 | 0 | — |
case-08 | pass→pass | 12,104 | 7,170 | -41% | 1 | 1 | 0% | 2,071 | 3,249 | +57% | 0 | 0 | — |
case-10 | fail→fail | 12,247 | 9,950 | -19% | 1 | 1 | 0% | 2,499 | 3,726 | +49% | 0 | 0 | — |
case-11 | fail→pass | 13,706 | 7,301 | -47% | 1 | 1 | 0% | 2,370 | 3,051 | +29% | 0 | 0 | — |
case-12 | pass→pass | 10,281 | 9,846 | -4% | 1 | 1 | 0% | 2,184 | 3,387 | +55% | 0 | 0 | — |
case-13 | fail→pass | 10,646 | 8,576 | -19% | 1 | 1 | 0% | 2,098 | 3,802 | +81% | 0 | 0 | — |
case-14 | pass→pass | 12,392 | 9,026 | -27% | 1 | 1 | 0% | 2,066 | 3,595 | +74% | 0 | 0 | — |
case-15 | pass→pass | 13,121 | 8,558 | -35% | 1 | 1 | 0% | 2,051 | 3,408 | +66% | 0 | 0 | — |
case-17 | pass→pass | 9,426 | 5,931 | -37% | 1 | 1 | 0% | 1,660 | 3,004 | +81% | 0 | 0 | — |
case-18 | pass→pass | 13,665 | 9,713 | -29% | 1 | 1 | 0% | 2,448 | 3,715 | +52% | 0 | 0 | — |
case-19 | pass→pass | 3,821 | 3,049 | -20% | 1 | 1 | 0% | 732 | 2,555 | +249% | 0 | 0 | — |
case-20 | pass→fail | 10,448 | 4,785 | -54% | 1 | 1 | 0% | 1,783 | 2,896 | +62% | 0 | 0 | — |
case-21 | pass→pass | 3,234 | 3,670 | +13% | 1 | 1 | 0% | 616 | 2,491 | +304% | 0 | 0 | — |
case-22 | pass→pass | 15,219 | 12,183 | -20% | 1 | 1 | 0% | 2,876 | 4,092 | +42% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted, and 21 counted toward the lift figure. The other 1 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +9 percentage points is the difference between those two pass rates over the 21 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.