Checkmarble Integration Flow

How the Banks API talks to Checkmarble (Marble) today, and where it does not send Marble the full picture.

Reviewed: D:\Workspace\API\Banks, branch feature/marble-2026-09-18 (commit 010c0b49), with the ArthaMarble 0.1.1 NuGet package (AmlCompliance.Marble.dll) decompiled to confirm exactly what goes over the wire. File and line references below are to that commit.

At a glance

Today
Money-movement flows that call Marble 2 of 7 — Quick Transfer withdrawals and Noah deposits
Transaction fields sent 16 of 42 that the Marble model supports — and 6 of those carry a wrong or incomplete value
Customer or wallet records sent NoneMarbleCustomer and MarbleWallet exist in the package but are never used
Decisions ignored 1 critical — a Marble Decline on a settled Noah deposit still credits the wallet
Failed sends that are retried None — ingest errors are logged and dropped

1. The moving parts

flowchart LR
  subgraph API["Banks API"]
    QT["Quick Transfer<br/>withdrawal"]
    ND["Noah fiat deposit<br/>webhook"]
    NS["Noah settlement<br/>webhook"]
    ED["External deposit<br/>approval"]
    AD["Admin test endpoint<br/>POST api/marble/decide-all"]
    SVC["MarbleComplianceService<br/>tenant switch · worst outcome · fail-safe"]
  end
  PKG["ArthaMarble 0.1.1<br/>MarbleAmlProvider"]
  M[("Checkmarble<br/>Marble")]
  QT -->|decide + ingest| SVC
  ND -->|decide + ingest| SVC
  NS -->|decide + ingest| SVC
  ED -->|ingest only| SVC
  AD -->|decide| SVC
  SVC --> PKG
  PKG -->|"POST /v1/decisions/all"| M
  PKG -->|"POST /v1/ingest/transactions"| M
  subgraph NC["Never reach Marble"]
    X1["TazaPay · Openpayd · Mesta<br/>FiatRepublic · Artha deposits"]
    X2["FiatRepublic withdrawal<br/>crypto withdrawal · refunds"]
    X3["Customers · wallets<br/>admin approve / reject"]
  end
Part Where What it does
ArthaMarble package NuGet ArthaMarble 0.1.1 HTTP client for Marble. DecideAllAsync posts to /{version}/decisions/all; IngestAsync posts to /{version}/ingest/{objectType}. Authenticates with X-API-KEY. Ships MarbleTransaction, MarbleCustomer and MarbleWallet models. Serializes with nulls omitted — any field the app leaves empty is silently left out of the request
MarbleComplianceService ApplicationService/Aml/MarbleComplianceService.cs The only door to Marble. Checks the tenant has switched Marble on, runs every scenario for the object type, reduces the results to one gate, and never lets a Marble error crash a money flow
Tenant switch Tenant setting ComplianceCheck Marble runs only when set to {"Provider":"Marble","IsEnable":1}. Anything else — including a missing or malformed value — means Marble is not called at all
Configuration Program.cs lines 133–160 Base URL and API key come from the host's CoreConfig (provider marble), falling back to the Marble section of appsettings.json (https://api.checkmarble.com, v1, 30-second timeout). No secrets are stored in appsettings
Admin test endpoint Controllers/WebhookController.cs line 461 POST api/marble/decide-all runs a decision on any posted JSON. Requires login plus an admin permission

2. How a decision becomes an action

EvaluateAsync sends the transaction to every Marble scenario for transactions, then takes the worst outcome. The score kept is the highest single-scenario score.

Marble returns Gate result What the money flow does
Decline or Block and Review Block Hold — do not pay out or credit
Review RequireApproval Hold for admin approval
Approve proceed Continue as normal
No scenario answered, or an unknown outcome RequireApproval Hold for admin approval
Marble error or timeout RequireApproval (fail-safe) Hold for admin approval — an outage never lets money through unreviewed
Marble switched off for the tenant NotEvaluated Continue as if Marble did not exist; nothing is recorded

The outcome and score are written to the transaction row as ComplianceStatus and ComplianceScore.

3. The four flows that touch Marble

3.1 Quick Transfer withdrawal

ApplicationService/WithdrawApplicationService.QuickTransfer.cs — decision at lines 226–236, ingest at 256–260.

sequenceDiagram
  participant C as Customer
  participant API as Banks API
  participant FX as Rate feed
  participant M as Marble
  participant DB as Database
  participant P as Payout provider
  C->>API: Quick Transfer request
  API->>API: Validate, build withdrawal
  API->>FX: Convert amount to CAD
  API->>M: Decide (all "transactions" scenarios)
  M-->>API: Worst outcome + score
  alt Approve
    API->>DB: Save withdrawal
    API->>P: Dispatch payout (unless admin approval or Travel Rule applies)
  else Review, Block or Marble error
    API->>DB: Save as Submitted, AML status Pending — held
  end
  API->>M: Ingest the withdrawal (best effort)

3.2 Noah fiat deposit

ApplicationService/WebhookApplicationService.NoahCallbacks.cs — decision at lines 196–201, ingest at 233–234.

sequenceDiagram
  participant N as Noah
  participant API as Banks API
  participant M as Marble
  participant DB as Database
  N->>API: FiatDeposit webhook (Pending or Settled)
  API->>API: Build deposit row, convert amount to CAD
  API->>M: Decide (all "transactions" scenarios)
  M-->>API: Worst outcome + score
  alt Approve, no admin approval needed
    API->>DB: Approved — wallet credited (fiat)
  else Review or Marble error
    API->>DB: Submitted for admin approval — not credited
  else Decline or Block and Review
    API->>DB: Pending — not credited
  end
  API->>M: Ingest the deposit (best effort)

3.3 Noah settlement — the decision is taken, then ignored

Same file — decision at lines 633–637, promotion at 688–784, ingest at 641–644.

sequenceDiagram
  participant N as Noah
  participant API as Banks API
  participant M as Marble
  participant DB as Database
  N->>API: Transaction event (In, Settled)
  API->>DB: Store the locked FX rate
  API->>API: Stop if the crypto leg is already AML-pending
  API->>M: Decide again (second decision for the same deposit)
  M-->>API: Outcome — could be Decline
  rect rgb(253, 226, 225)
  Note over API,DB: Outcome is only recorded on the row — never checked
  API->>DB: Promote to Approved and credit the crypto wallet — always
  end
  API->>M: Ingest the fiat leg only (best effort)

3.4 External deposit approval

Same file, ProcessExternalDepositAsync (line 977). A pending deposit is matched to Noah by transaction hash, promoted to Approved and credited without any Marble decision, and only then ingested (lines 1062–1066). The crypto leg that is actually credited (referenceTx) is never sent.

4. What Marble actually receives

The two builders — BuildMarbleWithdrawTransaction (QuickTransfer.cs 1217–1238) and BuildMarbleTransaction (NoahCallbacks.cs 293–321) — are near copies and fill the same 16 fields.

Sent today

Marble field Value sent Problem
object_id Transaction id
created_at, updated_at Row timestamps
cust_id Internal customer id Points at a customer Marble has never received (see MG-04)
wallet_id Customer wallet id Points at a wallet Marble has never received
amount, fiat_value Amount converted to CAD Becomes the original amount, still labelled CAD, when the rate is missing (MG-03)
net_amount Same CAD amount Fees ignored — should be amount minus fee
transaction_amount Same CAD amount, as text Code comment says "original currency" — the original is lost
asset Always "CAD" Original currency is lost
direction In / Out
status Always "Completed" Wrong for held, pending, submitted and declined transactions (MG-05)
transaction_type, transaction_subtype e.g. Withdraw / Fiat
payee_name SenderName — for withdrawals the payee's nickname (FavouriteName) Useless for name screening; the legal name is in BeneficiaryName / HolderName
source SenderAccountNumber For withdrawals this is the beneficiary account — belongs in destination

Supported by Marble, not sent — but the data exists in the Banks API

Marble field Where the data already is
destination Withdrawals: SenderAccountNumber / payee IBAN / AccNoorWalletAddress
fee TransactionEntity.Fee / TotalComission
payment_method TransactionEntity.PaymentType, payee PaymentType — a code comment claims it is sent; it is not
provider_reference TransactionReferenceId (Noah id) / TxRef — a code comment claims it is sent; it is not
country CustomerEntity.Country (loaded in both flows)
payee_bank_country PayeeAccountEntity.BankCountry (loaded in Quick Transfer)
payee_aml_status PayeeAccountEntity.Status
wallet_address TransactionEntity.AccNoOrCryptoAddress
crypto_value TransactionEntity.Volume
tx_hash External deposit request TxHash
ip_address The customer's HTTP request in Quick Transfer

Supported by Marble, not available yet

Marble field Why it is empty Requirement
risk_score No customer risk-rating model exists yet GAP-06, FR-451
crypto_aml_score The KYT result goes to a Sumsub service-bus topic asynchronously and never comes back to Marble FR-409
is_vasp_identified Travel Rule data stays in Sumsub FR-605
device_* (5 fields) The Banks API does not capture device data R-050
merchant_* (5 fields), batch_payment_id Not part of the Banks API flows FR-208

5. Gaps

Ordered by severity. Each links to the requirement it breaks in the Requirements tab.

Critical

ID Gap Where Impact Fix
MG-01 Settlement decision is ignored. The Marble outcome is recorded but never checked; the deposit is always promoted and credited NoahCallbacks.cs 634–639, 717–725 A Marble Decline on a settled deposit still puts money in the customer's wallet (FR-504) Check aml.Block / aml.RequireApproval before promoting; hold instead
MG-02 Most money never reaches Marble. Deposits via TazaPay, Openpayd, Mesta, FiatRepublic and Artha, the FiatRepublic withdrawal, the crypto withdrawal (InsertWithdrawaCryptoTransaction) and account-rejection refunds create transactions with no decision and no ingest WebhookApplicationService.* (5 files), AccountsApplicationService*.cs Unmonitored flows (FR-501). Marble's 24-hour totals and structuring rules undercount, so large-transaction reports can be missed (FR-551, R-002, R-003) Route every transaction insert through one shared Marble step
MG-03 Currency conversion fails open. If the CAD rate is missing or the feed errors, the original amount is sent as if it were CAD QuickTransfer.cs 1251–1265; NoahCallbacks.cs 479–501 A GBP 8,000 withdrawal (about CAD 14,700) is scored as CAD 8,000 and slips under the CAD 10,000 rules (FR-554, FR-555) Treat a missing rate as an evaluation failure → hold for approval, and send the rate, source and time

High

ID Gap Where Impact Fix
MG-04 No customer or wallet records. MarbleCustomer / MarbleWallet are never ingested, although cust_id is sent and a comment says it "must match the customer object ingested into Marble" Whole solution Scenarios cannot use customer type, country, age, KYC state or PEP status (FR-505, FR-452) Ingest the customer on KYC / Noah customer events and on change; ingest wallets on creation
MG-05 Status is always "Completed". Held, pending, submitted and declined transactions are all sent as completed QuickTransfer.cs 1231; NoahCallbacks.cs 314 Aggregations count blocked money as completed; analysts see wrong state Send the real State / AdminState
MG-06 Lost sends are never retried. TryIngestAsync logs and drops any error MarbleComplianceService.cs 140–168 One Marble hiccup permanently removes a transaction from Marble's history (FR-513, NFR-06, DQ-03) Queue the ingest in the existing outbox / service bus and retry; reconcile daily against the ledger
MG-07 Original currency and amount are dropped. asset is forced to CAD and transaction_amount repeats the CAD figure Both builders No audit trail of what the customer actually moved, or at what rate (FR-554) Keep CAD in amount for the scenarios; send the original amount and currency, plus the FX rate, source and time
MG-08 Available fields are not mapped. fee, payment_method, provider_reference, destination, country, payee_bank_country, wallet_address, crypto_value, ip_address — see section 4 Both builders Rules on method, geography, payee country, wallet or IP cannot be written (R-031, R-033, R-045) One shared mapper that fills every field the entity already has
MG-09 Payee identity is wrong. payee_name is the payee's nickname and the beneficiary account is sent as source QuickTransfer.cs 1060–1062, 1234–1235 Payee screening and destination rules work on the wrong data (FR-301, FR-316) Send BeneficiaryName / HolderName, and the account as destination
MG-10 Admin decisions never go back to Marble. Approving or rejecting a held withdrawal or deposit does not update Marble WithdrawApplicationService.StateChange.cs and deposit approval paths Marble's case view stays out of date; tuning has no outcome data (FR-859, FR-874) Re-ingest the updated transaction on every admin state change

Medium

ID Gap Where Impact Fix
MG-11 Duplicate decisions. A re-delivered Noah webhook is decided again, and every Noah deposit is decided twice (deposit, then settlement) NoahCallbacks.cs 196, 634 Duplicate decisions and cases for one deposit (FR-854, DQ-02) Skip the decision when the row already has a ComplianceStatus for that stage
MG-12 30-second timeout on a synchronous call inside a customer request and a webhook MarbleOptions.Timeout default, Program.cs 160–161 A slow Marble stalls withdrawals for up to 30 s (NFR-02: p99 under 2 s) Set TimeoutSeconds to about 3; the fail-safe already holds on timeout
MG-13 Only transactions are decided. No onboarding decision and no name screening Whole solution Customers and payees are never screened in Marble (FR-303, FR-316) Add customer and payee decisions once Marble Screening is licensed
MG-14 Tests cover the on/off switch only. The 12 Marble tests check tenant enablement; none check the payload or that a block actually holds money Banks.API.Tests/Tests/MarbleComplianceGateTests.cs MG-01 and MG-05 went unnoticed (T-03d, T-14) Add payload-mapping tests and "Decline must not credit" tests per flow
MG-15 Data residency unconfirmed. The fallback base URL is Checkmarble's hosted service; the real value comes from CoreConfig appsettings.json, Program.cs 143–145 Customer data may leave Canada (NFR-12, A-02) Confirm production CoreConfig points to the self-hosted Canadian instance

What is already done well

6. Target: one shared mapper

MG-03, MG-05, MG-07, MG-08 and MG-09 can all be fixed in one place: a single mapper used by every flow, replacing the two duplicated builders. A sketch using only fields that exist today on TransactionEntity, CustomerEntity and PayeeAccountEntity:

public static MarbleTransaction ToMarble(
    TransactionEntity txn, CustomerEntity? customer, PayeeAccountEntity? payee,
    CadValue cad, string direction, string? ipAddress)
{
    bool outbound = direction == BanksConstants.DirectionOut;
    return new MarbleTransaction
    {
        ObjectId           = txn.Id.ToString(),
        CreatedAt          = ToUtc(txn.CreatedDate),
        UpdatedAt          = ToUtc(txn.ModifiedDate ?? txn.CreatedDate),
        CustId             = txn.CustomerId?.ToString(),
        WalletId           = txn.CustomerWalletId?.ToString(),

        Amount             = cad.Amount,                    // scenarios stay in CAD
        FiatValue          = cad.Amount,
        Fee                = txn.TotalComission ?? txn.Fee,
        NetAmount          = cad.Amount - cad.ToCad(txn.TotalComission ?? txn.Fee ?? 0m),
        Asset              = "CAD",
        TransactionAmount  = $"{txn.Amount} {txn.TxWallet}", // original amount + currency
        CryptoValue        = txn.Volume,

        Direction          = direction,
        Status             = txn.State,                     // real state, not "Completed"
        TransactionType    = txn.TxType,
        TransactionSubtype = txn.TxSubType,
        PaymentMethod      = txn.PaymentType ?? payee?.PaymentType,
        ProviderReference  = txn.TransactionReferenceId ?? txn.TxRef,

        PayeeName          = payee?.BeneficiaryName ?? payee?.HolderName ?? txn.SenderName,
        PayeeBankCountry   = payee?.BankCountry,
        PayeeAmlStatus     = payee?.Status,
        Source             = outbound ? null : txn.SenderAccountNumber,
        Destination        = outbound ? payee?.IBAN ?? txn.SenderAccountNumber : null,
        WalletAddress      = txn.AccNoOrCryptoAddress,
        Country            = customer?.Country,
        IpAddress          = ipAddress,
    };
}

CadValue carries the converted amount together with the rate, its source and its timestamp. When no rate is available it fails the evaluation (hold for approval) rather than passing the original amount through. The rate, source and timestamp — and any other attribute Marble has no field for, such as purpose — need new fields in Marble's data model and in the ArthaMarble package (a 0.1.2 release), because MarbleTransaction is sealed.

7. Suggested order of work

Step Fixes Why first
1 MG-01 — honour the settlement decision; MG-03 — hold on a missing rate Small code changes that stop money moving against a Marble verdict or on a wrong amount
2 MG-05, MG-07, MG-08, MG-09 — one shared mapper (section 6) One change fixes most of the data quality
3 MG-02 — call the mapper from every provider flow Gives Marble the full transaction history its aggregate rules depend on
4 MG-04 — ingest customers and wallets Unlocks customer-level rules and, later, screening (MG-13)
5 MG-06, MG-10, MG-11 — outbox with retries, sync admin decisions, stop duplicates Makes Marble's copy complete and trustworthy
6 MG-12, MG-14, MG-15 — timeout, tests, residency check Hardening and evidence for examiners