{"id":"d1b4ec87-6177-4b17-9a5b-dc70cefb1b06","shortId":"9znVBM","kind":"skill","title":"plaid-fintech","tagline":"Expert patterns for Plaid API integration including Link token","description":"# Plaid Fintech\n\nExpert patterns for Plaid API integration including Link token flows,\ntransactions sync, identity verification, Auth for ACH, balance checks,\nwebhook handling, and fintech compliance best practices.\n\n## Patterns\n\n### Link Token Creation and Exchange\n\nCreate a link_token for Plaid Link, exchange public_token for access_token.\nLink tokens are short-lived, one-time use. Access tokens don't expire but\nmay need updating when users change passwords.\n\n// server.ts - Link token creation endpoint\nimport { Configuration, PlaidApi, PlaidEnvironments, Products, CountryCode } from 'plaid';\n\nconst configuration = new Configuration({\n  basePath: PlaidEnvironments[process.env.PLAID_ENV || 'sandbox'],\n  baseOptions: {\n    headers: {\n      'PLAID-CLIENT-ID': process.env.PLAID_CLIENT_ID,\n      'PLAID-SECRET': process.env.PLAID_SECRET,\n    },\n  },\n});\n\nconst plaidClient = new PlaidApi(configuration);\n\n// Create link token for new user\napp.post('/api/plaid/create-link-token', async (req, res) => {\n  const { userId } = req.body;\n\n  try {\n    const response = await plaidClient.linkTokenCreate({\n      user: {\n        client_user_id: userId,  // Your internal user ID\n      },\n      client_name: 'My Finance App',\n      products: [Products.Transactions],\n      country_codes: [CountryCode.Us],\n      language: 'en',\n      webhook: 'https://yourapp.com/api/plaid/webhooks',\n      // Request 180 days for recurring transactions\n      transactions: {\n        days_requested: 180,\n      },\n    });\n\n    res.json({ link_token: response.data.link_token });\n  } catch (error) {\n    console.error('Link token creation failed:', error);\n    res.status(500).json({ error: 'Failed to create link token' });\n  }\n});\n\n// Exchange public token for access token\napp.post('/api/plaid/exchange-token', async (req, res) => {\n  const { publicToken, userId } = req.body;\n\n  try {\n    // Exchange for permanent access token\n    const exchangeResponse = await plaidClient.itemPublicTokenExchange({\n      public_token: publicToken,\n    });\n\n    const { access_token, item_id } = exchangeResponse.data;\n\n    // Store securely - access_token doesn't expire!\n    await db.plaidItem.create({\n      data: {\n        userId,\n        itemId: item_id,\n        accessToken: await encrypt(access_token),  // Encrypt at rest\n        status: 'ACTIVE',\n        products: ['transactions'],\n      },\n    });\n\n    // Trigger initial transaction sync\n    await initiateTransactionSync(item_id, access_token);\n\n    res.json({ success: true, itemId: item_id });\n  } catch (error) {\n    console.error('Token exchange failed:', error);\n    res.status(500).json({ error: 'Failed to exchange token' });\n  }\n});\n\n// Frontend - React component\nimport { usePlaidLink } from 'react-plaid-link';\n\nfunction BankLinkButton({ userId }: { userId: string }) {\n  const [linkToken, setLinkToken] = useState<string | null>(null);\n\n  useEffect(() => {\n    async function createLinkToken() {\n      const response = await fetch('/api/plaid/create-link-token', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ userId }),\n      });\n      const { link_token } = await response.json();\n      setLinkToken(link_token);\n    }\n    createLinkToken();\n  }, [userId]);\n\n  const { open, ready } = usePlaidLink({\n    token: linkToken,\n    onSuccess: async (publicToken, metadata) => {\n      // Exchange public token for access token\n      await fetch('/api/plaid/exchange-token', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ publicToken, userId }),\n      });\n    },\n    onExit: (error, metadata) => {\n      if (error) {\n        console.error('Link exit error:', error);\n      }\n    },\n  });\n\n  return (\n    <button onClick={() => open()} disabled={!ready}>\n      Connect Bank Account\n    </button>\n  );\n}\n\n### Context\n\n- initial bank linking\n- user onboarding\n- connecting accounts\n\n### Transactions Sync\n\nUse /transactions/sync for incremental transaction updates. More efficient\nthan /transactions/get. Handle webhooks for real-time updates instead of\npolling.\n\n// Transactions sync service\ninterface TransactionSyncState {\n  cursor: string | null;\n  hasMore: boolean;\n}\n\nasync function syncTransactions(\n  accessToken: string,\n  itemId: string\n): Promise<void> {\n  // Get last cursor from database\n  const item = await db.plaidItem.findUnique({\n    where: { itemId },\n  });\n\n  let cursor = item?.transactionsCursor || null;\n  let hasMore = true;\n  let addedCount = 0;\n  let modifiedCount = 0;\n  let removedCount = 0;\n\n  while (hasMore) {\n    try {\n      const response = await plaidClient.transactionsSync({\n        access_token: accessToken,\n        cursor: cursor || undefined,\n        count: 500,  // Max per request\n      });\n\n      const { added, modified, removed, next_cursor, has_more } = response.data;\n\n      // Process added transactions\n      if (added.length > 0) {\n        await db.transaction.createMany({\n          data: added.map(txn => ({\n            plaidTransactionId: txn.transaction_id,\n            itemId,\n            accountId: txn.account_id,\n            amount: txn.amount,\n            date: new Date(txn.date),\n            name: txn.name,\n            merchantName: txn.merchant_name,\n            category: txn.personal_finance_category?.primary,\n            subcategory: txn.personal_finance_category?.detailed,\n            pending: txn.pending,\n            paymentChannel: txn.payment_channel,\n            location: txn.location ? JSON.stringify(txn.location) : null,\n          })),\n          skipDuplicates: true,\n        });\n        addedCount += added.length;\n      }\n\n      // Process modified transactions\n      for (const txn of modified) {\n        await db.transaction.updateMany({\n          where: { plaidTransactionId: txn.transaction_id },\n          data: {\n            amount: txn.amount,\n            name: txn.name,\n            merchantName: txn.merchant_name,\n            pending: txn.pending,\n            updatedAt: new Date(),\n          },\n        });\n        modifiedCount++;\n      }\n\n      // Process removed transactions\n      if (removed.length > 0) {\n        await db.transaction.deleteMany({\n          where: {\n            plaidTransactionId: {\n              in: removed.map(r => r.transaction_id),\n            },\n          },\n        });\n        removedCount += removed.length;\n      }\n\n      cursor = next_cursor;\n      hasMore = has_more;\n\n    } catch (error: any) {\n      if (error.response?.data?.error_code === 'TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION') {\n        // Data changed during pagination, restart from null\n        cursor = null;\n        continue;\n      }\n      throw error;\n    }\n  }\n\n  // Save cursor for next sync\n  await db.plaidItem.update({\n    where: { itemId },\n    data: { transactionsCursor: cursor },\n  });\n\n  console.log(`Sync complete: +${addedCount} ~${modifiedCount} -${removedCount}`);\n}\n\n// Webhook handler for real-time updates\napp.post('/api/plaid/webhooks', async (req, res) => {\n  const { webhook_type, webhook_code, item_id } = req.body;\n\n  // Verify webhook (see webhook verification pattern)\n  if (!verifyPlaidWebhook(req)) {\n    return res.status(401).send('Invalid webhook');\n  }\n\n  if (webhook_type === 'TRANSACTIONS') {\n    switch (webhook_code) {\n      case 'SYNC_UPDATES_AVAILABLE':\n        // New transactions available, trigger sync\n        await queueTransactionSync(item_id);\n        break;\n      case 'INITIAL_UPDATE':\n        // Initial batch of transactions ready\n        await queueTransactionSync(item_id);\n        break;\n      case 'HISTORICAL_UPDATE':\n        // Historical transactions ready\n        await queueTransactionSync(item_id);\n        break;\n    }\n  }\n\n  res.sendStatus(200);\n});\n\n### Context\n\n- fetching transactions\n- transaction history\n- account activity\n\n### Item Error Handling and Update Mode\n\nHandle ITEM_LOGIN_REQUIRED errors by putting users through Link update mode.\nListen for PENDING_DISCONNECT webhook to proactively prompt users.\n\n// Create link token for update mode\napp.post('/api/plaid/create-update-token', async (req, res) => {\n  const { itemId } = req.body;\n\n  const item = await db.plaidItem.findUnique({\n    where: { itemId },\n    include: { user: true },\n  });\n\n  if (!item) {\n    return res.status(404).json({ error: 'Item not found' });\n  }\n\n  try {\n    const response = await plaidClient.linkTokenCreate({\n      user: {\n        client_user_id: item.userId,\n      },\n      client_name: 'My Finance App',\n      country_codes: [CountryCode.Us],\n      language: 'en',\n      webhook: 'https://yourapp.com/api/plaid/webhooks',\n      // Update mode: provide access_token instead of products\n      access_token: await decrypt(item.accessToken),\n    });\n\n    res.json({ link_token: response.data.link_token });\n  } catch (error) {\n    console.error('Update token creation failed:', error);\n    res.status(500).json({ error: 'Failed to create update token' });\n  }\n});\n\n// Handle item errors from webhooks\napp.post('/api/plaid/webhooks', async (req, res) => {\n  const { webhook_type, webhook_code, item_id, error } = req.body;\n\n  if (webhook_type === 'ITEM') {\n    switch (webhook_code) {\n      case 'ERROR':\n        // Item has entered an error state\n        await db.plaidItem.update({\n          where: { itemId: item_id },\n          data: {\n            status: 'ERROR',\n            errorCode: error?.error_code,\n            errorMessage: error?.error_message,\n          },\n        });\n\n        // Notify user to reconnect\n        if (error?.error_code === 'ITEM_LOGIN_REQUIRED') {\n          await notifyUserReconnect(item_id, 'Please reconnect your bank account');\n        }\n        break;\n\n      case 'PENDING_DISCONNECT':\n        // User needs to reauthorize soon\n        await db.plaidItem.update({\n          where: { itemId: item_id },\n          data: { status: 'PENDING_DISCONNECT' },\n        });\n\n        // Proactive notification\n        await notifyUserReconnect(item_id, 'Your bank connection will expire soon');\n        break;\n\n      case 'USER_PERMISSION_REVOKED':\n        // User revoked access at their bank\n        await db.plaidItem.update({\n          where: { itemId: item_id },\n          data: { status: 'REVOKED' },\n        });\n\n        // Clean up stored data\n        await db.transaction.deleteMany({\n          where: { itemId: item_id },\n        });\n        break;\n    }\n  }\n\n  res.sendStatus(200);\n});\n\n// Check item status before API calls\nasync function getItemWithValidation(itemId: string) {\n  const item = await db.plaidItem.findUnique({\n    where: { itemId },\n  });\n\n  if (!item) {\n    throw new Error('Item not found');\n  }\n\n  if (item.status === 'ERROR') {\n    throw new ItemNeedsUpdateError(item.errorCode, item.errorMessage);\n  }\n\n  return item;\n}\n\n### Context\n\n- error recovery\n- reauthorization\n- credential updates\n\n### Auth for ACH Transfers\n\nUse Auth product to get account and routing numbers for ACH transfers.\nCombine with Identity to verify account ownership before initiating\ntransfers.\n\n// Get account and routing numbers\nasync function getACHNumbers(accessToken: string): Promise<ACHInfo[]> {\n  const response = await plaidClient.authGet({\n    access_token: accessToken,\n  });\n\n  const { accounts, numbers } = response.data;\n\n  // Map ACH numbers to accounts\n  return accounts.map(account => {\n    const achNumber = numbers.ach.find(\n      n => n.account_id === account.account_id\n    );\n\n    return {\n      accountId: account.account_id,\n      name: account.name,\n      mask: account.mask,\n      type: account.type,\n      subtype: account.subtype,\n      routing: achNumber?.routing,\n      account: achNumber?.account,\n      wireRouting: achNumber?.wire_routing,\n    };\n  });\n}\n\n// Verify identity before ACH transfer\nasync function verifyAndInitiateTransfer(\n  accessToken: string,\n  userId: string,\n  amount: number\n): Promise<TransferResult> {\n  // Get identity from linked account\n  const identityResponse = await plaidClient.identityGet({\n    access_token: accessToken,\n  });\n\n  const accountOwners = identityResponse.data.accounts[0]?.owners || [];\n\n  // Get user's stored identity\n  const user = await db.user.findUnique({\n    where: { id: userId },\n  });\n\n  // Match identity\n  const matchResponse = await plaidClient.identityMatch({\n    access_token: accessToken,\n    user: {\n      legal_name: user.legalName,\n      phone_number: user.phoneNumber,\n      email_address: user.email,\n      address: {\n        street: user.street,\n        city: user.city,\n        region: user.state,\n        postal_code: user.postalCode,\n        country: 'US',\n      },\n    },\n  });\n\n  const matchScores = matchResponse.data.accounts[0]?.legal_name;\n\n  // Require high confidence for transfers\n  if ((matchScores?.score || 0) < 70) {\n    throw new Error('Identity verification failed');\n  }\n\n  // Get real-time balance for the transfer\n  const balanceResponse = await plaidClient.accountsBalanceGet({\n    access_token: accessToken,\n  });\n\n  const account = balanceResponse.data.accounts[0];\n\n  // Check sufficient funds (consider pending)\n  const availableBalance = account.balances.available ?? account.balances.current;\n  if (availableBalance < amount) {\n    throw new Error('Insufficient funds');\n  }\n\n  // Get ACH numbers and initiate transfer\n  const authResponse = await plaidClient.authGet({\n    access_token: accessToken,\n  });\n\n  const achNumbers = authResponse.data.numbers.ach.find(\n    n => n.account_id === account.account_id\n  );\n\n  // Initiate ACH transfer with your payment processor\n  return await initiateACHTransfer({\n    routingNumber: achNumbers.routing,\n    accountNumber: achNumbers.account,\n    amount,\n    accountType: account.subtype,\n  });\n}\n\n### Context\n\n- ach transfers\n- money movement\n- account funding\n\n### Real-Time Balance Check\n\nUse /accounts/balance/get for real-time balance (paid endpoint).\n/accounts/get returns cached data suitable for display but not\nreal-time decisions.\n\ninterface BalanceInfo {\n  accountId: string;\n  available: number | null;\n  current: number;\n  limit: number | null;\n  isoCurrencyCode: string;\n  lastUpdated: Date;\n  isRealtime: boolean;\n}\n\n// Get cached balance (free, suitable for display)\nasync function getCachedBalances(accessToken: string): Promise<BalanceInfo[]> {\n  const response = await plaidClient.accountsGet({\n    access_token: accessToken,\n  });\n\n  return response.data.accounts.map(account => ({\n    accountId: account.account_id,\n    available: account.balances.available,\n    current: account.balances.current,\n    limit: account.balances.limit,\n    isoCurrencyCode: account.balances.iso_currency_code || 'USD',\n    lastUpdated: new Date(account.balances.last_updated_datetime || Date.now()),\n    isRealtime: false,\n  }));\n}\n\n// Get real-time balance (paid, for payment validation)\nasync function getRealTimeBalance(\n  accessToken: string,\n  accountIds?: string[]\n): Promise<BalanceInfo[]> {\n  const response = await plaidClient.accountsBalanceGet({\n    access_token: accessToken,\n    options: accountIds ? { account_ids: accountIds } : undefined,\n  });\n\n  return response.data.accounts.map(account => ({\n    accountId: account.account_id,\n    available: account.balances.available,\n    current: account.balances.current,\n    limit: account.balances.limit,\n    isoCurrencyCode: account.balances.iso_currency_code || 'USD',\n    lastUpdated: new Date(),\n    isRealtime: true,\n  }));\n}\n\n// Payment validation with balance check\nasync function validatePayment(\n  accessToken: string,\n  accountId: string,\n  amount: number\n): Promise<PaymentValidation> {\n  const balances = await getRealTimeBalance(accessToken, [accountId]);\n  const account = balances.find(b => b.accountId === accountId);\n\n  if (!account) {\n    return { valid: false, reason: 'Account not found' };\n  }\n\n  const available = account.available ?? account.current;\n\n  if (available < amount) {\n    return {\n      valid: false,\n      reason: 'Insufficient funds',\n      available,\n      requested: amount,\n    };\n  }\n\n  return {\n    valid: true,\n    available,\n    requested: amount,\n  };\n}\n\n### Context\n\n- balance checking\n- fund availability\n- payment validation\n\n### Webhook Verification\n\nVerify Plaid webhooks using the verification key endpoint.\nHandle duplicate webhooks idempotently and design for out-of-order\ndelivery.\n\nimport jwt from 'jsonwebtoken';\nimport jwksClient from 'jwks-rsa';\n\n// Cache JWKS client\nconst client = jwksClient({\n  jwksUri: 'https://production.plaid.com/.well-known/jwks.json',\n  cache: true,\n  cacheMaxAge: 86400000,  // 24 hours\n});\n\nasync function getSigningKey(kid: string): Promise<string> {\n  const key = await client.getSigningKey(kid);\n  return key.getPublicKey();\n}\n\nasync function verifyPlaidWebhook(req: Request): Promise<boolean> {\n  const signedJwt = req.headers['plaid-verification'];\n\n  if (!signedJwt) {\n    return false;\n  }\n\n  try {\n    // Decode to get kid\n    const decoded = jwt.decode(signedJwt, { complete: true });\n    if (!decoded?.header?.kid) {\n      return false;\n    }\n\n    // Get signing key\n    const key = await getSigningKey(decoded.header.kid);\n\n    // Verify JWT\n    const claims = jwt.verify(signedJwt, key, {\n      algorithms: ['ES256'],\n    }) as any;\n\n    // Verify body hash\n    const bodyHash = crypto\n      .createHash('sha256')\n      .update(JSON.stringify(req.body))\n      .digest('hex');\n\n    if (claims.request_body_sha256 !== bodyHash) {\n      return false;\n    }\n\n    // Check timestamp (within 5 minutes)\n    const issuedAt = new Date(claims.iat * 1000);\n    const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000);\n    if (issuedAt < fiveMinutesAgo) {\n      return false;\n    }\n\n    return true;\n  } catch (error) {\n    console.error('Webhook verification failed:', error);\n    return false;\n  }\n}\n\n// Idempotent webhook handler\napp.post('/api/plaid/webhooks', async (req, res) => {\n  // Verify webhook signature\n  if (!await verifyPlaidWebhook(req)) {\n    return res.status(401).send('Invalid signature');\n  }\n\n  const { webhook_type, webhook_code, item_id } = req.body;\n\n  // Create idempotency key\n  const idempotencyKey = `${webhook_type}:${webhook_code}:${item_id}:${JSON.stringify(req.body)}`;\n  const idempotencyHash = crypto.createHash('sha256').update(idempotencyKey).digest('hex');\n\n  // Check if already processed\n  const existing = await db.webhookLog.findUnique({\n    where: { idempotencyHash },\n  });\n\n  if (existing) {\n    console.log('Duplicate webhook, skipping:', idempotencyHash);\n    return res.sendStatus(200);\n  }\n\n  // Record webhook before processing\n  await db.webhookLog.create({\n    data: {\n      idempotencyHash,\n      webhookType: webhook_type,\n      webhookCode: webhook_code,\n      itemId: item_id,\n      payload: req.body,\n      processedAt: new Date(),\n    },\n  });\n\n  // Process webhook (async for quick response)\n  processWebhookAsync(req.body).catch(console.error);\n\n  res.sendStatus(200);\n});\n\n### Context\n\n- webhook security\n- event processing\n- production deployment\n\n## Sharp Edges\n\n### Access Tokens Never Expire But Are Highly Sensitive\n\nSeverity: CRITICAL\n\n### accounts/get Returns Cached Balances, Not Real-Time\n\nSeverity: HIGH\n\n### Webhooks May Arrive Out of Order or Duplicated\n\nSeverity: HIGH\n\n### Items Enter Error States That Require User Action\n\nSeverity: HIGH\n\n### Sandbox Does Not Reflect Production Complexity\n\nSeverity: MEDIUM\n\n### TRANSACTIONS_SYNC_MUTATION_DURING_PAGINATION Requires Restart\n\nSeverity: MEDIUM\n\n### Link Tokens Are Short-Lived and Single-Use\n\nSeverity: MEDIUM\n\n### Recurring Transactions Need 180+ Days of History\n\nSeverity: MEDIUM\n\n## Validation Checks\n\n### Access Token Stored in Plain Text\n\nSeverity: ERROR\n\nPlaid access tokens must be encrypted at rest\n\nMessage: Plaid access token appears to be stored unencrypted. Encrypt at rest.\n\n### Plaid Secret in Client Code\n\nSeverity: ERROR\n\nPlaid secret must never be exposed to clients\n\nMessage: Plaid secret may be exposed. Keep server-side only.\n\n### Hardcoded Plaid Credentials\n\nSeverity: ERROR\n\nCredentials must use environment variables\n\nMessage: Hardcoded Plaid credentials. Use environment variables.\n\n### Missing Webhook Signature Verification\n\nSeverity: ERROR\n\nPlaid webhooks must verify JWT signature\n\nMessage: Webhook handler without signature verification. Verify Plaid-Verification header.\n\n### Using Cached Balance for Payment Decision\n\nSeverity: ERROR\n\nUse real-time balance for payment validation\n\nMessage: Using accountsGet (cached) for payment. Use accountsBalanceGet for real-time balance.\n\n### Missing Item Error State Handling\n\nSeverity: WARNING\n\nAPI calls should handle ITEM_LOGIN_REQUIRED\n\nMessage: API call without ITEM_LOGIN_REQUIRED handling. Handle item error states.\n\n### Polling for Transactions Instead of Webhooks\n\nSeverity: WARNING\n\nUse webhooks for transaction updates\n\nMessage: Polling for transactions. Configure webhooks for SYNC_UPDATES_AVAILABLE.\n\n### Link Token Cached or Reused\n\nSeverity: WARNING\n\nLink tokens are single-use and expire in 4 hours\n\nMessage: Link tokens should not be cached. Create fresh token for each session.\n\n### Using Deprecated Public Key\n\nSeverity: ERROR\n\nPublic key integration ended January 2025\n\nMessage: Public key is deprecated. Use Link tokens instead.\n\n### Transaction Sync Without Cursor Storage\n\nSeverity: WARNING\n\nStore cursor for incremental syncs\n\nMessage: Transaction sync without cursor persistence. Store cursor for incremental sync.\n\n## Collaboration\n\n### Delegation Triggers\n\n- user needs payment processing -> stripe-integration (Stripe for actual payment, Plaid for account linking)\n- user needs budgeting features -> analytics-specialist (Transaction categorization and analysis)\n- user needs investment tracking -> data-engineer (Portfolio analysis and reporting)\n- user needs compliance/audit -> security-specialist (SOC 2, PCI compliance)\n- user needs mobile app -> mobile-developer (React Native Plaid SDK)\n\n## When to Use\n- User mentions or implies: plaid\n- User mentions or implies: bank account linking\n- User mentions or implies: bank connection\n- User mentions or implies: ach\n- User mentions or implies: account aggregation\n- User mentions or implies: bank transactions\n- User mentions or implies: open banking\n- User mentions or implies: fintech\n- User mentions or implies: identity verification banking\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.","tags":["plaid","fintech","antigravity","awesome","skills","sickn33","agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows"],"capabilities":["skill","source-sickn33","skill-plaid-fintech","topic-agent-skills","topic-agentic-skills","topic-ai-agent-skills","topic-ai-agents","topic-ai-coding","topic-ai-workflows","topic-antigravity","topic-antigravity-skills","topic-claude-code","topic-claude-code-skills","topic-codex-cli","topic-codex-skills"],"categories":["antigravity-awesome-skills"],"synonyms":[],"warnings":[],"endpointUrl":"https://skills.sh/sickn33/antigravity-awesome-skills/plaid-fintech","protocol":"skill","transport":"skills-sh","auth":{"type":"none","details":{"cli":"npx skills add sickn33/antigravity-awesome-skills","source_repo":"https://github.com/sickn33/antigravity-awesome-skills","install_from":"skills.sh"}},"qualityScore":"0.700","qualityRationale":"deterministic score 0.70 from registry signals: · indexed on github topic:agent-skills · 34616 github stars · SKILL.md body (22,610 chars)","verified":false,"liveness":"unknown","lastLivenessCheck":null,"agentReviews":{"count":0,"score_avg":null,"cost_usd_avg":null,"success_rate":null,"latency_p50_ms":null,"narrative_summary":null,"summary_updated_at":null},"enrichmentModel":"deterministic:skill-github:v1","enrichmentVersion":1,"enrichedAt":"2026-04-23T00:51:23.793Z","embedding":null,"createdAt":"2026-04-18T21:42:25.873Z","updatedAt":"2026-04-23T00:51:23.793Z","lastSeenAt":"2026-04-23T00:51:23.793Z","tsv":"'/.well-known/jwks.json'',':1541 '/accounts/balance/get':1296 '/accounts/get':1304 '/api/plaid/create-link-token':131,321 '/api/plaid/create-update-token':764 '/api/plaid/exchange-token':207,360 '/api/plaid/webhooks':649,855,1672 '/api/plaid/webhooks'',':167,813 '/transactions/get':410 '/transactions/sync':402 '0':460,463,466,499,580,1142,1190,1201,1227 '1000':1643,1651 '180':169,177,1853 '2':2155 '200':722,983,1737,1771 '2025':2075 '24':1546 '4':2049 '401':672,1685 '404':784 '5':1636,1649 '500':192,284,481,841 '60':1650 '70':1202 '86400000':1545 'access':58,70,204,219,229,236,251,268,356,474,817,822,958,1067,1136,1162,1221,1255,1353,1404,1781,1861,1870,1879 'accesstoken':248,434,476,1059,1069,1120,1138,1164,1223,1257,1345,1355,1394,1406,1443,1454 'account':390,398,728,919,1034,1046,1052,1071,1078,1081,1105,1107,1131,1225,1288,1358,1409,1415,1457,1463,1468,2124,2182,2199 'account.account':1088,1092,1264,1360,1417 'account.available':1473 'account.balances.available':1235,1363,1420 'account.balances.current':1236,1365,1422 'account.balances.iso':1369,1426 'account.balances.last':1376 'account.balances.limit':1367,1424 'account.current':1474 'account.mask':1097 'account.name':1095 'account.subtype':1101,1282 'account.type':1099 'accountid':509,1091,1319,1359,1396,1408,1411,1416,1445,1455,1461 'accountnumb':1278 'accountown':1140 'accounts.map':1080 'accounts/get':1791 'accountsbalanceget':1978 'accountsget':1973 'accounttyp':1281 'ach':31,1027,1039,1075,1115,1246,1267,1284,2194 'achinfo':1062 'achnumb':1083,1103,1106,1109,1259 'achnumbers.account':1279 'achnumbers.routing':1277 'action':1818 'activ':257,729 'actual':2120 'ad':486,495 'added.length':498,546 'added.map':503 'addedcount':459,545,638 'address':1173,1175 'aggreg':2200 'algorithm':1609 'alreadi':1720 'amount':512,562,1124,1239,1280,1447,1477,1486,1492 'analysi':2136,2145 'analyt':2131 'analytics-specialist':2130 'api':8,19,988,1991,1999 'app':156,804,2161 'app.post':130,206,648,763,854,1671 'appear':1881 'application/json':328,367 'arriv':1803 'ask':2258 'async':132,208,314,349,431,650,765,856,990,1056,1117,1342,1391,1440,1548,1561,1673,1762 'auth':29,1025,1030 'authrespons':1252 'authresponse.data.numbers.ach.find':1260 'avail':686,689,1321,1362,1419,1472,1476,1484,1490,1497,2032 'availablebal':1234,1238 'await':141,223,241,249,264,319,335,358,446,472,500,555,581,628,692,705,716,773,793,824,883,911,929,941,962,975,997,1065,1134,1151,1160,1219,1253,1274,1351,1402,1452,1556,1599,1680,1724,1742 'b':1459 'b.accountid':1460 'balanc':32,1213,1293,1301,1337,1386,1438,1451,1494,1794,1957,1967,1983 'balanceinfo':1318,1348,1399 'balancerespons':1218 'balanceresponse.data.accounts':1226 'balances.find':1458 'bank':389,393,918,946,961,2181,2188,2205,2212,2224 'banklinkbutton':302 'baseopt':105 'basepath':100 'batch':701 'best':39 'bodi':329,368,1614,1628 'bodyhash':1617,1630 'boolean':430,1334 'boundari':2266 'break':696,709,720,920,951,981 'budget':2128 'button':383 'cach':1306,1336,1532,1542,1793,1956,1974,2035,2057 'cachemaxag':1544 'call':989,1992,2000 'case':683,697,710,875,921,952 'catch':183,276,598,832,1659,1768 'categor':2134 'categori':523,526,531 'chang':81,612 'channel':537 'check':33,984,1228,1294,1439,1495,1633,1718,1860 'citi':1178 'claim':1605 'claims.iat':1642 'claims.request':1627 'clarif':2260 'clean':971 'clear':2233 'client':109,112,144,152,796,800,1534,1536,1892,1903 'client.getsigningkey':1557 'code':160,605,657,682,806,863,874,895,907,1183,1371,1428,1693,1705,1751,1893 'collabor':2108 'combin':1041 'complet':637,1586 'complex':1826 'complianc':38,2157 'compliance/audit':2150 'compon':293 'confid':1195 'configur':89,97,99,123,2027 'connect':388,397,947,2189 'consid':1231 'console.error':185,278,377,834,1661,1769 'console.log':635,1730 'const':96,119,135,139,211,221,228,306,317,332,342,444,470,485,551,653,768,771,791,859,995,1063,1070,1082,1132,1139,1149,1158,1187,1217,1224,1233,1251,1258,1349,1400,1450,1456,1471,1535,1554,1567,1582,1597,1604,1616,1638,1644,1689,1700,1710,1722 'content':326,365 'content-typ':325,364 'context':391,723,1019,1283,1493,1772 'continu':620 'count':480 'countri':159,805,1185 'countrycod':93 'countrycode.us':161,807 'creat':47,124,197,757,846,1697,2058 'createhash':1619 'createlinktoken':316,340 'creation':44,86,188,837 'credenti':1023,1917,1920,1928 'criteria':2269 'critic':1790 'crypto':1618 'crypto.createhash':1712 'currenc':1370,1427 'current':1324,1364,1421 'cursor':426,441,451,477,478,490,592,594,618,624,634,2088,2093,2101,2104 'data':243,502,561,603,611,632,889,935,968,974,1307,1744,2142 'data-engin':2141 'databas':443 'date':514,516,573,1332,1375,1432,1641,1647,1759 'date.now':1379,1648 'datetim':1378 'day':170,175,1854 'db.plaiditem.create':242 'db.plaiditem.findunique':447,774,998 'db.plaiditem.update':629,884,930,963 'db.transaction.createmany':501 'db.transaction.deletemany':582,976 'db.transaction.updatemany':556 'db.user.findunique':1152 'db.webhooklog.create':1743 'db.webhooklog.findunique':1725 'decis':1316,1960 'decod':1578,1583,1589 'decoded.header.kid':1601 'decrypt':825 'deleg':2109 'deliveri':1521 'deploy':1778 'deprec':2065,2080 'describ':2237 'design':1515 'detail':532 'develop':2164 'digest':1624,1716 'disabl':386 'disconnect':751,923,938 'display':1310,1341 'doesn':238 'duplic':1511,1731,1808 'edg':1780 'effici':408 'email':1172 'en':163,809 'encrypt':250,253,1874,1886 'end':2073 'endpoint':87,1303,1509 'engin':2143 'enter':879,1812 'env':103 'environ':1923,1930,2249 'environment-specif':2248 'error':184,190,194,277,282,286,373,376,380,381,599,604,622,731,740,786,833,839,843,851,866,876,881,891,893,894,897,898,905,906,1005,1011,1020,1205,1242,1660,1665,1813,1868,1895,1919,1937,1962,1986,2008,2069 'error.response':602 'errorcod':892 'errormessag':896 'es256':1610 'event':1775 'exchang':46,54,200,216,280,289,352 'exchangerespons':222 'exchangeresponse.data':233 'exist':1723,1729 'exit':379 'expert':4,15,2254 'expir':74,240,949,1784,2047 'expos':1901,1909 'fail':189,195,281,287,838,844,1208,1664 'fals':1381,1466,1480,1576,1593,1632,1656,1667 'featur':2129 'fetch':320,359,724 'financ':155,525,530,803 'fintech':3,14,37,2217 'fiveminutesago':1645,1654 'flow':24 'found':789,1008,1470 'free':1338 'fresh':2059 'frontend':291 'function':301,315,432,991,1057,1118,1343,1392,1441,1549,1562 'fund':1230,1244,1289,1483,1496 'get':439,1033,1051,1127,1144,1209,1245,1335,1382,1580,1594 'getachnumb':1058 'getcachedbal':1344 'getitemwithvalid':992 'getrealtimebal':1393,1453 'getsigningkey':1550,1600 'handl':35,411,732,736,849,1510,1988,1994,2005,2006 'handler':642,1670,1946 'hardcod':1915,1926 'hash':1615 'hasmor':429,456,468,595 'header':106,324,363,1590,1954 'hex':1625,1717 'high':1194,1787,1800,1810,1820 'histor':711,713 'histori':727,1856 'hour':1547,2050 'id':110,113,146,151,232,247,267,275,507,511,560,589,659,695,708,719,798,865,888,914,934,944,967,980,1087,1089,1093,1154,1263,1265,1361,1410,1418,1695,1707,1754 'idempot':1513,1668,1698 'idempotencyhash':1711,1727,1734,1745 'idempotencykey':1701,1715 'ident':27,1043,1113,1128,1148,1157,1206,2222 'identityrespons':1133 'identityresponse.data.accounts':1141 'impli':2175,2180,2187,2193,2198,2204,2210,2216,2221 'import':88,294,1522,1526 'includ':10,21,777 'increment':404,2095,2106 'initi':261,392,698,700,1049,1249,1266 'initiateachtransf':1275 'initiatetransactionsync':265 'input':2263 'instead':418,819,2013,2084 'insuffici':1243,1482 'integr':9,20,2072,2117 'interfac':424,1317 'intern':149 'invalid':674,1687 'invest':2139 'isocurrencycod':1329,1368,1425 'isrealtim':1333,1380,1433 'issuedat':1639,1653 'item':231,246,266,274,445,452,658,694,707,718,730,737,772,781,787,850,864,871,877,887,908,913,933,943,966,979,985,996,1002,1006,1018,1694,1706,1753,1811,1985,1995,2002,2007 'item.accesstoken':826 'item.errorcode':1015 'item.errormessage':1016 'item.status':1010 'item.userid':799 'itemid':245,273,436,449,508,631,769,776,886,932,965,978,993,1000,1752 'itemneedsupdateerror':1014 'januari':2074 'json':193,285,785,842 'json.stringify':330,369,540,1622,1708 'jsonwebtoken':1525 'jwks':1530,1533 'jwks-rsa':1529 'jwksclient':1527,1537 'jwksuri':1538 'jwt':1523,1603,1942 'jwt.decode':1584 'jwt.verify':1606 'keep':1910 'key':1508,1555,1596,1598,1608,1699,2067,2071,2078 'key.getpublickey':1560 'kid':1551,1558,1581,1591 'languag':162,808 'last':440 'lastupd':1331,1373,1430 'legal':1166,1191 'let':450,455,458,461,464 'limit':1326,1366,1423,2225 'link':11,22,42,49,53,60,84,125,179,186,198,300,333,338,378,394,745,758,828,1130,1838,2033,2040,2052,2082,2125,2183 'linktoken':307,347 'listen':748 'live':65,1843 'locat':538 'login':738,909,1996,2003 'map':1074 'mask':1096 'match':1156,2234 'matchrespons':1159 'matchresponse.data.accounts':1189 'matchscor':1188,1199 'max':482 'may':76,1802,1907 'medium':1828,1837,1849,1858 'mention':2173,2178,2185,2191,2196,2202,2208,2214,2219 'merchantnam':520,566 'messag':899,1877,1904,1925,1944,1971,1998,2023,2051,2076,2097 'metadata':351,374 'method':322,361 'minut':1637 'miss':1932,1984,2271 'mobil':2160,2163 'mobile-develop':2162 'mode':735,747,762,815 'modifi':487,548,554 'modifiedcount':462,574,639 'money':1286 'movement':1287 'must':1872,1898,1921,1940 'mutat':608,1831 'n':1085,1261 'n.account':1086,1262 'name':153,518,522,564,568,801,1094,1167,1192 'nativ':2166 'need':77,925,1852,2112,2127,2138,2149,2159 'never':1783,1899 'new':98,121,128,515,572,687,1004,1013,1204,1241,1374,1431,1640,1646,1758 'next':489,593,626 'notif':940 'notifi':900 'notifyuserreconnect':912,942 'null':311,312,428,454,542,617,619,1323,1328 'number':1037,1055,1072,1076,1125,1170,1247,1322,1325,1327,1448 'numbers.ach.find':1084 'onboard':396 'onclick':384 'one':67 'one-tim':66 'onexit':372 'onsuccess':348 'open':343,385,2211 'option':1407 'order':1520,1806 'out-of-ord':1517 'output':2243 'owner':1143 'ownership':1047 'pagin':610,614,1833 'paid':1302,1387 'password':82 'pattern':5,16,41,666 'payload':1755 'payment':1271,1389,1435,1498,1959,1969,1976,2113,2121 'paymentchannel':535 'pci':2156 'pend':533,569,750,922,937,1232 'per':483 'perman':218 'permiss':954,2264 'persist':2102 'phone':1169 'plaid':2,7,13,18,52,95,108,115,299,1503,1571,1869,1878,1889,1896,1905,1916,1927,1938,1952,2122,2167,2176 'plaid-client-id':107 'plaid-fintech':1 'plaid-secret':114 'plaid-verif':1570,1951 'plaidapi':90,122 'plaidclient':120 'plaidclient.accountsbalanceget':1220,1403 'plaidclient.accountsget':1352 'plaidclient.authget':1066,1254 'plaidclient.identityget':1135 'plaidclient.identitymatch':1161 'plaidclient.itempublictokenexchange':224 'plaidclient.linktokencreate':142,794 'plaidclient.transactionssync':473 'plaidenviron':91,101 'plaidtransactionid':505,558,584 'plain':1865 'pleas':915 'poll':420,2010,2024 'portfolio':2144 'post':323,362 'postal':1182 'practic':40 'primari':527 'proactiv':754,939 'process':494,547,575,1721,1741,1760,1776,2114 'process.env.plaid':102,111,117 'processedat':1757 'processor':1272 'processwebhookasync':1766 'product':92,157,258,821,1031,1777,1825 'production.plaid.com':1540 'production.plaid.com/.well-known/jwks.json'',':1539 'products.transactions':158 'promis':438,1061,1126,1347,1398,1449,1553,1566 'prompt':755 'provid':816 'public':55,201,225,353,2066,2070,2077 'publictoken':212,227,350,370 'put':742 'queuetransactionsync':693,706,717 'quick':1764 'r':587 'r.transaction':588 'react':292,298,2165 'react-plaid-link':297 'readi':344,387,704,715 'real':415,645,1211,1291,1299,1314,1384,1797,1965,1981 'real-tim':414,644,1210,1290,1298,1313,1383,1796,1964,1980 'reason':1467,1481 'reauthor':927,1022 'reconnect':903,916 'record':1738 'recoveri':1021 'recur':172,1850 'reflect':1824 'region':1180 'remov':488,576 'removed.length':579,591 'removed.map':586 'removedcount':465,590,640 'report':2147 'req':133,209,651,669,766,857,1564,1674,1682 'req.body':137,214,660,770,867,1623,1696,1709,1756,1767 'req.headers':1569 'request':168,176,484,1485,1491,1565 'requir':739,910,1193,1816,1834,1997,2004,2262 'res':134,210,652,767,858,1675 'res.json':178,270,827 'res.sendstatus':721,982,1736,1770 'res.status':191,283,671,783,840,1684 'respons':140,318,471,792,1064,1350,1401,1765 'response.data':493,1073 'response.data.accounts.map':1357,1414 'response.data.link':181,830 'response.json':336 'rest':255,1876,1888 'restart':615,1835 'return':382,670,782,1017,1079,1090,1273,1305,1356,1413,1464,1478,1487,1559,1575,1592,1631,1655,1657,1666,1683,1735,1792 'reus':2037 'review':2255 'revok':955,957,970 'rout':1036,1054,1102,1104,1111 'routingnumb':1276 'rsa':1531 'safeti':2265 'sandbox':104,1821 'save':623 'scope':2236 'score':1200 'sdk':2168 'secret':116,118,1890,1897,1906 'secur':235,1774,2152 'security-specialist':2151 'see':663 'send':673,1686 'sensit':1788 'server':1912 'server-sid':1911 'server.ts':83 'servic':423 'session':2063 'setlinktoken':308,337 'sever':1789,1799,1809,1819,1827,1836,1848,1857,1867,1894,1918,1936,1961,1989,2016,2038,2068,2090 'sha256':1620,1629,1713 'sharp':1779 'short':64,1842 'short-liv':63,1841 'side':1913 'sign':1595 'signatur':1678,1688,1934,1943,1948 'signedjwt':1568,1574,1585,1607 'singl':1846,2044 'single-us':1845,2043 'skill':2228 'skill-plaid-fintech' 'skip':1733 'skipdupl':543 'soc':2154 'soon':928,950 'source-sickn33' 'specialist':2132,2153 'specif':2250 'state':882,1814,1987,2009 'status':256,890,936,969,986 'stop':2256 'storag':2089 'store':234,973,1147,1863,1884,2092,2103 'street':1176 'string':305,310,427,435,437,994,1060,1121,1123,1320,1330,1346,1395,1397,1444,1446,1552 'stripe':2116,2118 'stripe-integr':2115 'subcategori':528 'substitut':2246 'subtyp':1100 'success':271,2268 'suffici':1229 'suitabl':1308,1339 'switch':680,872 'sync':26,263,400,422,607,627,636,684,691,1830,2030,2086,2096,2099,2107 'synctransact':433 'task':2232 'test':2252 'text':1866 'throw':621,1003,1012,1203,1240 'time':68,416,646,1212,1292,1300,1315,1385,1798,1966,1982 'timestamp':1634 'token':12,23,43,50,56,59,61,71,85,126,180,182,187,199,202,205,220,226,230,237,252,269,279,290,334,339,346,354,357,475,759,818,823,829,831,836,848,1068,1137,1163,1222,1256,1354,1405,1782,1839,1862,1871,1880,2034,2041,2053,2060,2083 'topic-agent-skills' 'topic-agentic-skills' 'topic-ai-agent-skills' 'topic-ai-agents' 'topic-ai-coding' 'topic-ai-workflows' 'topic-antigravity' 'topic-antigravity-skills' 'topic-claude-code' 'topic-claude-code-skills' 'topic-codex-cli' 'topic-codex-skills' 'track':2140 'transact':25,173,174,259,262,399,405,421,496,549,577,606,679,688,703,714,725,726,1829,1851,2012,2021,2026,2085,2098,2133,2206 'transactionscursor':453,633 'transactionsyncst':425 'transfer':1028,1040,1050,1116,1197,1216,1250,1268,1285 'treat':2241 'tri':138,215,469,790,1577 'trigger':260,690,2110 'true':272,457,544,779,1434,1489,1543,1587,1658 'txn':504,552 'txn.account':510 'txn.amount':513,563 'txn.date':517 'txn.location':539,541 'txn.merchant':521,567 'txn.name':519,565 'txn.payment':536 'txn.pending':534,570 'txn.personal':524,529 'txn.transaction':506,559 'type':327,366,655,678,861,870,1098,1691,1703,1748 'undefin':479,1412 'unencrypt':1885 'updat':78,406,417,647,685,699,712,734,746,761,814,835,847,1024,1377,1621,1714,2022,2031 'updatedat':571 'us':1186 'usd':1372,1429 'use':69,401,1029,1295,1505,1847,1922,1929,1955,1963,1972,1977,2018,2045,2064,2081,2171,2226 'useeffect':313 'useplaidlink':295,345 'user':80,129,143,145,150,395,743,756,778,795,797,901,924,953,956,1145,1150,1165,1817,2111,2126,2137,2148,2158,2172,2177,2184,2190,2195,2201,2207,2213,2218 'user.city':1179 'user.email':1174 'user.legalname':1168 'user.phonenumber':1171 'user.postalcode':1184 'user.state':1181 'user.street':1177 'userid':136,147,213,244,303,304,331,341,371,1122,1155 'usest':309 'valid':1390,1436,1465,1479,1488,1499,1859,1970,2251 'validatepay':1442 'variabl':1924,1931 'verif':28,665,1207,1501,1507,1572,1663,1935,1949,1953,2223 'verifi':661,1045,1112,1502,1602,1613,1676,1941,1950 'verifyandinitiatetransf':1119 'verifyplaidwebhook':668,1563,1681 'warn':1990,2017,2039,2091 'webhook':34,164,412,641,654,656,662,664,675,677,681,752,810,853,860,862,869,873,1500,1504,1512,1662,1669,1677,1690,1692,1702,1704,1732,1739,1747,1750,1761,1773,1801,1933,1939,1945,2015,2019,2028 'webhookcod':1749 'webhooktyp':1746 'wire':1110 'wirerout':1108 'within':1635 'without':1947,2001,2087,2100 'yourapp.com':166,812 'yourapp.com/api/plaid/webhooks'',':165,811","prices":[{"id":"3e7e8d21-1cc2-46ba-9bb8-bad40f43644f","listingId":"d1b4ec87-6177-4b17-9a5b-dc70cefb1b06","amountUsd":"0","unit":"free","nativeCurrency":null,"nativeAmount":null,"chain":null,"payTo":null,"paymentMethod":"skill-free","isPrimary":true,"details":{"org":"sickn33","category":"antigravity-awesome-skills","install_from":"skills.sh"},"createdAt":"2026-04-18T21:42:25.873Z"}],"sources":[{"listingId":"d1b4ec87-6177-4b17-9a5b-dc70cefb1b06","source":"github","sourceId":"sickn33/antigravity-awesome-skills/plaid-fintech","sourceUrl":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/plaid-fintech","isPrimary":false,"firstSeenAt":"2026-04-18T21:42:25.873Z","lastSeenAt":"2026-04-23T00:51:23.793Z"}],"details":{"listingId":"d1b4ec87-6177-4b17-9a5b-dc70cefb1b06","quickStartSnippet":null,"exampleRequest":null,"exampleResponse":null,"schema":null,"openapiUrl":null,"agentsTxtUrl":null,"citations":[],"useCases":[],"bestFor":[],"notFor":[],"kindDetails":{"org":"sickn33","slug":"plaid-fintech","github":{"repo":"sickn33/antigravity-awesome-skills","stars":34616,"topics":["agent-skills","agentic-skills","ai-agent-skills","ai-agents","ai-coding","ai-workflows","antigravity","antigravity-skills","claude-code","claude-code-skills","codex-cli","codex-skills","cursor","cursor-skills","developer-tools","gemini-cli","gemini-skills","kiro","mcp","skill-library"],"license":"mit","html_url":"https://github.com/sickn33/antigravity-awesome-skills","pushed_at":"2026-04-22T06:40:00Z","description":"Installable GitHub library of 1,400+ agentic skills for Claude Code, Cursor, Codex CLI, Gemini CLI, Antigravity, and more. Includes installer CLI, bundles, workflows, and official/community skill collections.","skill_md_sha":"77ff3b9c6788d34c6e3873955afec3760e35d28d","skill_md_path":"skills/plaid-fintech/SKILL.md","default_branch":"main","skill_tree_url":"https://github.com/sickn33/antigravity-awesome-skills/tree/main/skills/plaid-fintech"},"layout":"multi","source":"github","category":"antigravity-awesome-skills","frontmatter":{"name":"plaid-fintech","description":"Expert patterns for Plaid API integration including Link token"},"skills_sh_url":"https://skills.sh/sickn33/antigravity-awesome-skills/plaid-fintech"},"updatedAt":"2026-04-23T00:51:23.793Z"}}