{
  "_meta": {
    "description": "Inventory of bucketed open-banking capabilities used to score each candidate provider. Originally derived from Cleo's Plaid integration (meetcleo/meetcleo + meetcleo/webhooks-app, Apr 2026), then extended to a UNION SURFACE in May 2026 \u2014 buckets that Plaid lacks but EU/UK competitors offer (affordability scoring, VRP, income certificates, deep merchant enrichment, pay-by-link, EU PSD2 breadth) were added so that Plaid is no longer the de-facto reference.",
    "source_repos": [
      "meetcleo/meetcleo",
      "meetcleo/webhooks-app"
    ],
    "plaid_sdk_version": "~> 41.0.0",
    "plaid_api_version": "2020-09-14",
    "markets_covered": [
      "US",
      "UK"
    ],
    "last_updated": "2026-05-07",
    "dimensions": {
      "integration": "Buckets that govern API/data-model fit with Cleo's current Plaid integration: auth flow shape, connection lifecycle, webhooks, errors, sandbox.",
      "capability": "Buckets that govern data points + features the provider offers: accounts, balances, transactions, enrichment, identity, payment products, etc."
    },
    "surface_type": "union"
  },
  "buckets": [
    {
      "id": "auth_link",
      "name": "Auth / Link Token / Connect flow",
      "criticality": "must_have",
      "plaid_methods": [
        "link_token_create",
        "link_token_create (update mode)",
        "item_public_token_exchange"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#create_link_token, #create_update_link_token, #exchange_public_token)",
        "app/services/plaid_wrapper/create_web_link_token.rb",
        "app/controllers/api/v1/plaid/link_tokens_controller.rb",
        "app/controllers/api/v1/plaid/session_tokens_controller.rb",
        "app/models/plaid_link_token.rb",
        "app/javascript/modules/connect-bank-us/PlaidConnect.tsx",
        "app/javascript/modules/connect-bank-uk/components/PlaidConnect.tsx"
      ],
      "notes": "Cleo creates a Plaid Link Token per-user with country_codes, products, webhook URL, transactions={days_requested:365}, layer template. Update mode used for reconnect / account-selection update.",
      "dimension": "integration"
    },
    {
      "id": "item_management",
      "name": "Item / connection lifecycle",
      "criticality": "must_have",
      "plaid_methods": [
        "item_get",
        "item_remove",
        "item_webhook_update"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_item, #remove_item, #update_item_webhook)",
        "app/services/plaid_wrapper/remove_login.rb",
        "app/models/plaid_login.rb",
        "app/models/concerns/plaid_login_state_machine.rb"
      ],
      "notes": "Item = Plaid's term for one user-bank connection. We rotate webhook URLs and remove items on disconnect.",
      "dimension": "integration"
    },
    {
      "id": "accounts",
      "name": "Account info (list, ids)",
      "criticality": "must_have",
      "plaid_methods": [
        "accounts_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_accounts)",
        "app/services/plaid_wrapper/account_processor.rb"
      ],
      "notes": "Lists accounts in an Item: account_id, name, type, subtype, mask. Used to seed wallets.",
      "dimension": "capability"
    },
    {
      "id": "balance",
      "name": "Balance fetch",
      "criticality": "must_have",
      "plaid_methods": [
        "accounts_balance_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_accounts_balance)",
        "app/services/plaid_wrapper/fetch_balance_snapshot.rb",
        "app/services/plaid_wrapper/refresh_login_balance.rb",
        "app/workers/record_plaid_balance_worker.rb"
      ],
      "notes": "Includes per-institution edge case (Capital One requires min_last_updated_datetime). Balance snapshot stored separately from transactions.",
      "dimension": "capability"
    },
    {
      "id": "transactions_sync",
      "name": "Transactions sync (incremental, cursor-based)",
      "criticality": "must_have",
      "plaid_methods": [
        "transactions_sync"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#sync_transactions)",
        "app/services/plaid_wrapper/transactions/incremental_processor.rb",
        "app/workers/plaid/sync/fetch_transactions_worker.rb",
        "app/workers/plaid/sync/process_transactions_worker.rb",
        "app/workers/plaid/sync/missing_transactions_worker.rb",
        "app/models/plaid_transaction_sync_cursor.rb",
        "app/instruments/plaid/transaction_changes_*_instrument.rb"
      ],
      "notes": "Modern path: server stores per-login cursor, polls /transactions/sync. Handles added/modified/removed deltas. Migration path from legacy /transactions/get exists. Requires include_personal_finance_category and include_original_description options.",
      "dimension": "capability"
    },
    {
      "id": "transactions_legacy",
      "name": "Transactions point-in-time fetch (legacy)",
      "criticality": "deprecating",
      "plaid_methods": [
        "transactions_get",
        "transactions_refresh"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_transactions, #refresh_transactions)",
        "app/services/plaid_wrapper/transactions/full_processor.rb",
        "app/services/plaid_wrapper/refresh_transactions.rb"
      ],
      "notes": "Pre-/transactions/sync path. Still in use during sync migration. Less important for new provider integrations.",
      "dimension": "capability"
    },
    {
      "id": "categorization_enrichment",
      "name": "Transaction categorization & enrichment",
      "criticality": "must_have",
      "plaid_methods": [
        "transactions_sync (with include_personal_finance_category)"
      ],
      "cleo_files": [
        "app/models/plaidegory.rb",
        "app/models/plaid_categories_mapper.rb",
        "app/domains/transactions/models/plaid_derived_category.rb",
        "app/domains/transactions/models/plaid_extra_attribute.rb"
      ],
      "notes": "Cleo maps Plaid's personal_finance_category taxonomy to internal Cleo categories. Replacement provider needs comparable categorization or we self-categorize.",
      "dimension": "capability"
    },
    {
      "id": "identity",
      "name": "Identity verification & matching (fraud)",
      "criticality": "nice_to_have",
      "plaid_methods": [
        "identity_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_identity)",
        "app/services/plaid_wrapper/get_identity.rb",
        "app/models/plaid_identity.rb",
        "app/services/fraud/enforce_similar_plaid_identity_information.rb",
        "app/services/fraud/plaid_identity_matching.rb",
        "app/services/fraud/rules/plaid_identity_matches_user.rb",
        "app/services/fraud/rules/plaid_identity_matches_account.rb",
        "app/services/fraud/seon/payload_builders/plaid_identity_details_payload.rb"
      ],
      "notes": "Identity returns name/email/phone/address from bank. Used in 7+ fraud rules (duplicate accounts, identity-mismatch, etc.). Critical for KYC/AML \u2014 replacement needs equivalent identity product or we lose fraud detection.",
      "dimension": "capability"
    },
    {
      "id": "auth_routing_numbers",
      "name": "Auth (account & routing numbers)",
      "criticality": "us_only",
      "plaid_methods": [
        "auth_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_auth)"
      ],
      "notes": "Returns ACH account/routing numbers for US bank accounts. Required for ACH disbursements / direct-deposit setup.",
      "dimension": "capability"
    },
    {
      "id": "processor_tokens",
      "name": "Processor tokens (Dwolla / Unit downstream)",
      "criticality": "us_only",
      "plaid_methods": [
        "processor_token_create"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#create_dwolla_processor_token, #create_unit_processor_token)",
        "app/services/dwolla/create_funding_source_from_plaid_account.rb",
        "app/services/synapse_fi/create_funding_source_from_plaid_account.rb",
        "app/models/cleo_bank/unit_plaid_connection.rb"
      ],
      "notes": "Plaid creates a one-time token redeemable by a downstream payment processor (Dwolla, Unit). Replacement provider would need similar processor partnerships OR we'd swap to a new payment-rail integration model.",
      "dimension": "capability"
    },
    {
      "id": "signal_risk",
      "name": "Plaid Signal (real-time risk score)",
      "criticality": "optional",
      "plaid_methods": [
        "signal/evaluate",
        "signal/decision",
        "signal/return"
      ],
      "cleo_files": [
        "app/services/plaid_wrapper/signal/evaluate.rb",
        "app/services/plaid_wrapper/signal/decision.rb",
        "app/services/plaid_wrapper/signal/return.rb",
        "app/services/payments/logging/call_plaid_signal.rb",
        "app/events/events/plaid_signal_score.rb",
        "app/models/report_to_plaid_signal_failed_wallet_transaction_listener.rb"
      ],
      "notes": "Plaid's risk-scoring product for ACH return prediction. Optional add-on; if a candidate provider lacks an equivalent we can keep using Plaid Signal independently or use a third-party risk service.",
      "dimension": "capability"
    },
    {
      "id": "transfer_us",
      "name": "Plaid Transfer (US ACH payment initiation)",
      "criticality": "us_only",
      "plaid_methods": [
        "transfer_authorization_create",
        "transfer_create",
        "transfer_metrics_get",
        "transfer_ledger_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/transfer.rb",
        "app/services/plaid_wrapper/transfer/authorize_transfer.rb",
        "app/services/plaid_wrapper/transfer/create_transfer.rb",
        "app/services/plaid_wrapper/transfer/get_account_capabilities.rb",
        "app/services/plaid_wrapper/disburse_cash_advance.rb",
        "app/models/payment_methods/plaid_rtp.rb"
      ],
      "notes": "Plaid Transfer = ACH payment-initiation product. Used for cash-advance disbursement in the US. RTP = Real-Time Payments rail.",
      "dimension": "capability"
    },
    {
      "id": "wallet_uk",
      "name": "Plaid Wallet (UK BACS / payment initiation)",
      "criticality": "uk_only",
      "plaid_methods": [
        "wallet_transaction_execute",
        "wallet_transaction_get",
        "wallet_get"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/transfer.rb",
        "app/services/plaid_wrapper/wallet/create_transfer.rb",
        "app/services/plaid_wrapper/wallet/get_wallet_transaction.rb",
        "app/services/plaid_wrapper/wallet/process_wallet_transaction_status_update.rb",
        "app/services/plaid_wrapper/wallet/update_wallet_transaction_from_webhook.rb",
        "app/services/plaid_wrapper/wallet/compensate_returned_payout.rb",
        "app/services/plaid_wrapper/wallet/report_manual_reconciliation.rb",
        "app/models/payment_methods/plaid_bacs.rb"
      ],
      "notes": "Plaid Wallet = UK BACS payment-initiation product. Used for advance disbursement & repayment in the UK. New geos (PL, ES) would use SEPA-based equivalents; this bucket is UK-specific.",
      "dimension": "capability"
    },
    {
      "id": "institutions",
      "name": "Institution metadata (logos, colors, list)",
      "criticality": "nice_to_have",
      "plaid_methods": [
        "institutions_get",
        "institutions_get_by_id"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/client.rb (#get_institutions, #get_institution_by_id)",
        "app/workers/plaid/update_provider_logos_worker.rb",
        "app/workers/plaid/update_provider_primary_colors_worker.rb",
        "app/models/bin_plaid_provider_match.rb"
      ],
      "notes": "Used to populate UI: bank logos, brand colors, supported features. Most providers expose similar metadata.",
      "dimension": "capability"
    },
    {
      "id": "webhooks",
      "name": "Webhook intake & processing",
      "criticality": "must_have",
      "plaid_methods": [
        "ITEM_ERROR / PENDING_EXPIRATION / USER_PERMISSION_REVOKED webhooks",
        "TRANSACTIONS_SYNC_UPDATES_AVAILABLE webhook",
        "TRANSFER status update webhooks",
        "WALLET transaction status update webhooks"
      ],
      "cleo_files": [
        "webhooks/app/controllers/webhooks/plaid_controller.rb (in webhooks-app)",
        "webhooks/lib/webhooks/plaid_webhook_verifier.rb (in webhooks-app)",
        "app/workers/webhook_workers/plaid_worker.rb",
        "app/services/plaid_wrapper/wallet/update_wallet_transaction_from_webhook.rb",
        "app/services/plaid_wrapper/wallet/webhook_payload_context.rb"
      ],
      "notes": "Webhook signature verification uses Plaid's JWT/ES256 model. Webhook URL configurable per-item via item_webhook_update.",
      "dimension": "integration"
    },
    {
      "id": "error_mapping",
      "name": "Error model & code mapping",
      "criticality": "must_have",
      "plaid_methods": [
        "Plaid::ApiError catch-all"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/api_error_exception.rb",
        "app/lib/plaid_wrapper/ignorable_api_error_exception.rb",
        "app/services/provider_error_mapping/plaid.rb",
        "app/helpers/plaid_errors.rb",
        "app/models/metric/plaid_error.rb"
      ],
      "notes": "Cleo wraps Plaid's error codes (ITEM_LOGIN_REQUIRED, INVALID_ACCESS_TOKEN, ITEM_NOT_FOUND, NO_ACCOUNTS, RATE_LIMIT_EXCEEDED, etc.) into ApiErrorException. Each new provider needs equivalent error-code taxonomy mapping.",
      "dimension": "integration"
    },
    {
      "id": "sandbox_mocks",
      "name": "Sandbox / dev mocks",
      "criticality": "must_have",
      "plaid_methods": [
        "sandbox_public_token_create",
        "sandbox_item_fire_webhook",
        "sandbox_item_reset_login",
        "sandbox_payment_simulate"
      ],
      "cleo_files": [
        "app/lib/plaid_wrapper/service_mocks_configuration.rb",
        "service_mocks/lib/service_mocks/plaid.rb",
        "service_mocks/app/controllers/service_mocks/plaid/auth_controller.rb",
        "app/services/sandbox/plaid_service_mock_auth_details.rb"
      ],
      "notes": "Local dev environment uses internal mock service. Replacement provider must offer comparable sandbox / staging environment for CI.",
      "dimension": "integration"
    },
    {
      "id": "third_party_costs",
      "name": "Per-call / per-transaction cost tracking",
      "criticality": "nice_to_have",
      "plaid_methods": [
        "billing inferred from transaction volume"
      ],
      "cleo_files": [
        "app/workers/third_party_costs/backfill_monthly_plaid_transactions_charges_worker.rb",
        "app/workers/third_party_costs/create_monthly_plaid_transactions_charge_worker.rb",
        "app/workers/third_party_costs/create_monthly_plaid_transactions_charges_worker.rb"
      ],
      "notes": "Monthly cost roll-up per market/user. Each new provider would need an equivalent billing model worker.",
      "dimension": "capability"
    },
    {
      "id": "affordability_check",
      "name": "Affordability / eligibility scoring",
      "criticality": "nice_to_have",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "Lender-grade affordability or eligibility scoring product (income vs outflows, debt-to-income, structured creditworthiness). Plaid offers Signal for ACH-return risk but not a structured affordability product; competitors like Yapily Validate, Basiq Insights, Tink Risk Insights, Kontomatik Risk Analysis are explicit affordability products.",
      "not_in_plaid_surface": true
    },
    {
      "id": "vrp_recurring_payments",
      "name": "Variable Recurring Payments (VRP, sweeping)",
      "criticality": "nice_to_have",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "Open-banking-native variable-recurring or sweeping payments (UK CMA9 VRP standard, EU emerging). Yapily/Token/Tink/TrueLayer all offer commercial VRP; Plaid's UK Wallet covers some payment use cases but not full VRP at this level.",
      "not_in_plaid_surface": true
    },
    {
      "id": "income_certificate",
      "name": "Signed income confirmation certificate",
      "criticality": "nice_to_have",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "Lender-grade signed income document (PSD2-based income confirmation accepted by domestic underwriters). Kontomatik's signed income certificate is widely recognised in PL. Plaid has identity + transactions but no signed income document equivalent.",
      "not_in_plaid_surface": true
    },
    {
      "id": "deep_merchant_enrichment",
      "name": "Deep merchant enrichment (beyond basic categorisation)",
      "criticality": "nice_to_have",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "Rich merchant identification, recurring-subscription detection, vendor logos, location, brand-level cleanup. Plaid's personal_finance_category covers basic categorisation; Tink Data Enrichment, Yapily Data Plus, Kontomatik labelling, Basiq Insights go materially deeper.",
      "not_in_plaid_surface": true
    },
    {
      "id": "pay_by_link",
      "name": "Pre-authorised pay-by-link / hosted payment page",
      "criticality": "optional",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "Hosted, shareable payment URL that opens the bank-selection + auth flow with the payment pre-filled. Yapily Pay by Link, Token.io, TrueLayer Hosted Payment Page, Mastercard OB Payment Links all expose this. Plaid Transfer is initiated server-side without a hosted-link primitive.",
      "not_in_plaid_surface": true
    },
    {
      "id": "psd2_eu_breadth",
      "name": "Cross-border EU PSD2 bank coverage breadth",
      "criticality": "nice_to_have",
      "dimension": "capability",
      "plaid_methods": [],
      "cleo_files": [],
      "notes": "How many EU PSD2 banks the provider connects to across multiple member states. Salt Edge claims 5000+ banks worldwide; Yapily covers 19+ countries; Tink/Token are pan-EU. Plaid's EU coverage exists but is narrower than EU-native players.",
      "not_in_plaid_surface": true
    }
  ]
}