alpaca-broker-reconciliation-idempotency について
Alpaca Broker APIを統合する開発者向けのスキルです。冪等な書き込み、イベントの重複排除、定期的な照合でローカル状態を同期する方法を扱い、非同期決済やイベント欠落、状態訂正に備えます。
主な機能
- 01注文・ジャーナルの安全な再試行
- 02SSEイベントの重複排除と状態ガード
- 03期間指定による正本データとの照合
- 04イベントのない経路のポーリング
- 05状態マッピングと終端状態の管理
活用シーン
- 01資金移動や注文の再試行設計
- 02重複・欠落イベントからの状態復旧
- 03口座・ジャーナル・送金記録の定期照合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ alpacahq/ alpaca-skills --skill alpaca-broker-reconciliation-idempotencyインストール後、Claude Code で/alpaca-broker-reconciliation-idempotencyを入力して読み込まれたか確認します。
同梱ファイルreference.md
詳細
Alpaca — 照合と冪等性
デモと本番運用を分けるのが、このスキルです。Alpaca は非同期で、結果整合性を持つシステムです。書き込みが反映されるまで時間がかかり、イベントが欠落したり再送されたりすることがあり、イベントを一切送信しない決済経路もあります。また、「実行済み」が後から取り消されることもあります。ローカルデータベースを、Alpaca の状態を忠実に反映し、自動修復できるミラーにすることが役割です。
ベース URL と認証(クライアントクレデンシャルの Bearer トークン。従来の Basic も引き続き利用可能)については、最初に
alpaca-broker-integrationを読み、その後alpaca-broker-sse-eventsと関連するドメインスキルを読んでください。このスキルは、それらを結び付けるアーキテクチャです。
中核となる原則
Alpaca を信頼できる唯一の情報源として扱い、DB は Alpaca の状態に収束させる必要があるキャッシュとして扱ってください。 すべての書き込みはリクエストであり、確定した事実ではありません。すべてのイベントは手掛かりであり、保証ではありません。正確性は、冪等な処理と照合ループによって確保されます。単一の呼び出しやイベントが一度だけ正確に成功したと仮定してはなりません。
1. 3 層の防御
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
3 つすべてが必要です。レイヤー 1 と 2 で通常時やリトライ時の正確性を保ち、レイヤー 3 でそれでも起こる問題(停止、バグ、イベントの欠落、修正)をすべて検出します。
2. レイヤー 1 — 冪等な書き込み
タイムアウトと成功を見分けることはできないため、すべての資金・注文関連の書き込みは安全に再試行できなければなりません。
- 注文: 取引 ID から生成した独自の
client_order_id(最大 128 文字)を設定します。応答を受け取れなかった場合は、再試行する前にorders:by_client_order_idで注文を検索します。 - ジャーナル:
Idempotency-Keyヘッダーを送信します。同じキーと同じ本文なら元のジャーナルが返されます。同じキーで本文が異なる場合は422になります。(alpaca-broker-journalsを参照してください。) - ローカル優先の順序付け: ネットワーク呼び出しの前に、生成したキーを含む処理意図のレコードを書き込みます。これにより、呼び出し中にクラッシュしても照合可能な記録が残り、見つけられない Alpaca オブジェクトが孤立することを防げます。
- 返された Alpaca ID をすぐに永続化します(
account_id、order_id、journal_id、transfer_id)。イベントとの相関や照合に使えるキーは、これだけです。
3. レイヤー 2 — 冪等なイベント取り込み
イベントは少なくとも 1 回配信されます。再生カーソル、再接続、修正によって、同じイベントが複数回届くことがあります。
- 重複排除 ID をキーに、スナップショットを先に保存します。 upsert または重複時のスキップを使って未加工のイベントを挿入し、重複を何もしない処理にします。キーは慎重に選んでください。
event_id(ULID)は再生カーソルです。Activity SSE では、重複排除キーはref_idです。これは、再生時の少なくとも 1 回の再配信と、previous_idによる修正・取り消しの関連付けに使う、文書化されたキーです。従来のステータスストリームでは、event_idが両方の役割を果たします。 - 業務レコードはローカルのオートインクリメント値ではなく Alpaca ID をキーにして、upsert セマンティクスを使います。
- 現在のステータスに基づいて遷移を制御します。 処理を行う前に、レコードがすでに終端状態ではないことを確認してください。これにより、重複した「実行済み」や「約定済み」で支払い処理や通知が再実行されるのを防げます。
- レコードごとの変更を直列化します。 Alpaca はアカウント単位のイベント順序を保証しているため、コンシューマーを
account_idでパーティション分割するのが最も簡単な標準的な方法です。代替手段として、SELECT … FOR UPDATEによる行ロックや楽観的バージョン列があります。 - 成功した後にのみカーソルを進めます(少なくとも 1 回の配信を、上記の方法で安全に処理します)。
4. レイヤー 3 — 照合/修復ジョブ
Alpaca から信頼できる状態を再取得し、ローカルにアップサートする定期ジョブです。これにより、システムは自己修復できるようになります。
正規の期間指定による修復(教訓):
- Activity SSE(
GET /v2beta1/events/activities)から、since/untilの範囲を指定してアクティビティを再取得します。範囲を指定した再生はサブスクリプションではなくバッチジョブです。untilに達すると、ストリームは200で終了し、since/untilは業務時刻(at)で絞り込みます。(GET /v1/accounts/activitiesは完全に置き換えられたとされていますが、2026-02-11 より前に記帳されたアクティビティを取得できる唯一のソースです。その日より前まで遡る期間には、こちらを使用してください。) - 同じ期間のジャーナル(
GET /v1/journals)と振替をページネーションで取得します。 - Alpaca ID をキーとして、各項目をテーブルにアップサート(挿入または更新)します。再実行しても安全で、状態は収束します。
- 同時実行数(小規模なワーカープール)を制限し、レート制限を守ります(
alpaca-broker-rate-limits-resilience)。
単に「前回の実行以降」ではなく、期間を指定する理由:毎晩すべての履歴を再スキャンせずに、修正、遅延決済、デプロイ中に取りこぼしたイベントを取り込めるためです。想定される最長の停止時間より長い期間を選択してください。
5. イベントがない経路のポーリング
すべての項目が SSE を発行するわけではありません。イベントがない場合は、ポーリングが必須です。
- 資金調達ウォレットの振替ごとのステータス(v1beta)はプッシュされないため、スケジュールに従って
GET /v1beta/.../funding_wallet/transfers/{id}をポーリングします。 - ポーラーの実行を期間内でずらし、すべての経路が同時に呼び出されて API 負荷が急増しないようにします。
- 終端状態でないレコードのみをポーリングします。 クエリを
status IN (pending, processing, …)で絞り込みます。レコードが終端状態になったら、ポーリング対象から除外します。これにより、作業量を抑え、通知の重複を防げます。 - 注意点 — 従来の電信送金には ID 指定の GET がありません:
GET /v1/accounts/{id}/transfers?direction=OUTGOING(一覧)を実行し、クライアント側で ID を照合する必要があります。実行中はアカウントごとに一覧をキャッシュしてください。
教訓: ポーリングには遅延が伴います。期待される遅延(例:「出金ステータスは約 1 時間以内に更新」)を文書化し、プロダクト/サポート担当者が適切な期待値を設定できるようにしてください。
6. ステータス状態機械のマッピング
Alpaca は複数のステータス列挙型(口座、注文、ジャーナル、振替、資金調達ウォレット)を公開しており、それぞれ用語が異なります。アプリ内に Alpaca の文字列を直接散在させないでください。
- ドメインごとに、Alpaca のステータスから内部ステータスへの明示的なマッピングテーブルを定義します(例:振替の
COMPLETE→COMPLETED、REJECTED/CANCELED/RETURNED→CANCELLED)。 - 大文字・小文字の使い方と値の集合は統一されていません。 振替のステータスは大文字で、
RETURNEDが含まれます。failedはありません。ジャーナルのステータスは小文字です。REST のTransferStatus列挙型と資金調達イベントの列挙型は、そもそも同じ値の集合ではないため、両方の和集合をマッピングしてください。 - 未知のステータスは適切に処理します。ログに記録してスキップし、決してクラッシュさせないでください。Alpaca は時間の経過とともに値を追加します。
- どの状態が終端状態か把握します(列挙型ごとに異なります)。そうすることで、それらのポーリング/処理を停止できます。
7. 設計時に考慮すべき結果整合性の落とし穴
executed/COMPLETEが常に最終状態とは限りません — ジャーナルは出納処理によって取り消される場合があり、振替は完了したように見えた後でRETURNEDになることがあります。「正常な」終端状態を過ぎても、一定期間は照合を続けてください。- 修正によって新しい ID が作成されます。 ジャーナルの
correctは元のジャーナルをキャンセルし、実際の資金を反映した新しいジャーナル ID を発行します。イベントのスナップショットと Alpaca ID をキーにした照合ならこれに対応できますが、元のレコードを直接変更するロジックでは対応できません。 200は受け付けられたことを意味し、決済済みを意味するわけではありません。 作成レスポンスだけで資金がユーザーに移動したと確定してはいけません。終端イベントまたはポーリング結果に基づいて確認してください。- イベントの順序が前後したり重複したりするのは通常のことです(
alpaca-broker-sse-events§5 を参照)。冪等性によって吸収できます。
8. アンチパターン(実際に見られた例)
- ❌ since カーソル(v2 では
since_id、v1 ではsince_ulid)を指定せずに SSE ストリームを再接続する → 切断中のすべてのイベントが通知されないまま失われます。(修正方法:カーソルを永続化して再生します。) - ❌ SSE ストリームだけを唯一のデータソースとして扱う → 取りこぼしたイベントを補う手段がありません。(修正方法:修復ジョブを追加します。)
- ❌ 冪等性キーを使わずにジャーナル/注文を無条件に再試行する → 資金移動が二重に発生します。
- ❌
executedを取り消し不能な状態として扱う → 反対仕訳/訂正が届いたときに帳簿が不整合になります。 - ❌
seenセットではなくconfirmedセットをキーに重複を排除する → 判定/却下の繰り返しによって、処理がいつまでも再実行されます。確認したすべてのイベントを対象に重複を排除し、Alpaca/イベント ID をキーにします。
9. 全体をまとめる
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: windowed re-pull of activities/journals/transfers → upsert
MAP: Alpaca status → internal status, terminal-aware, unknown-tolerant
関連スキル: イベント消費の仕組み → alpaca-broker-sse-events;ドメインごとの冪等性キー → alpaca-broker-journals、alpaca-broker-trading-orders;ポーリングの仕組み → alpaca-broker-funding-transfers;修復ジョブでのバックオフとレート制限 → alpaca-broker-rate-limits-resilience。