DEV Community

Mohamed Zaghloul
Mohamed Zaghloul Subscriber

Posted on AI-assisted

Every ID on this invoice is valid. Here's why you shouldn't pay it (MCP payee check)

Every number on this invoice is valid. You still shouldn't pay it.

Here's an invoice from a German supplier: German VAT number, German BIC, and an IBAN that passes its mod-97 checksum. A normal IBAN or VAT validator will say all three are valid. But that IBAN belongs to a Dutch bank, so the invoice is asking you to pay a German supplier into a Dutch account. That's what changed-bank-details fraud usually looks like: a real supplier, real-looking IDs, and an account that doesn't fit.

I added a tool to my invoice validator MCP server that checks for exactly this. It's called check_invoice_payee, and it returns one verdict: pay, review or hold. Here's what it gives back for that invoice. This is real output from the free demo endpoint, trimmed for length:

{
  "verdict": "hold",
  "risk_score": 90,
  "summary": "Hold payment: BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account; Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number.",
  "reasons": [
    { "code": "bic_iban_country_mismatch", "severity": "high",
      "message": "BIC is from DE but the IBAN is from NL: the two don't belong to the same bank account" },
    { "code": "iban_country_differs_from_supplier", "severity": "high",
      "message": "Supplier is in DE but wants payment to a NL bank account: the classic changed-bank-details fraud pattern. Confirm by phone using a known number" }
  ],
  "checks": {
    "iban": { "valid": true, "country": "NL", "sepa": true, "formatted": "NL91 ABNA 0417 1643 00", "bank_code": "ABNA" },
    "bic":  { "valid": false, "country": "DE", "bank_code": "COBA", "matches_iban_country": false,
              "errors": ["country_mismatch_with_iban"] },
    "vat":  { "valid": true, "kind": "eu_vat", "country": "DE", "number": "DE136695976" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Look at checks.iban.valid: it's true. A per-field validator stops there. What matters is comparing the fields against each other.

Swap in a German IBAN (DE89370400440532013000) and keep everything else the same, and you get:

{"verdict":"pay","risk_score":0,"summary":"All identifiers are valid and consistent with each other. OK to pay.","reasons":[]}
Enter fullscreen mode Exit fullscreen mode

What it cross-checks

You send whatever the invoice has (iban, bic, vat, ein), plus supplier_country, and optionally supplier_name, currency and live. It then checks:

  • IBAN vs supplier country. A German supplier asking to be paid into a Dutch or Lithuanian account gets flagged.
  • BIC vs IBAN. Both have to point at the same country, or they can't be the same account.
  • VAT ownership. With live: true, the company name EU VIES has on file is compared with supplier_name.
  • The smaller stuff: unregistered VAT numbers, test BICs, and EUR invoices paid into non-SEPA accounts.

Each problem comes back as a reasons[] entry with a machine-readable code and a plain-English message, so an agent can act on it and a person can read it.

What it doesn't do: it checks formats, checksums, how the IDs relate to each other, and (with live) VIES. It can't prove a bank account belongs to a particular company. Nothing short of the bank can. A hold means "call the supplier on a number you already have", not "this is fraud".

Add it to Claude Desktop or Cursor

The server is hosted (remote, Streamable HTTP), so there's nothing to install. For the free demo (10 tool calls per IP per day, no key), add this to your MCP config:

{
  "mcpServers": {
    "invoice-validator": {
      "url": "https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In Cursor that goes in ~/.cursor/mcp.json. In Claude (desktop or web), go to Settings → Connectors → Add custom connector and paste the URL. If your client only supports stdio servers, including Claude Desktop's claude_desktop_config.json, bridge it with mcp-remote:

{
  "mcpServers": {
    "invoice-validator": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/mcp"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Then just ask: "Check this payee before I pay it: supplier in Germany, IBAN NL91ABNA0417164300, BIC COBADEFFXXX, VAT DE136695976." The model calls check_invoice_payee and you get the verdict and the reasons.

For production use, the same server runs behind RapidAPI (the free Basic plan gives 100 requests a month):

{
  "mcpServers": {
    "invoice-validator": {
      "url": "https://invoice-payment-id-validator.p.rapidapi.com/mcp",
      "headers": {
        "X-RapidAPI-Key": "YOUR_RAPIDAPI_KEY",
        "X-RapidAPI-Host": "invoice-payment-id-validator.p.rapidapi.com"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Or just call it over plain HTTP

curl -X POST https://invoice-validator.mohamed-bahaa53000.workers.dev/demo/v1/payee-check \
  -H 'content-type: application/json' \
  -d '{"supplier_country":"DE","supplier_name":"Muster GmbH","iban":"NL91ABNA0417164300","bic":"COBADEFFXXX","vat":"DE136695976","currency":"EUR"}'
Enter fullscreen mode Exit fullscreen mode

The paid route is POST /v1/payee-check on the RapidAPI host. The server also has single-ID tools (validate_iban, validate_bic, validate_eu_vat, validate_gb_vat, validate_ein) for when you only need one check.

Docs and the MCP Registry manifest are at github.com/Mowaah/invoice-validator-mcp. A simple rule for an accounts-payable agent: auto-pay only on pay, send review to a person, and never pay on hold until someone has phoned the supplier.

If you'd rather click than code, there's a free IBAN & VAT checker in the browser, and I'll also set the whole invoice-triage workflow up in your n8n for $149.

Top comments (0)