vat-validator-mcp 1.4.8 → 1.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -158
- package/package.json +1 -1
- package/server.json +2 -2
- package/smithery.yaml +1 -1
- package/src/server.js +75 -32
package/README.md
CHANGED
|
@@ -1,177 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
# VAT Validator MCP
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**AI-powered VAT fraud detection and live VAT number validation
|
|
4
|
+
for AI agents.**
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
Validates EU, UK, and AU VAT numbers against authoritative live
|
|
7
|
+
sources and uses AI pattern analysis to detect invoice fraud
|
|
8
|
+
before payment is authorised. Built for compliance agents,
|
|
9
|
+
invoice processing workflows, and supplier onboarding pipelines.
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
## What This Solves
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
VAT fraud costs EU businesses €50bn annually. The most common
|
|
14
|
+
attack vectors — missing trader fraud, carousel fraud,
|
|
15
|
+
deregistered entity re-use — share one common signal: a VAT
|
|
16
|
+
number that looks valid but isn't registered to the entity on
|
|
17
|
+
the invoice.
|
|
10
18
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"url": "https://vat-validator-mcp-production.up.railway.app"
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
Or via Smithery:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
npx -y @smithery/cli@latest mcp add OjasKord/vat-validator-mcp
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Why Use This
|
|
19
|
+
Claude and other LLMs cannot reliably check live VAT registration
|
|
20
|
+
status from training data. This tool calls the authoritative
|
|
21
|
+
sources directly:
|
|
26
22
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
- **EU VIES** — all 27 member states, real-time
|
|
24
|
+
- **UK HMRC VAT API v2** — OAuth2, authoritative
|
|
25
|
+
- **AU ABR** — Australian Business Register
|
|
30
26
|
|
|
31
27
|
## Tools
|
|
32
28
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
{ "vat_number": "DE811128135" }
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### `validate_uk_vat`
|
|
45
|
-
UK-specific validation against HMRC live records. Returns HMRC consultation number for audit trail. Use when you need to prove compliance during a tax audit.
|
|
46
|
-
|
|
47
|
-
```json
|
|
48
|
-
{ "vat_number": "GB123456789" }
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
### `get_vat_rates`
|
|
52
|
-
Current VAT rates for all 27 EU member states, UK, and Australia. Use before generating any cross-border invoice or quote.
|
|
53
|
-
|
|
54
|
-
```json
|
|
55
|
-
{ "country_code": "DE" }
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### `batch_validate` *(Paid only)*
|
|
59
|
-
Validate up to 10 VAT numbers in one call across any mix of EU, UK, and Australian numbers. Use for supplier onboarding batches and monthly vendor audits.
|
|
60
|
-
|
|
61
|
-
```json
|
|
62
|
-
{ "vat_numbers": ["DE811128135", "GB123456789", "FR12345678901"] }
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### `analyse_vat_risk` *(AI-powered — NOT a database lookup)*
|
|
66
|
-
AI fraud risk assessment after validation. Returns CLEAR/REVIEW/BLOCK recommendation with specific fraud signals. Catches name mismatches between invoice and registry, newly registered companies with large invoice values, dormant status, shell company indicators, and address anomalies. Use before approving any payment or signing any contract with a first-time counterparty.
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"vat_number": "DE811128135",
|
|
71
|
-
"validation_result": { "valid": true, "company_name": null, "country": "DE" },
|
|
72
|
-
"invoice_amount": 50000,
|
|
73
|
-
"invoice_company_name": "Deutsche Test GmbH"
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### `compare_invoice_details` *(AI-powered — NOT a database lookup)*
|
|
78
|
-
AI comparison of invoice details against official registry records. Flags discrepancies between the company name, address, and VAT number on an invoice versus registered government data. A name mismatch is one of the most common invoice fraud signals. Use before approving payment on any invoice from an unverified supplier.
|
|
79
|
-
|
|
80
|
-
```json
|
|
81
|
-
{
|
|
82
|
-
"invoice_company_name": "Deutsche Test GmbH",
|
|
83
|
-
"invoice_vat_number": "DE811128135",
|
|
84
|
-
"invoice_address": "Musterstrasse 1, Berlin",
|
|
85
|
-
"validation_result": { "valid": true, "company_name": null, "country": "DE" }
|
|
86
|
-
}
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
## Example Responses
|
|
29
|
+
| Tool | Free Tier | Use When |
|
|
30
|
+
|---|---|---|
|
|
31
|
+
| validate_vat | 20/month | Before approving any EU supplier or invoice |
|
|
32
|
+
| validate_uk_vat | 20/month | Before approving any UK supplier or invoice |
|
|
33
|
+
| get_vat_rates | 20/month | Before calculating cross-border invoice totals |
|
|
34
|
+
| batch_validate | Paid | Validating a supplier list or invoice batch |
|
|
35
|
+
| analyse_vat_risk | 20/month | Before approving any high-value cross-border invoice |
|
|
36
|
+
| compare_invoice_details | 20/month | Before authorising payment on any supplier invoice |
|
|
90
37
|
|
|
91
|
-
|
|
92
|
-
```json
|
|
93
|
-
{
|
|
94
|
-
"valid": true,
|
|
95
|
-
"vat_number": "DE811128135",
|
|
96
|
-
"country": "DE",
|
|
97
|
-
"company_name": null,
|
|
98
|
-
"source": "VIES",
|
|
99
|
-
"source_url": "ec.europa.eu/taxation_customs/vies",
|
|
100
|
-
"checked_at": "2026-04-09T06:17:00Z"
|
|
101
|
-
}
|
|
102
|
-
```
|
|
38
|
+
## Add to Your Agent
|
|
103
39
|
|
|
104
|
-
**
|
|
105
|
-
```json
|
|
40
|
+
**Claude Code** — add to .mcp.json:
|
|
106
41
|
{
|
|
107
|
-
"
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
"summary": "Valid VAT number but missing registry information prevents full verification."
|
|
42
|
+
"mcpServers": {
|
|
43
|
+
"vat-validator": {
|
|
44
|
+
"type": "sse",
|
|
45
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
114
48
|
}
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
## Recommended Workflows
|
|
118
|
-
|
|
119
|
-
**Invoice processing (3 calls):**
|
|
120
|
-
1. `validate_vat` — confirm VAT number is real and active
|
|
121
|
-
2. `compare_invoice_details` — AI checks invoice name/address against registry
|
|
122
|
-
3. `analyse_vat_risk` — AI fraud risk assessment with CLEAR/REVIEW/BLOCK
|
|
123
|
-
Only proceed with payment if recommendation is CLEAR.
|
|
124
|
-
|
|
125
|
-
**Supplier onboarding (2 calls):**
|
|
126
|
-
1. `validate_vat` — confirm registration
|
|
127
|
-
2. `analyse_vat_risk` — AI fraud signal check
|
|
128
|
-
|
|
129
|
-
**Monthly vendor audit (1 call):**
|
|
130
|
-
- `batch_validate` — re-validate all active suppliers. Registrations can lapse.
|
|
131
|
-
|
|
132
|
-
## Data Sources
|
|
133
|
-
|
|
134
|
-
| Tool | Data Source | Update Frequency |
|
|
135
|
-
|---|---|---|
|
|
136
|
-
| validate_vat (EU) | EU VIES (ec.europa.eu/taxation_customs/vies) | Real-time |
|
|
137
|
-
| validate_vat (UK) | UK HMRC (api.service.hmrc.gov.uk) | Real-time |
|
|
138
|
-
| validate_vat (AU) | Australian ABR (abr.business.gov.au) | Real-time |
|
|
139
|
-
| analyse_vat_risk | Registry data + Claude AI analysis | Real-time |
|
|
140
|
-
| compare_invoice_details | Registry data + Claude AI analysis | Real-time |
|
|
141
|
-
|
|
142
|
-
Every response includes `source_url` and `checked_at` so agents can verify exactly where data came from and when.
|
|
143
|
-
|
|
144
|
-
## Supported Jurisdictions
|
|
145
49
|
|
|
146
|
-
**
|
|
50
|
+
**LangChain:**
|
|
51
|
+
from langchain_mcp import MCPClient
|
|
52
|
+
client = MCPClient(
|
|
53
|
+
"https://vat-validator-mcp-production.up.railway.app/sse"
|
|
54
|
+
)
|
|
147
55
|
|
|
148
|
-
**
|
|
149
|
-
|
|
150
|
-
|
|
56
|
+
**OpenAI Agents SDK:**
|
|
57
|
+
from agents.mcp import MCPServerSse
|
|
58
|
+
mcp_server = MCPServerSse(
|
|
59
|
+
params={
|
|
60
|
+
"url": "https://vat-validator-mcp-production.up.railway.app/sse"
|
|
61
|
+
}
|
|
62
|
+
)
|
|
151
63
|
|
|
152
64
|
## Pricing
|
|
153
65
|
|
|
154
|
-
|
|
|
66
|
+
| Tier | Calls | Price |
|
|
155
67
|
|---|---|---|
|
|
156
|
-
| Free | 20/month | No
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
Upgrade
|
|
161
|
-
|
|
162
|
-
##
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
## Connect
|
|
174
|
-
|
|
175
|
-
- Website: [kordagencies.com](https://kordagencies.com)
|
|
176
|
-
- Smithery: [smithery.ai/server/OjasKord/vat-validator-mcp](https://smithery.ai/server/OjasKord/vat-validator-mcp)
|
|
177
|
-
- Contact: ojas@kordagencies.com
|
|
68
|
+
| Free | 20/month | No card required |
|
|
69
|
+
| Pro | Unlimited | $39/month |
|
|
70
|
+
| Enterprise | Unlimited + priority | $199/month |
|
|
71
|
+
|
|
72
|
+
Upgrade: https://kordagencies.com
|
|
73
|
+
|
|
74
|
+
## Search Terms
|
|
75
|
+
|
|
76
|
+
VAT number validation · EU VAT check · VIES lookup · HMRC VAT
|
|
77
|
+
verification · VAT fraud detection · invoice VAT compliance ·
|
|
78
|
+
missing trader fraud · carousel fraud detection · cross-border
|
|
79
|
+
invoice check · supplier VAT screening · VAT registration
|
|
80
|
+
verification · EU invoice compliance · AU ABR lookup ·
|
|
81
|
+
UK VAT API · invoice fraud detection API · supplier VAT fraud
|
|
82
|
+
risk · accounts payable compliance agent · VAT invoice audit ·
|
|
83
|
+
AI invoice compliance · MCP VAT validation
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vat-validator-mcp",
|
|
3
3
|
"mcpName": "io.github.OjasKord/vat-validator-mcp",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.12",
|
|
5
5
|
"description": "VAT number validation for AI agents. EU VIES, UK HMRC, Australian ABN in one call.",
|
|
6
6
|
"main": "src/server.js",
|
|
7
7
|
"scripts": {
|
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.OjasKord/vat-validator-mcp",
|
|
4
4
|
"title": "VAT Validator MCP",
|
|
5
5
|
"description": "Validate EU, UK, AU VAT numbers for AI agents. EU ViDA e-invoicing compliance.",
|
|
6
|
-
"version": "1.4.
|
|
6
|
+
"version": "1.4.12",
|
|
7
7
|
"websiteUrl": "https://kordagencies.com",
|
|
8
8
|
"repository": {
|
|
9
9
|
"url": "https://github.com/OjasKord/vat-validator-mcp",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"identifier": "vat-validator-mcp",
|
|
16
|
-
"version": "1.4.
|
|
16
|
+
"version": "1.4.12",
|
|
17
17
|
"transport": { "type": "stdio" },
|
|
18
18
|
"environmentVariables": [
|
|
19
19
|
{ "name": "ANTHROPIC_API_KEY", "description": "Anthropic API key for AI-powered fraud risk analysis", "isRequired": true, "isSecret": true },
|
package/smithery.yaml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
description: "VAT
|
|
1
|
+
description: "AI-powered VAT fraud detection and live VAT validation via EU VIES (27 member states), UK HMRC, and AU ABR. Call before invoice approval, supplier onboarding, or cross-border payment. Detects missing trader fraud, carousel fraud, deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK verdict."
|
|
2
2
|
startCommand:
|
|
3
3
|
type: http
|
|
4
4
|
url: https://vat-validator-mcp-production.up.railway.app
|
package/src/server.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
|
|
6
6
|
const PERSIST_FILE = '/tmp/vat_stats.json';
|
|
7
7
|
const API_KEYS_FILE = '/tmp/vat_apikeys.json';
|
|
8
|
-
const VERSION = '1.4.
|
|
8
|
+
const VERSION = '1.4.12';
|
|
9
9
|
const PRO_UPGRADE_URL = 'https://buy.stripe.com/28EeVceUB06N1ty3teebu0l';
|
|
10
10
|
const ENTERPRISE_UPGRADE_URL = 'https://buy.stripe.com/00w14m7s96vb1ty5Bmebu0m';
|
|
11
11
|
const RESEND_API_KEY = process.env.RESEND_API_KEY || '';
|
|
@@ -15,8 +15,11 @@ const STATS_KEY = process.env.STATS_KEY || 'ojas2026';
|
|
|
15
15
|
|
|
16
16
|
const freeTierUsage = new Map();
|
|
17
17
|
const usageLog = [];
|
|
18
|
+
const toolUsageCounts = {};
|
|
19
|
+
const trialExtensions = new Map();
|
|
18
20
|
const FREE_TIER_LIMIT = 20;
|
|
19
|
-
const FREE_TIER_WARNING = 16;
|
|
21
|
+
const FREE_TIER_WARNING = 16;
|
|
22
|
+
const TRIAL_EXTENSION_CALLS = 10;
|
|
20
23
|
const apiKeys = new Map();
|
|
21
24
|
const PLAN_LIMITS = { pro: 5000, enterprise: Infinity };
|
|
22
25
|
|
|
@@ -24,7 +27,9 @@ function saveStats() {
|
|
|
24
27
|
try {
|
|
25
28
|
fs.writeFileSync(PERSIST_FILE, JSON.stringify({
|
|
26
29
|
freeTierUsage: Array.from(freeTierUsage.entries()),
|
|
27
|
-
usageLog: usageLog.slice(-1000)
|
|
30
|
+
usageLog: usageLog.slice(-1000),
|
|
31
|
+
toolUsageCounts,
|
|
32
|
+
trialExtensions: Array.from(trialExtensions.entries())
|
|
28
33
|
}));
|
|
29
34
|
} catch(e) { console.error('Stats save error:', e.message); }
|
|
30
35
|
}
|
|
@@ -35,13 +40,22 @@ function loadStats() {
|
|
|
35
40
|
const data = JSON.parse(fs.readFileSync(PERSIST_FILE, 'utf8'));
|
|
36
41
|
if (data.freeTierUsage) data.freeTierUsage.forEach(([k, v]) => freeTierUsage.set(k, v));
|
|
37
42
|
if (data.usageLog) usageLog.push(...data.usageLog);
|
|
38
|
-
|
|
43
|
+
if (data.toolUsageCounts) Object.assign(toolUsageCounts, data.toolUsageCounts);
|
|
44
|
+
if (data.trialExtensions) data.trialExtensions.forEach(([k, v]) => trialExtensions.set(k, v));
|
|
45
|
+
console.log('Stats loaded: ' + freeTierUsage.size + ' IPs, ' + usageLog.length + ' calls, ' + trialExtensions.size + ' trial extensions');
|
|
39
46
|
}
|
|
40
47
|
} catch(e) { console.error('Stats load error:', e.message); }
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
function getMonthKey(ip) { return ip + ':' + new Date().toISOString().slice(0, 7); }
|
|
44
51
|
|
|
52
|
+
function getEffectiveLimit(ip) {
|
|
53
|
+
for (const record of trialExtensions.values()) {
|
|
54
|
+
if (record.ip === ip) return FREE_TIER_LIMIT + TRIAL_EXTENSION_CALLS;
|
|
55
|
+
}
|
|
56
|
+
return FREE_TIER_LIMIT;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
function saveApiKeys() {
|
|
46
60
|
try { fs.writeFileSync(API_KEYS_FILE, JSON.stringify(Array.from(apiKeys.entries()))); } catch(e) { console.error('API keys save error:', e.message); }
|
|
47
61
|
}
|
|
@@ -249,20 +263,21 @@ async function executeTool(name, args) {
|
|
|
249
263
|
const result = await validateHMRC(detected.number);
|
|
250
264
|
if (result.error) return { valid: null, vat_number, country: 'GB', source: 'HMRC', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), retry: true, _disclaimer: LEGAL_DISCLAIMER };
|
|
251
265
|
const d = result.data;
|
|
252
|
-
if (result.status === 200 && d.target) return { valid: true, vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
253
|
-
return { valid: false, vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
266
|
+
if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', vat_number, country: 'GB', company_name: d.target.name || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', consultation_number: d.consultationNumber || null, checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
267
|
+
return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, country: 'GB', source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
254
268
|
}
|
|
255
269
|
if (detected.type === 'eu') {
|
|
256
270
|
const result = await validateVIES(detected.country, detected.number);
|
|
257
271
|
if (result.error) return { valid: null, vat_number, agent_action: 'RETRY_IN_30_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 1800000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), country: detected.country, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', error: 'EU VIES portal is temporarily unavailable — this is a known issue with the official EU system, not a problem with the VAT number. Retry in 30 minutes.', likely_cause: 'external VAT registry temporarily unavailable', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
258
272
|
const d = result.data;
|
|
259
|
-
return { valid: d.isValid || false, vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
273
|
+
return { valid: d.isValid || false, agent_action: d.isValid ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: detected.country, company_name: d.traderName || null, address: d.traderAddress || null, source: 'VIES', source_url: 'ec.europa.eu/taxation_customs/vies', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
260
274
|
}
|
|
261
275
|
if (detected.type === 'au') {
|
|
262
276
|
const result = await validateABN(detected.number);
|
|
263
277
|
if (result.error) return { valid: null, vat_number, country: 'AU', source: 'ABR', error: result.error, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
264
278
|
const d = result.data;
|
|
265
|
-
|
|
279
|
+
const isValidABN = !!(d.Abn && d.AbnStatus === 'Active');
|
|
280
|
+
return { valid: isValidABN, agent_action: isValidABN ? 'PROCEED' : 'VERIFY_MANUALLY', vat_number, country: 'AU', company_name: d.EntityName || null, abn_status: d.AbnStatus || null, source: 'ABR', source_url: 'abr.business.gov.au', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
266
281
|
}
|
|
267
282
|
return { valid: null, vat_number, agent_action: 'PROVIDE_COUNTRY_PREFIX', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), error: 'Could not detect country. Supported prefixes: EU (AT BE BG CY CZ DE DK EE EL ES FI FR HR HU IE IT LT LU LV MT NL PL PT RO SE SI SK), UK (GB), Australia (AU).', likely_cause: 'required field missing or malformed', _disclaimer: LEGAL_DISCLAIMER };
|
|
268
283
|
}
|
|
@@ -274,18 +289,18 @@ async function executeTool(name, args) {
|
|
|
274
289
|
const result = await validateHMRC(vat_number);
|
|
275
290
|
if (result.error) return { valid: null, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', error: 'UK HMRC API is temporarily unavailable — this is not a problem with the VAT number. Retry in a few minutes.', likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
276
291
|
const d = result.data;
|
|
277
|
-
if (result.status === 200 && d.target) return { valid: true, vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
278
|
-
return { valid: false, vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
292
|
+
if (result.status === 200 && d.target) return { valid: true, agent_action: 'PROCEED', vat_number, company_name: d.target.name || null, registered_address: d.target.address ? Object.values(d.target.address).filter(Boolean).join(', ') : null, consultation_number: d.consultationNumber || null, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
293
|
+
return { valid: false, agent_action: 'VERIFY_MANUALLY', vat_number, source: 'HMRC', source_url: 'api.service.hmrc.gov.uk', reason: d.code || 'VAT number not found', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
279
294
|
}
|
|
280
295
|
|
|
281
296
|
if (name === 'get_vat_rates') {
|
|
282
297
|
const country_code = args.country_code;
|
|
283
298
|
const checkedAt = nowISO();
|
|
284
|
-
if (!country_code) return { rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
299
|
+
if (!country_code) return { agent_action: 'PROCEED', rates: VAT_RATES, note: 'VAT rates as of 2026. Verify with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER };
|
|
285
300
|
const code = country_code.toUpperCase();
|
|
286
301
|
const rate = VAT_RATES[code];
|
|
287
302
|
if (!rate) return { error: 'No VAT rate data for: ' + code + '. Supported: ' + Object.keys(VAT_RATES).join(', '), likely_cause: 'required field missing or malformed', agent_action: 'PROVIDE_REQUIRED_FIELD', category: 'invalid_input', retryable: false, retry_after_ms: null, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
288
|
-
return Object.assign({ country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
303
|
+
return Object.assign({ agent_action: 'PROCEED', country_code: code }, rate, { note: 'Verify current rates with official tax authority before use.', source_url: 'kordagencies.com', checked_at: checkedAt, _disclaimer: LEGAL_DISCLAIMER });
|
|
289
304
|
}
|
|
290
305
|
|
|
291
306
|
if (name === 'batch_validate') {
|
|
@@ -296,7 +311,7 @@ async function executeTool(name, args) {
|
|
|
296
311
|
try { return await executeTool('validate_vat', { vat_number: vat }); }
|
|
297
312
|
catch(e) { return { vat_number: vat, valid: null, error: e.message, likely_cause: 'external VAT registry temporarily unavailable', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10) }; }
|
|
298
313
|
}));
|
|
299
|
-
return { summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
|
|
314
|
+
return { agent_action: 'PROCEED', summary: { total: results.length, valid: results.filter(r => r.valid === true).length, invalid: results.filter(r => r.valid === false).length, error: results.filter(r => r.valid === null).length }, results, _disclaimer: LEGAL_DISCLAIMER };
|
|
300
315
|
}
|
|
301
316
|
|
|
302
317
|
if (name === 'analyse_vat_risk') {
|
|
@@ -309,7 +324,8 @@ async function executeTool(name, args) {
|
|
|
309
324
|
try {
|
|
310
325
|
const response = await callClaude(prompt);
|
|
311
326
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
312
|
-
|
|
327
|
+
const vatRiskAction = (result.risk_level === 'HIGH' || result.risk_level === 'CRITICAL') ? 'HOLD' : result.risk_level === 'MEDIUM' ? 'VERIFY_MANUALLY' : 'PROCEED';
|
|
328
|
+
return Object.assign({}, result, { vat_number, agent_action: vatRiskAction, _disclaimer: LEGAL_DISCLAIMER });
|
|
313
329
|
} catch(e) {
|
|
314
330
|
return { recommendation: 'REVIEW', risk_level: 'MEDIUM', risk_score: 50, vat_number, error: 'AI analysis unavailable - manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), _disclaimer: LEGAL_DISCLAIMER };
|
|
315
331
|
}
|
|
@@ -322,7 +338,7 @@ async function executeTool(name, args) {
|
|
|
322
338
|
try {
|
|
323
339
|
const response = await callClaude(prompt);
|
|
324
340
|
const result = JSON.parse(response.replace(/```json|```/g, '').trim());
|
|
325
|
-
const agentAction = result.
|
|
341
|
+
const agentAction = result.match_verdict === 'MATCH' ? 'PROCEED' : 'INVESTIGATE';
|
|
326
342
|
return Object.assign({}, result, { invoice_vat_number, agent_action: agentAction, discrepancies: result.discrepancies || [], _disclaimer: LEGAL_DISCLAIMER });
|
|
327
343
|
} catch(e) {
|
|
328
344
|
return { match_verdict: 'UNVERIFIABLE', agent_action: 'RETRY_IN_2_MIN', category: 'upstream_unavailable', retryable: true, retry_after_ms: 120000, fallback_tool: null, trace_id: Math.random().toString(36).slice(2, 10), fraud_risk: 'MEDIUM', invoice_vat_number, discrepancies: [], error: 'AI analysis unavailable -- manual review recommended', likely_cause: 'AI analysis failed — transient Anthropic API issue', _disclaimer: LEGAL_DISCLAIMER };
|
|
@@ -344,11 +360,12 @@ function checkAccess(req) {
|
|
|
344
360
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
345
361
|
const monthKey = getMonthKey(ip);
|
|
346
362
|
const calls = freeTierUsage.get(monthKey) || 0;
|
|
347
|
-
if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit reached.
|
|
363
|
+
if (calls >= FREE_TIER_LIMIT) return { allowed: false, reason: 'Free tier limit of ' + FREE_TIER_LIMIT + ' calls/month reached. Option 1: POST /trial-extension with {"name":"...","email":"...","use_case":"..."} for 10 extra free calls. Option 2: Upgrade to Pro at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).', upgrade_url: PRO_UPGRADE_URL, trial_extension: { endpoint: '/trial-extension', method: 'POST', body: { name: 'string', email: 'string', use_case: 'string' } }, tier: 'free_limit_reached' };
|
|
348
364
|
freeTierUsage.set(monthKey, calls + 1);
|
|
349
365
|
saveStats();
|
|
350
366
|
const remaining = FREE_TIER_LIMIT - calls - 1;
|
|
351
|
-
|
|
367
|
+
const warningMsg = remaining < 5 ? remaining + ' free validations remaining this month. Need more? POST /trial-extension with your email for 10 extra free calls, or upgrade at ' + PRO_UPGRADE_URL + ' (500 calls, never expire).' : null;
|
|
368
|
+
return { allowed: true, tier: 'free', remaining, warning: warningMsg };
|
|
352
369
|
}
|
|
353
370
|
|
|
354
371
|
function verifyStripeSignature(body, sig, secret) {
|
|
@@ -399,12 +416,12 @@ async function handleStripeWebhook(body, sig) {
|
|
|
399
416
|
}
|
|
400
417
|
|
|
401
418
|
const tools = [
|
|
402
|
-
{ name: 'validate_vat', idempotent: true, description: '
|
|
403
|
-
{ name: 'validate_uk_vat', idempotent: true, description: '
|
|
404
|
-
{ name: 'get_vat_rates', idempotent: true, description: '
|
|
405
|
-
{ name: 'batch_validate', idempotent: true, description: '
|
|
406
|
-
{ name: 'analyse_vat_risk', idempotent: true, description: '
|
|
407
|
-
{ name: 'compare_invoice_details', idempotent: true, description: '
|
|
419
|
+
{ name: 'validate_vat', idempotent: true, description: 'Call this BEFORE your agent approves any EU supplier, issues a cross-border invoice, or processes an inbound EU payment. Validates VAT registration numbers against EU VIES (all 27 member states) and AU ABR in real time. Returns valid/invalid status, registered company name, and address — data your agent cannot retrieve reliably from training data. A deregistered or invalid VAT number on an approved invoice creates an unrecoverable tax liability. One call eliminates that risk. Returns structured JSON with machine-readable fields — no further analysis needed.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'VAT number with country prefix (e.g. DE123456789, GB123456789, FR12345678901)' } }, required: ['vat_number'] } },
|
|
420
|
+
{ name: 'validate_uk_vat', idempotent: true, description: 'Call this BEFORE your agent approves any UK supplier, issues a UK VAT invoice, or processes a UK inbound payment. Validates UK VAT numbers directly against HMRC VAT API v2 via OAuth2 — the only authoritative UK source. Returns valid/invalid, registered business name, and address. An invalid UK VAT number on an approved invoice triggers HMRC penalties with no appeal path. One call, authoritative source, machine-readable verdict.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'UK VAT number with or without GB prefix' } }, required: ['vat_number'] } },
|
|
421
|
+
{ name: 'get_vat_rates', idempotent: true, description: 'Call this BEFORE your agent calculates invoice totals, applies tax rates, or generates VAT-inclusive pricing for any EU country or UK. Returns current standard, reduced, and zero VAT rates for all 27 EU member states and UK. VAT rates change without notice — your agent cannot rely on training data for current rates. Returns structured JSON — no parsing needed.', inputSchema: { type: 'object', properties: { country_code: { type: 'string', description: 'ISO 2-letter country code (e.g. DE, FR, GB). Leave blank for all countries.' } }, required: [] } },
|
|
422
|
+
{ name: 'batch_validate', idempotent: true, description: 'Call this when your agent needs to validate multiple supplier VAT numbers in a single workflow — onboarding a supplier list, auditing an invoice batch, or screening a counterparty database. Validates against EU VIES and HMRC in one call. One undetected invalid VAT number in a supplier batch creates downstream tax exposure across every transaction with that supplier. Returns per-number verdicts in structured JSON.', inputSchema: { type: 'object', properties: { vat_numbers: { type: 'array', items: { type: 'string' }, description: 'Array of VAT numbers with country prefixes (max 10)' } }, required: ['vat_numbers'] } },
|
|
423
|
+
{ name: 'analyse_vat_risk', idempotent: true, description: 'Call this BEFORE your agent approves a high-value invoice, onboards a new EU or UK supplier, or processes any cross-border payment where VAT fraud is a material risk. AI-powered fraud risk scoring — NOT a simple database lookup. Analyses VAT registration patterns, company age, jurisdiction risk, and invoice behaviour to detect missing trader fraud, carousel fraud, and deregistered entity re-use. Returns CLEAR/REVIEW/BLOCK recommendation, risk score 0-100, fraud signals list, and agent_action field (PROCEED/VERIFY_MANUALLY/HOLD) — no further analysis needed. The only MCP that combines live VIES validation with AI fraud pattern detection.', inputSchema: { type: 'object', properties: { vat_number: { type: 'string', description: 'The VAT number that was validated' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat' }, invoice_amount: { type: 'number', description: 'Optional - invoice or transaction amount in local currency.' }, invoice_company_name: { type: 'string', description: 'Optional - company name as it appears on the invoice.' } }, required: ['vat_number', 'validation_result'] } },
|
|
424
|
+
{ name: 'compare_invoice_details', idempotent: true, description: 'Call this BEFORE your agent finalises payment on any invoice where the supplier VAT number, company name, or address requires verification. Cross-checks invoice details against live VIES and HMRC registry data. A single name mismatch between invoice and registry is the most common signal of invoice fraud — one call catches it before payment is authorised. Returns MATCH/MISMATCH verdict with field-level detail and agent_action. Machine-ready output, no parsing needed.', inputSchema: { type: 'object', properties: { invoice_company_name: { type: 'string', description: 'Company name as it appears on the invoice' }, invoice_address: { type: 'string', description: 'Address as it appears on the invoice (optional)' }, invoice_vat_number: { type: 'string', description: 'VAT number as it appears on the invoice' }, validation_result: { type: 'object', description: 'The full result object returned by validate_vat or validate_uk_vat for this VAT number' } }, required: ['invoice_company_name', 'invoice_vat_number', 'validation_result'] } }
|
|
408
425
|
];
|
|
409
426
|
|
|
410
427
|
const sseClients = new Map();
|
|
@@ -450,11 +467,34 @@ const server = http.createServer(async (req, res) => {
|
|
|
450
467
|
if (req.url === '/stats' && req.method === 'GET') {
|
|
451
468
|
if (req.headers['x-stats-key'] !== STATS_KEY) { res.writeHead(401, cors); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
452
469
|
const totalFreeCalls = Array.from(freeTierUsage.values()).reduce((a, b) => a + b, 0);
|
|
453
|
-
const toolCounts = {};
|
|
454
|
-
usageLog.forEach(e => { toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1; });
|
|
455
|
-
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
456
470
|
const freeUniqueIPs = new Set(Array.from(freeTierUsage.keys()).map(k => k.split(':')[0])).size;
|
|
457
|
-
res.
|
|
471
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
472
|
+
res.end(JSON.stringify({ free_tier_unique_ips: freeUniqueIPs, free_tier_total_calls: totalFreeCalls, paid_keys_issued: apiKeys.size, tool_usage: toolUsageCounts, recent_calls: usageLog.slice(-20).reverse(), trial_extensions_granted: trialExtensions.size }));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (req.url === '/trial-extension' && req.method === 'POST') {
|
|
477
|
+
let body = ''; req.on('data', c => body += c);
|
|
478
|
+
req.on('end', async () => {
|
|
479
|
+
try {
|
|
480
|
+
const { name, email, use_case } = JSON.parse(body);
|
|
481
|
+
if (!name || !email) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'name and email are required', agent_action: 'PROVIDE_REQUIRED_FIELDS' })); return; }
|
|
482
|
+
const emailKey = 'trial:' + email.toLowerCase().trim();
|
|
483
|
+
if (trialExtensions.has(emailKey)) { res.writeHead(409, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Trial extension already granted for this email.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'INFORM_USER_TRIAL_ALREADY_USED' })); return; }
|
|
484
|
+
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
485
|
+
const monthKey = getMonthKey(ip);
|
|
486
|
+
const currentCalls = freeTierUsage.get(monthKey) || 0;
|
|
487
|
+
freeTierUsage.set(monthKey, Math.max(0, currentCalls - TRIAL_EXTENSION_CALLS));
|
|
488
|
+
trialExtensions.set(emailKey, { name, email, use_case: use_case || '', ip, granted_at: nowISO() });
|
|
489
|
+
saveStats();
|
|
490
|
+
await sendEmail('ojas@kordagencies.com', 'VAT Validator -- Trial Extension: ' + name,
|
|
491
|
+
'<p><b>Name:</b> ' + name + '<br><b>Email:</b> ' + email + '<br><b>Use case:</b> ' + (use_case || 'Not provided') + '<br><b>IP:</b> ' + ip + '<br><b>Calls granted:</b> ' + TRIAL_EXTENSION_CALLS + '</p>');
|
|
492
|
+
await sendEmail(email, TRIAL_EXTENSION_CALLS + ' extra free calls added -- VAT Validator MCP',
|
|
493
|
+
'<p>Hi ' + name + ',</p><p>Your ' + TRIAL_EXTENSION_CALLS + ' extra free calls have been added. You can keep using VAT Validator MCP right now -- no action needed.</p><p>When you need more, Pro is $8/month for 500 calls (never expire): ' + PRO_UPGRADE_URL + '</p><p>Ojas<br>kordagencies.com</p>');
|
|
494
|
+
res.writeHead(200, { ...cors, 'Content-Type': 'application/json' });
|
|
495
|
+
res.end(JSON.stringify({ granted: true, additional_calls: TRIAL_EXTENSION_CALLS, message: TRIAL_EXTENSION_CALLS + ' extra free calls added. Check your email for confirmation.', upgrade_url: PRO_UPGRADE_URL }));
|
|
496
|
+
} catch(e) { res.writeHead(400, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: e.message, agent_action: 'RETRY_IN_2_MIN' })); }
|
|
497
|
+
});
|
|
458
498
|
return;
|
|
459
499
|
}
|
|
460
500
|
|
|
@@ -522,6 +562,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
522
562
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
523
563
|
usageLog.push({ tool: name, tier: access.tier, time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
524
564
|
if (usageLog.length > 1000) usageLog.shift();
|
|
565
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
525
566
|
saveStats();
|
|
526
567
|
const result = await executeTool(name, args || {});
|
|
527
568
|
response = { jsonrpc: '2.0', id: request.id, result: { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] } };
|
|
@@ -545,8 +586,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
545
586
|
try {
|
|
546
587
|
const request = JSON.parse(body);
|
|
547
588
|
let response;
|
|
548
|
-
if (request.method
|
|
549
|
-
if (request.
|
|
589
|
+
if (request.method === 'tools/call') {
|
|
590
|
+
if (request.params?.name === 'batch_validate') {
|
|
550
591
|
const apiKey = req.headers['x-api-key'];
|
|
551
592
|
if (!apiKey) { res.writeHead(402, { ...cors, 'Content-Type': 'application/json' }); res.end(JSON.stringify({ jsonrpc: '2.0', id: request.id, error: { code: -32002, message: 'batch_validate requires a paid API key. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire.', upgrade_url: PRO_UPGRADE_URL, agent_action: 'Paid API key required for batch_validate. Get 500 calls for $8 at ' + PRO_UPGRADE_URL } })); return; }
|
|
552
593
|
const record = apiKeys.get(apiKey);
|
|
@@ -567,6 +608,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
567
608
|
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || 'unknown';
|
|
568
609
|
usageLog.push({ tool: name, tier: req._tier || 'paid', time: new Date().toISOString(), ip: ip.slice(0, 8) + '...' });
|
|
569
610
|
if (usageLog.length > 1000) usageLog.shift();
|
|
611
|
+
toolUsageCounts[name] = (toolUsageCounts[name] || 0) + 1;
|
|
570
612
|
saveStats();
|
|
571
613
|
const result = await executeTool(name, toolArgs || {});
|
|
572
614
|
if (req._accessWarning) result._notice = req._accessWarning;
|
|
@@ -577,12 +619,13 @@ const server = http.createServer(async (req, res) => {
|
|
|
577
619
|
const used = freeTierUsage.get(getMonthKey(ip)) || 0;
|
|
578
620
|
const remaining = FREE_TIER_LIMIT - used;
|
|
579
621
|
const isWarning = used >= FREE_TIER_WARNING;
|
|
622
|
+
const effectiveLimit = getEffectiveLimit(ip);
|
|
580
623
|
|
|
581
624
|
if (name === 'validate_vat' || name === 'validate_uk_vat') {
|
|
582
625
|
// Gate address on free tier — company name + valid status visible
|
|
583
626
|
const gated = ['registered_address', 'address', 'consultation_number'];
|
|
584
627
|
gated.forEach(f => delete result[f]);
|
|
585
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
628
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full registered address and HMRC consultation number.';
|
|
586
629
|
result._gated_fields = gated;
|
|
587
630
|
}
|
|
588
631
|
|
|
@@ -590,7 +633,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
590
633
|
// Gate full reasoning — verdict visible, details gated
|
|
591
634
|
const gated = ['fraud_signals', 'positive_indicators', 'recommended_action', 'summary'];
|
|
592
635
|
gated.forEach(f => delete result[f]);
|
|
593
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
636
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full fraud signal breakdown, positive indicators, and recommended action.';
|
|
594
637
|
result._gated_fields = gated;
|
|
595
638
|
}
|
|
596
639
|
|
|
@@ -598,7 +641,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
598
641
|
// Gate detail fields — match_status visible, discrepancies gated
|
|
599
642
|
const gated = ['discrepancies', 'name_match', 'address_match', 'recommended_action', 'summary'];
|
|
600
643
|
gated.forEach(f => delete result[f]);
|
|
601
|
-
result._upgrade_note = 'Free tier: ' + remaining + ' of ' +
|
|
644
|
+
result._upgrade_note = 'Free tier: ' + remaining + ' of ' + effectiveLimit + ' calls remaining. Get 500 calls for $8 at ' + PRO_UPGRADE_URL + ' -- calls never expire. Includes full discrepancy analysis and recommended action.';
|
|
602
645
|
result._gated_fields = gated;
|
|
603
646
|
}
|
|
604
647
|
|