Plaid Mining
Mining fetches transactions, accounts, balances, liabilities, and KYC results from Plaid and writes them to DynamoDB. It is triggered by Plaid webhooks and by synthetic webhooks from the API and the listener.
Pipeline
| Stage | Component | Input | Output |
|---|---|---|---|
Intake |
webhook |
|
Message on |
Synthetic intake |
api, listener |
Item add, Layer creation, remine; Auth0 login |
Message on |
Route and fetch |
miner |
Webhook message |
Plaid calls; raw records on |
Paginate |
throttle-miner |
|
Next page on Kinesis; further |
Refine |
refiner |
Kinesis records |
|
Feed |
feeder |
DynamoDB Streams |
|
The miner and throttle-miner report failed messages as batch item failures; a message that fails five times lands in its queue’s DLQ. The refiner processes records in order with fmsdk kinesis.ProcessRecords; the first record that fails is reported by its sequence number as the batch item failure, and Lambda bisects the batch and retries from that record. No retry limit or failure destination is configured, so a failed record is retried until it expires from the stream. A record whose failure a retry cannot fix is logged and skipped instead, and its data is not written: a record that does not decode, Plaid data that cannot be converted to rows (nil transaction, bad date, unknown currency), or a DynamoDB ValidationException. Records that fail to refine count toward txn.refiner.record.failed, tagged result:retried (with throttle:true|false) or result:skipped. The listener logs processing errors and does not retry.
Webhook Intake
The webhook Lambda serves POST /v1/plaid (spec: pkg/webhook/spec/plaid.yaml).
-
Plaid-Verificationheader missing:401. -
Verify the header as an ES256 JWT against Plaid’s JWK for its
kid, and the request body against the token’s body hash. A rejected token or hash mismatch:401. An error reading the body or running the validation:500. -
Body is not JSON:
422. -
Send the payload to
site-txn-plaid-webhooks; failure:500. Success:200.
Source: pkg/webhook/middleware.go, pkg/webhook/server.go
Miner Routing
For each queue message:
-
Parse.
webhook_typemust beTRANSACTIONS,ITEM,AUTH,LIABILITIES,IDENTITY_VERIFICATION, orSCREENING; anITEMcode must beWEBHOOK_UPDATE_ACKNOWLEDGED,LOGIN_REPAIRED,USER_PERMISSION_REVOKED,USER_ACCOUNT_REVOKED, orERROR. Anything else is dropped without retry. Source:pkg/plaid/webhook.go. -
WEBHOOK_UPDATE_ACKNOWLEDGED: done. -
IDENTITY_VERIFICATIONandSCREENINGcarry no item id and go straight to KYC webhooks. -
Look up the item by id; not found: dropped.
-
Item is
REMOVEDandsource=FROM_LISTENER: dropped. -
Refresh the item from Plaid:
item_error_code, status (ERRORwhen a code is present, elseACTIVE), available and billed products, last successful and failed update times,item_last_webhook_sent_at. -
Route by type per the tables below. Every handled webhook writes a
WEBHOOK_INFOrow.
A user.ErrNotFound from the User Service drops the message; other errors return it for retry.
TRANSACTIONS
| Code | Window | Action |
|---|---|---|
|
14 days |
Mine; a Plaid |
|
180 days |
Mine |
|
30 days |
Mine |
|
— |
Put |
Source: pkg/miner/transactions.go
Pagination
A mine calls Plaid for up to 500 transactions at the request’s offset, puts the response on Kinesis with last_page = total - offset ⇐ 500, and, when more remain, sends a MineRequest (item_id, user_id, start_date, end_date, offset + page length) to site-txn-plaid-throttle. The queue’s 30-second delivery delay spaces the pages. The throttle-miner repeats the same step until last_page. The WEBHOOK_INFO row for the webhook records the first page’s totals and accounts.
ITEM
| Code | Action |
|---|---|
|
Segment event |
|
Segment event |
|
Revoke the token at Plaid; set |
|
Publish |
|
No action |
Source: pkg/miner/item.go, pkg/miner/events.go
AUTH
DEFAULT_UPDATE: when account_ids_with_updated_auth includes the item’s main account, call the Payments Service to delete the stored bank account so the next fetch re-reads the numbers from Plaid. Other accounts: no action.
LIABILITIES
DEFAULT_UPDATE: when ENABLE_LIABILITY_MINING is off (prod), return without writing. Otherwise fetch liabilities for the new and updated account ids and store each as a current row plus a history row — Liability.
Listener
Auth0 login events reach site-txn-listener-eventbridge through the rule in EventBridge.
-
Parse;
detail.data.typemust begd_auth_succeed,sepft, orsertft(case-insensitive), else skip. The user id isdetail.data.user_idwith anyauth0|prefix stripped. -
Read the user’s items from
site-txn-plaid; none: skip. Items whose status is notACTIVEare logged and skipped; the remaining steps run once per item. -
Read the item’s
TRANSACTIONSWEBHOOK_INFOrows (GSI2) and decide:
| History | Action |
|---|---|
A |
Skip |
No |
Synthetic |
Anything else, including no rows |
Synthetic |
Synthetic messages carry source=FROM_LISTENER. Processing errors are logged and the message is not retried.
Source: pkg/listener/listener.go
Refiner
For each Kinesis record:
-
Parse the wrapper (
user_id,item_id,transactions,removed,last_page). -
When a transactions response is present:
-
Each account becomes an
ACCOUNTrow and aBALANCErow (accounts with an unknown currency are skipped). -
Each transaction becomes a
TRANSACTIONrow, batch-written. Transactions that already have aREMOVEtombstone are re-tombstoned instead, and their ids are sent tosite-insight-replay-feederas{user_id, removed}. -
last_pagewith at least one transaction: publishuser_new_txns_batch_completed.
-
-
When
removedis present: write aREMOVEtombstone per id and mark existingTRANSACTIONrows as removed — Transaction.
Source: pkg/refiner/refiner.go
Feeder
| Stream record | Action |
|---|---|
TTL expiry of a |
For each stub entry with no saved |
TTL expiry of an |
A prequal item that still has a |
Insert or modify of an |
Build the account, set |
Anything else |
Skipped (and filtered before invocation — Event Source Mappings) |
Source: pkg/feeder/feeder.go