nordic-data 0.2.0 → 0.4.0
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 +134 -88
- package/bin/cli.js +620 -13
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -1,118 +1,160 @@
|
|
|
1
|
-
# Nordic Data
|
|
1
|
+
# Nordic Data
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Norwegian, Swedish, and US business intelligence in one terminal — plus a hosted MCP server for AI agents.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npx nordic-data lookup 923609016 # full snapshot of Equinor
|
|
7
|
+
npx nordic-data list elektriker --city Stavanger --employees-min 5
|
|
8
|
+
npx nordic-data nl "Norwegian fintech startups under 20 employees"
|
|
9
|
+
npx nordic-data kyb 918274758 # full due-diligence pack
|
|
10
|
+
npx nordic-data verify-invoice 923609016 DE89370400440532013000
|
|
11
|
+
npx nordic-data us-search Microsoft
|
|
12
|
+
```
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
**Get a free key:** [nordicdata.cloud](https://nordicdata.cloud) — 500 requests/month, no card
|
|
15
|
+
**Docs:** [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
|
|
11
16
|
|
|
12
17
|
---
|
|
13
18
|
|
|
14
|
-
##
|
|
19
|
+
## Install
|
|
15
20
|
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
```bash
|
|
22
|
+
npx nordic-data --help # zero install
|
|
23
|
+
# or
|
|
24
|
+
npm install -g nordic-data
|
|
25
|
+
nordic-data --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configure your API key
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
export NORDIC_DATA_KEY=nrd_live_… # or pass --key on each call
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Most commands work without a key on the public widget tier (4 lookups per IP per 24h). Discovery, KYB, AI, and US commands all require a key.
|
|
26
35
|
|
|
27
|
-
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Commands
|
|
39
|
+
|
|
40
|
+
### Core lookup (Norway + Sweden)
|
|
41
|
+
| Command | What it does |
|
|
42
|
+
|---|---|
|
|
43
|
+
| `search <query>` | Search companies by name or org number |
|
|
44
|
+
| `lookup <orgnr>` | Full snapshot of one company (NO 9-digit + SE 10-digit auto-routes) |
|
|
45
|
+
| `contacts <orgnr>` | Emails, phones, and named executives |
|
|
46
|
+
| `board <orgnr>` | Board + leadership |
|
|
47
|
+
| `finances <orgnr>` | Latest financial summary |
|
|
48
|
+
| `procurement <orgnr>` | Public-sector contract aggregates |
|
|
49
|
+
| `grants <orgnr>` | EU R&D grant participations |
|
|
50
|
+
| `sanctions <orgnr>` | Sanctions screening (OFAC + EU) on company + officers |
|
|
51
|
+
| `shareholders <orgnr>` | Shareholder cap-table |
|
|
52
|
+
| `contacts-se <orgnr>` | Sweden: identity + AI-enriched contacts |
|
|
53
|
+
|
|
54
|
+
### Discovery (v0.4.0)
|
|
55
|
+
| Command | What it does |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `find <q>` | Universal resolver: name / orgnr / domain / email → company |
|
|
58
|
+
| `list <profession>` | Filter NO companies by industry (× `--city` × `--employees-min`) |
|
|
59
|
+
| `nl "<question>"` | Natural-language discovery — LLM parses to filter spec |
|
|
60
|
+
| `near <lat> <lng>` | Geo-radius search (`--radius` km, `--profession` <slug>) |
|
|
61
|
+
| `similar <orgnr>` | Lookalike companies by industry + geo + size |
|
|
62
|
+
| `trades <slug>` | Vertical shortcut: elektriker, rorlegger, snekker, tannlege, etc. |
|
|
63
|
+
| `decision-makers <orgnr>` | CEO + board + named contacts in one call |
|
|
64
|
+
| `resolve <type> <value>` | email / domain / phone / iban / vat / lei / address / person → entity |
|
|
65
|
+
|
|
66
|
+
### KYB & Compliance & AI (v0.4.0)
|
|
67
|
+
| Command | What it does |
|
|
68
|
+
|---|---|
|
|
69
|
+
| `kyb <orgnr>` | One-call due-diligence pack (identity + sanctions + officers + signals) |
|
|
70
|
+
| `diff <orgnr>` | Typed changelog of recent changes (`--since YYYY-MM-DD`) |
|
|
71
|
+
| `verify-invoice <o> <iban>` | Invoice fraud check via IBAN country-mismatch heuristic |
|
|
72
|
+
| `explain <orgnr>` | AI 3-sentence narrative |
|
|
73
|
+
| `ask <orgnr> "<q>"` | Q&A grounded in the company's data |
|
|
74
|
+
| `pitch <orgnr>` | Sales talking-points referencing concrete facts |
|
|
75
|
+
|
|
76
|
+
### United States (v0.4.0 — SEC EDGAR + USA Spending)
|
|
77
|
+
| Command | What it does |
|
|
78
|
+
|---|---|
|
|
79
|
+
| `us-search <query>` | Search ~10K US public companies by name / ticker |
|
|
80
|
+
| `us-lookup <q>` | Resolve a US public company by CIK, ticker, or name |
|
|
81
|
+
| `us-filings <cik>` | Recent SEC filings (`--type 10-K`) with direct EDGAR URLs |
|
|
82
|
+
| `us-kyb <cik>` | US KYB pack: identity + filings + sanctions + federal contracts |
|
|
83
|
+
|
|
84
|
+
### Meta
|
|
85
|
+
| Command | What it does |
|
|
86
|
+
|---|---|
|
|
87
|
+
| `mcp` | Print MCP config snippet for Claude Desktop / Cursor |
|
|
88
|
+
| `signup` | Open the free-tier signup in your browser |
|
|
89
|
+
| `--help`, `-h` | Help (per-command help with `<cmd> --help`) |
|
|
90
|
+
| `--version`, `-v` | Show version |
|
|
91
|
+
|
|
92
|
+
### Flags
|
|
93
|
+
- `--json` — output raw JSON instead of formatted
|
|
94
|
+
- `--key <api-key>` — API key (or set `NORDIC_DATA_KEY` env var)
|
|
95
|
+
- `--no-color` — disable ANSI colors
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## MCP server (for AI agents)
|
|
28
100
|
|
|
29
|
-
|
|
101
|
+
The same backend is also a [Model Context Protocol](https://modelcontextprotocol.io) server with **69 tools** exposed natively for AI agents — no SDK glue required.
|
|
30
102
|
|
|
31
|
-
|
|
103
|
+
### Hosted endpoint
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
URL: https://api.nordicdata.cloud/mcp
|
|
107
|
+
Header: X-API-Key: <your key>
|
|
108
|
+
Transport: Streamable HTTP
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Claude Desktop / Cursor config
|
|
112
|
+
|
|
113
|
+
Edit `claude_desktop_config.json` (or `~/.cursor/mcp.json`):
|
|
32
114
|
|
|
33
115
|
```json
|
|
34
116
|
{
|
|
35
117
|
"mcpServers": {
|
|
36
118
|
"nordic-data": {
|
|
37
119
|
"url": "https://api.nordicdata.cloud/mcp",
|
|
38
|
-
"headers": {
|
|
39
|
-
"X-API-Key": "YOUR_API_KEY_HERE"
|
|
40
|
-
}
|
|
120
|
+
"headers": { "X-API-Key": "YOUR_API_KEY_HERE" }
|
|
41
121
|
}
|
|
42
122
|
}
|
|
43
123
|
}
|
|
44
124
|
```
|
|
45
125
|
|
|
46
|
-
Restart
|
|
126
|
+
Restart the client — all 69 tools appear in the MCP picker.
|
|
47
127
|
|
|
48
|
-
###
|
|
128
|
+
### Smithery one-click install
|
|
49
129
|
|
|
50
|
-
|
|
130
|
+
[smithery.ai/server/sofia-jameson-20/Nordic-Data](https://smithery.ai/server/sofia-jameson-20/Nordic-Data)
|
|
51
131
|
|
|
52
|
-
|
|
132
|
+
---
|
|
53
133
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
134
|
+
## What's covered
|
|
135
|
+
|
|
136
|
+
- **Norwegian company registry** — full company data + complete shareholder cap tables (3M+ positions across 396K companies)
|
|
137
|
+
- **Swedish company data** — identity (~1.6M companies) + AI-enriched contacts (verified emails, direct phones, named executives)
|
|
138
|
+
- **Danish + Finnish company data** — identity + basic enrichment
|
|
139
|
+
- **United States public companies** — SEC EDGAR (~10K listed entities, full identity + filings) + USA Spending (federal contracts since 2008)
|
|
140
|
+
- **Officer & ownership network (Norway)** — board memberships, shortest-path queries, full role history
|
|
141
|
+
- **Public procurement (5 Nordic countries)** — EU-tier (above-threshold) + Norway-tier (below-EU-threshold municipal/county tenders)
|
|
142
|
+
- **Sanctions / AML** — OFAC SDN + EU consolidated lists, with strict-surname disambiguation to suppress first-name-only false positives
|
|
143
|
+
- **R&D grants** — Horizon Europe participations per recipient
|
|
144
|
+
- **Tech intelligence** — find companies using a specific technology stack
|
|
145
|
+
- **News** — Norwegian-language news mentioning a company
|
|
146
|
+
- **AI / NL** — natural-language search, Q&A, talking-points, narrative for any covered company
|
|
147
|
+
|
|
148
|
+
## Example prompts (via the MCP server in Claude/Cursor)
|
|
59
149
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
### Procurement
|
|
63
|
-
- `search_tenders` — Nordic procurement notices by country, keyword, CPV, date. Filter by coverage tier (EU-tier across all 5 Nordic countries, or Norway-tier including below-threshold municipal awards). Filter by `buyer_orgnr` to find all tenders from a specific Norwegian buyer.
|
|
64
|
-
- `get_tender` — Full details by ID. Accepts both EU-tier format (`317565-2026`) and Norway-tier format (`2026-108414`).
|
|
65
|
-
- `search_awards` — Search contract awards (who won what)
|
|
66
|
-
- `get_tender_leaderboard` — Top public-sector buyers in a Nordic country
|
|
67
|
-
- `get_company_contract_wins` — Public-sector contracts won by a Norwegian company
|
|
68
|
-
|
|
69
|
-
### Company data
|
|
70
|
-
- `search_companies` — Search by name, industry, location (Norway: full-text; Denmark + Finland: identity-only; Sweden + Iceland: not yet supported for full-text — use `get_company` for orgnr lookup)
|
|
71
|
-
- `get_company` — Full registry record. `country=NOR` (richest), `SWE` (identity), `DNK` (identity + phone + employees), `FIN` (identity + name history)
|
|
72
|
-
- `get_company_contact` — Norway: public email + phone (with MX-verified email candidates) + named executives
|
|
73
|
-
- **`get_company_se_contact` — Sweden: identity + AI-enriched contacts (MX-verified emails, direct phones, named executives where publicly documented)**
|
|
74
|
-
- `get_company_narrative` — AI-generated executive summary
|
|
75
|
-
- `get_company_peers` — Peer-cohort benchmarks
|
|
76
|
-
- `get_company_snapshot` — One-call snapshot across every data layer
|
|
77
|
-
- `get_company_changes` — Registry change history
|
|
78
|
-
- `get_company_subsidiaries` — Subsidiaries registered under this orgnr
|
|
79
|
-
- `bulk_get_companies` — Enrich a list of up to 100 companies in one call
|
|
80
|
-
|
|
81
|
-
### Financials & ownership (Norway)
|
|
82
|
-
- `get_company_accounts` — Annual accounts (revenue, profit, equity)
|
|
83
|
-
- `get_company_shareholders` — Full shareholder list — 3M+ positions across 396K companies
|
|
84
|
-
- `get_shareholder_portfolio` — All companies a person/entity owns shares in
|
|
85
|
-
|
|
86
|
-
### Officer & network graph (Norway)
|
|
87
|
-
- `search_persons` — Search persons in the Norwegian officer network
|
|
88
|
-
- `get_person` — Full role history across Norwegian companies
|
|
89
|
-
- `find_company_path` — Shortest path between two companies through shared officers
|
|
90
|
-
- `get_person_network` — Find who is connected to a person via shared boards
|
|
91
|
-
|
|
92
|
-
### News
|
|
93
|
-
- `get_company_news` — Recent Norwegian-language news mentioning a company
|
|
94
|
-
- `search_news` — Search Norwegian news headlines
|
|
95
|
-
|
|
96
|
-
### R&D
|
|
97
|
-
- `search_eu_grants` — Search EU research-grant history
|
|
98
|
-
- `get_company_eu_grants` — Norwegian company's R&D grant participations
|
|
99
|
-
|
|
100
|
-
### Tech intelligence
|
|
101
|
-
- `find_companies_using_tech` — Norwegian companies using a specific technology
|
|
102
|
-
|
|
103
|
-
### Compliance
|
|
104
|
-
- `screen_for_sanctions` — Screen any name against international sanctions lists
|
|
105
|
-
- `check_company_sanctions` — Sanctions screening for a Norwegian company + its officers
|
|
106
|
-
|
|
107
|
-
## Example prompts
|
|
108
|
-
|
|
109
|
-
- *"Which Norwegian municipalities tendered snow-clearing contracts under 5M NOK with deadlines in the next 30 days?"* (uses below-threshold municipal coverage)
|
|
150
|
+
- *"Find Norwegian electricians in Stavanger with 5+ employees and verified contact emails."*
|
|
110
151
|
- *"Pull the latest accounts and shareholders for orgnr 923609016."*
|
|
111
|
-
- *"
|
|
112
|
-
- *"
|
|
113
|
-
- *"
|
|
114
|
-
- *"
|
|
115
|
-
- *"
|
|
152
|
+
- *"Give me a full KYB pack on this counterparty before we onboard them."*
|
|
153
|
+
- *"Verify this invoice — orgnr 923609016 asking us to pay German IBAN DE89370400440532013000. Is that suspicious?"*
|
|
154
|
+
- *"Which Norwegian municipalities tendered snow-clearing contracts under 5M NOK with deadlines in the next 30 days?"*
|
|
155
|
+
- *"Resolve ceo@equinor.com to a company."*
|
|
156
|
+
- *"Find 20 lookalike companies for Cognite for our prospect list."*
|
|
157
|
+
- *"Look up Tesla's most recent 10-K filing."*
|
|
116
158
|
|
|
117
159
|
## Pricing
|
|
118
160
|
|
|
@@ -127,3 +169,7 @@ See [nordicdata.cloud](https://nordicdata.cloud).
|
|
|
127
169
|
- Docs: [nordicdata.cloud/docs](https://nordicdata.cloud/docs)
|
|
128
170
|
- LLM-friendly site index: [nordicdata.cloud/llms.txt](https://nordicdata.cloud/llms.txt)
|
|
129
171
|
- OpenAPI spec: [api.nordicdata.cloud/openapi.json](https://api.nordicdata.cloud/openapi.json)
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
MIT — see `LICENSE`.
|
package/bin/cli.js
CHANGED
|
@@ -28,8 +28,9 @@ const HELP = `${c('bold', 'nordic-data')} ${c('dim', '— every Norwegian compan
|
|
|
28
28
|
|
|
29
29
|
${c('bold', 'USAGE')}
|
|
30
30
|
nordic-data <command> [args]
|
|
31
|
+
nordic-data <command> --help ${c('dim', '# per-command help with examples')}
|
|
31
32
|
|
|
32
|
-
${c('bold', 'COMMANDS')}
|
|
33
|
+
${c('bold', 'COMMANDS — core lookup (NO + SE)')}
|
|
33
34
|
search <query> Search companies by name or org number
|
|
34
35
|
lookup <orgnr> Full snapshot of one company
|
|
35
36
|
contacts <orgnr> Emails, phones, and named executives
|
|
@@ -40,6 +41,32 @@ ${c('bold', 'COMMANDS')}
|
|
|
40
41
|
sanctions <orgnr> Sanctions screening (EU/UN/OFAC) hits
|
|
41
42
|
shareholders <orgnr> Shareholder graph aggregates (Norway)
|
|
42
43
|
contacts-se <orgnr> Sweden: identity + AI-enriched contacts (10-digit orgnr)
|
|
44
|
+
|
|
45
|
+
${c('bold', 'COMMANDS — discovery (v0.4.0)')}
|
|
46
|
+
find <q> Universal resolver: name / orgnr / domain / email → company
|
|
47
|
+
list <profession> Filter NO companies by industry (× --city × --employees-min)
|
|
48
|
+
nl "<question>" Natural-language discovery (LLM parses → filter spec → results)
|
|
49
|
+
near <lat> <lng> Geo-radius search (--radius km, --profession <slug>)
|
|
50
|
+
similar <orgnr> Lookalike companies by industry + geo + size
|
|
51
|
+
trades <slug> Vertical shortcut: elektriker, rorlegger, snekker, tannlege, etc.
|
|
52
|
+
decision-makers <orgnr> CEO + board + named contacts in one call
|
|
53
|
+
resolve <type> <value> email / domain / phone / iban / vat / lei / address / person → entity
|
|
54
|
+
|
|
55
|
+
${c('bold', 'COMMANDS — KYB / Compliance / AI')}
|
|
56
|
+
kyb <orgnr> One-call due-diligence pack (identity + sanctions + officers + signals)
|
|
57
|
+
diff <orgnr> Typed changelog of recent changes (--since YYYY-MM-DD)
|
|
58
|
+
explain <orgnr> AI-generated 3-sentence narrative about the company
|
|
59
|
+
ask <orgnr> "<question>" Q&A grounded in the company's data
|
|
60
|
+
pitch <orgnr> Sales talking-points (referencing concrete facts)
|
|
61
|
+
verify-invoice <o> <iban> Invoice fraud check via IBAN country-mismatch heuristic
|
|
62
|
+
|
|
63
|
+
${c('bold', 'COMMANDS — United States (SEC EDGAR + USA Spending)')}
|
|
64
|
+
us-search <query> Search US public companies by name / ticker
|
|
65
|
+
us-lookup <q> Resolve a US public company by CIK, ticker, or name
|
|
66
|
+
us-filings <cik> Recent SEC filings (--type 10-K to filter)
|
|
67
|
+
us-kyb <cik> US KYB pack (identity + filings + sanctions + federal contracts)
|
|
68
|
+
|
|
69
|
+
${c('bold', 'COMMANDS — meta')}
|
|
43
70
|
mcp Show MCP setup snippet for Claude Desktop / Cursor
|
|
44
71
|
signup Open the free-tier signup page in your browser
|
|
45
72
|
--help, -h Show this help
|
|
@@ -51,15 +78,39 @@ ${c('bold', 'FLAGS')}
|
|
|
51
78
|
--no-color Disable ANSI colors
|
|
52
79
|
|
|
53
80
|
${c('bold', 'EXAMPLES')}
|
|
54
|
-
${c('dim', '#
|
|
81
|
+
${c('dim', '# Core lookup')}
|
|
55
82
|
nordic-data search equinor
|
|
56
83
|
nordic-data lookup 923609016
|
|
57
84
|
nordic-data contacts 923609016
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
85
|
+
nordic-data contacts-se 5566370985 ${c('dim', '# Sweden (10-digit orgnr)')}
|
|
86
|
+
|
|
87
|
+
${c('dim', '# Discovery — find a list of companies')}
|
|
88
|
+
nordic-data list elektriker --city Stavanger --employees-min 5
|
|
89
|
+
nordic-data nl "dentists in Bergen with 10+ employees"
|
|
90
|
+
nordic-data trades snekker --city Tromsø
|
|
91
|
+
nordic-data near 58.97 5.73 --radius 5 --profession rorlegger
|
|
92
|
+
|
|
93
|
+
${c('dim', '# Universal resolver')}
|
|
94
|
+
nordic-data find Cognite ${c('dim', '# name → company')}
|
|
95
|
+
nordic-data resolve email ceo@equinor.com ${c('dim', '# email → company')}
|
|
96
|
+
nordic-data resolve domain cognite.com ${c('dim', '# domain → company')}
|
|
97
|
+
|
|
98
|
+
${c('dim', '# KYB / Compliance')}
|
|
99
|
+
nordic-data kyb 923609016
|
|
100
|
+
nordic-data verify-invoice 923609016 DE89370400440532013000
|
|
101
|
+
nordic-data diff 923609016 --since 2026-01-01
|
|
102
|
+
|
|
103
|
+
${c('dim', '# AI / NL')}
|
|
104
|
+
nordic-data explain 923609016
|
|
105
|
+
nordic-data ask 923609016 "who chairs the board"
|
|
106
|
+
nordic-data pitch 923609016
|
|
107
|
+
|
|
108
|
+
${c('dim', '# United States (SEC EDGAR + USA Spending)')}
|
|
109
|
+
nordic-data us-search Microsoft
|
|
110
|
+
nordic-data us-kyb 0000789019
|
|
111
|
+
nordic-data us-filings 0000320193 --type 10-K
|
|
112
|
+
|
|
113
|
+
${c('dim', '# Use your API key for full access (most v0.4.0 commands require a key)')}
|
|
63
114
|
export NORDIC_DATA_KEY=nrd_live_...
|
|
64
115
|
nordic-data lookup 923609016 --json | jq
|
|
65
116
|
|
|
@@ -72,22 +123,241 @@ ${c('bold', 'FREE TIER')}
|
|
|
72
123
|
${c('dim', 'Without a key, this CLI uses the public widget tier (4 lookups/IP/24h).')}
|
|
73
124
|
`;
|
|
74
125
|
|
|
126
|
+
// Per-command help text. Shown when user runs `nordic-data <cmd> --help`.
|
|
127
|
+
const COMMAND_HELP = {
|
|
128
|
+
search: `${c('bold', 'nordic-data search')} ${c('dim', '<query> [--json]')}
|
|
129
|
+
|
|
130
|
+
Search Norwegian companies by name or organisation number.
|
|
131
|
+
|
|
132
|
+
${c('bold', 'EXAMPLES')}
|
|
133
|
+
${c('dim', '# Fuzzy name search')}
|
|
134
|
+
nordic-data search equinor
|
|
135
|
+
|
|
136
|
+
${c('dim', '# JSON for scripting')}
|
|
137
|
+
nordic-data search "telenor" --json | jq '.results[0].orgnr'
|
|
138
|
+
|
|
139
|
+
${c('dim', '# Lookup by orgnr also works as a single-result search')}
|
|
140
|
+
nordic-data search 923609016
|
|
141
|
+
`,
|
|
142
|
+
lookup: `${c('bold', 'nordic-data lookup')} ${c('dim', '<orgnr> [--json]')}
|
|
143
|
+
|
|
144
|
+
Full snapshot of one company. Includes identity, address, key personnel,
|
|
145
|
+
contacts, and a sanctions hit count.
|
|
146
|
+
|
|
147
|
+
${c('bold', 'ORGNR FORMATS')}
|
|
148
|
+
9 digits ${c('dim', '— Norway (e.g. 923609016)')}
|
|
149
|
+
10 digits ${c('dim', '— Sweden (e.g. 5566370985 or 556637-0985) — auto-routes to Sweden command')}
|
|
150
|
+
|
|
151
|
+
${c('bold', 'EXAMPLES')}
|
|
152
|
+
${c('dim', '# Norway')}
|
|
153
|
+
nordic-data lookup 923609016
|
|
154
|
+
|
|
155
|
+
${c('dim', '# Sweden (10-digit orgnr — auto-detected)')}
|
|
156
|
+
nordic-data lookup 5566370985
|
|
157
|
+
|
|
158
|
+
${c('dim', '# Raw JSON')}
|
|
159
|
+
nordic-data lookup 923609016 --json | jq .identity.name
|
|
160
|
+
`,
|
|
161
|
+
contacts: `${c('bold', 'nordic-data contacts')} ${c('dim', '<orgnr> [--json]')}
|
|
162
|
+
|
|
163
|
+
Verified emails, phones, and named executives for a Norwegian company.
|
|
164
|
+
Cached 30 days. Empty when no public contact info is available.
|
|
165
|
+
|
|
166
|
+
${c('bold', 'EXAMPLES')}
|
|
167
|
+
nordic-data contacts 923609016
|
|
168
|
+
nordic-data contacts 923609016 --json
|
|
169
|
+
`,
|
|
170
|
+
'contacts-se': `${c('bold', 'nordic-data contacts-se')} ${c('dim', '<orgnr> [--json]')}
|
|
171
|
+
|
|
172
|
+
Sweden: identity + AI-enriched contacts (verified emails, phones, named executives).
|
|
173
|
+
|
|
174
|
+
${c('bold', 'EXAMPLES')}
|
|
175
|
+
nordic-data contacts-se 5566370985
|
|
176
|
+
nordic-data contacts-se 556637-0985 ${c('dim', '# dash is accepted')}
|
|
177
|
+
`,
|
|
178
|
+
board: `${c('bold', 'nordic-data board')} ${c('dim', '<orgnr> [--json]')}
|
|
179
|
+
|
|
180
|
+
Current board + leadership for a Norwegian company. Shows role category
|
|
181
|
+
(styre / ledelse / other) and full role description.
|
|
182
|
+
|
|
183
|
+
${c('bold', 'EXAMPLES')}
|
|
184
|
+
nordic-data board 923609016
|
|
185
|
+
`,
|
|
186
|
+
finances: `${c('bold', 'nordic-data finances')} ${c('dim', '<orgnr> [--json]')}
|
|
187
|
+
|
|
188
|
+
Latest annual accounts for a Norwegian company: revenue, operating profit,
|
|
189
|
+
net result, balance sheet totals, equity, and computed ratios.
|
|
190
|
+
|
|
191
|
+
${c('bold', 'EXAMPLES')}
|
|
192
|
+
nordic-data finances 923609016
|
|
193
|
+
nordic-data finances 923609016 --json | jq .ratios
|
|
194
|
+
`,
|
|
195
|
+
procurement: `${c('bold', 'nordic-data procurement')} ${c('dim', '<orgnr> [--json]')}
|
|
196
|
+
|
|
197
|
+
Public-sector contract aggregates for a Norwegian company — count of
|
|
198
|
+
contracts won, estimated total value, top buyers.
|
|
199
|
+
|
|
200
|
+
${c('bold', 'EXAMPLES')}
|
|
201
|
+
nordic-data procurement 923609016
|
|
202
|
+
`,
|
|
203
|
+
grants: `${c('bold', 'nordic-data grants')} ${c('dim', '<orgnr> [--json]')}
|
|
204
|
+
|
|
205
|
+
EU R&D grant participations for a Norwegian company. Returns each grant
|
|
206
|
+
with role (coordinator/participant), EU contribution, project budget.
|
|
207
|
+
|
|
208
|
+
${c('bold', 'EXAMPLES')}
|
|
209
|
+
nordic-data grants 923609016
|
|
210
|
+
`,
|
|
211
|
+
sanctions: `${c('bold', 'nordic-data sanctions')} ${c('dim', '<orgnr> [--json]')}
|
|
212
|
+
|
|
213
|
+
Sanctions / AML / KYC check. Screens the company AND its officers against
|
|
214
|
+
international watchlists (OFAC SDN; EU + UN forthcoming). Auto-fetches
|
|
215
|
+
officers if not yet cached — one call gives you the full picture.
|
|
216
|
+
|
|
217
|
+
${c('bold', 'EXAMPLES')}
|
|
218
|
+
nordic-data sanctions 923609016
|
|
219
|
+
`,
|
|
220
|
+
shareholders: `${c('bold', 'nordic-data shareholders')} ${c('dim', '<orgnr> [--json]')}
|
|
221
|
+
|
|
222
|
+
Shareholder cap table for a Norwegian AS — ownership %, share count, identity
|
|
223
|
+
of each holder.
|
|
224
|
+
|
|
225
|
+
${c('bold', 'EXAMPLES')}
|
|
226
|
+
nordic-data shareholders 923609016
|
|
227
|
+
`,
|
|
228
|
+
mcp: `${c('bold', 'nordic-data mcp')}
|
|
229
|
+
|
|
230
|
+
Print the MCP server config snippet to drop into Claude Desktop / Cursor.
|
|
231
|
+
Set NORDIC_DATA_KEY first to embed your key in the snippet.
|
|
232
|
+
|
|
233
|
+
${c('bold', 'EXAMPLES')}
|
|
234
|
+
${c('dim', '# Print the snippet')}
|
|
235
|
+
nordic-data mcp
|
|
236
|
+
|
|
237
|
+
${c('dim', '# Wire your key in the printed snippet')}
|
|
238
|
+
export NORDIC_DATA_KEY=nrd_live_...
|
|
239
|
+
nordic-data mcp
|
|
240
|
+
`,
|
|
241
|
+
signup: `${c('bold', 'nordic-data signup')}
|
|
242
|
+
|
|
243
|
+
Open the free-tier signup page in your browser. 500 requests/month, no card.
|
|
244
|
+
`,
|
|
245
|
+
// v0.4.0 — Discovery + KYB + AI/NL + US
|
|
246
|
+
find: `${c('bold', 'nordic-data find')} ${c('dim', '<query>')}
|
|
247
|
+
Universal resolver. Pass anything (name, orgnr, domain, email); get the best company match plus alternates.
|
|
248
|
+
nordic-data find Cognite
|
|
249
|
+
nordic-data find 918274758
|
|
250
|
+
nordic-data find cognite.com
|
|
251
|
+
nordic-data find ceo@equinor.com
|
|
252
|
+
`,
|
|
253
|
+
list: `${c('bold', 'nordic-data list')} ${c('dim', '<profession> [--city <city>] [--employees-min N] [--employees-max N] [--limit N]')}
|
|
254
|
+
Filter Norwegian companies by plain-text profession (auto-mapped to NACE) + city + size.
|
|
255
|
+
nordic-data list elektriker --city Stavanger --employees-min 5
|
|
256
|
+
nordic-data list tannlege --city Bergen
|
|
257
|
+
nordic-data list snekker --city Tromsø --limit 20
|
|
258
|
+
`,
|
|
259
|
+
nl: `${c('bold', 'nordic-data nl')} ${c('dim', '"<natural-language query>"')}
|
|
260
|
+
LLM parses your sentence into a filter spec and runs the search. Returns the parsed filter plus matching companies.
|
|
261
|
+
nordic-data nl "electricians in Stavanger with 5+ employees"
|
|
262
|
+
nordic-data nl "Norwegian fintech startups under 20 employees"
|
|
263
|
+
`,
|
|
264
|
+
kyb: `${c('bold', 'nordic-data kyb')} ${c('dim', '<orgnr>')}
|
|
265
|
+
One-call due-diligence bundle: identity + officers + sanctions (company + all officers) + bankruptcy + sister companies + recent changes + shell-likelihood signals.
|
|
266
|
+
nordic-data kyb 923609016
|
|
267
|
+
`,
|
|
268
|
+
explain: `${c('bold', 'nordic-data explain')} ${c('dim', '<orgnr>')}
|
|
269
|
+
AI-generated 3-sentence narrative: who the company is, who runs it, where they're going. Uses only structured registry data — no invented facts.
|
|
270
|
+
nordic-data explain 923609016
|
|
271
|
+
`,
|
|
272
|
+
ask: `${c('bold', 'nordic-data ask')} ${c('dim', '<orgnr> "<question>"')}
|
|
273
|
+
Q&A grounded in the company's registry + contact + contract data. Answers "Not available in the data provided" when the answer isn't in the data.
|
|
274
|
+
nordic-data ask 923609016 "who chairs the board"
|
|
275
|
+
nordic-data ask 923609016 "how many employees and where are they based"
|
|
276
|
+
`,
|
|
277
|
+
pitch: `${c('bold', 'nordic-data pitch')} ${c('dim', '<orgnr>')}
|
|
278
|
+
Sales talking points (4-5 numbered items) referencing concrete facts: recent contract wins, new execs, growth signals.
|
|
279
|
+
nordic-data pitch 923609016
|
|
280
|
+
`,
|
|
281
|
+
'verify-invoice': `${c('bold', 'nordic-data verify-invoice')} ${c('dim', '<orgnr> <iban>')}
|
|
282
|
+
Invoice fraud heuristic via IBAN-country-mismatch check. If a supposed Norwegian supplier asks you to pay a German IBAN, that's a classic CEO-fraud pattern.
|
|
283
|
+
nordic-data verify-invoice 923609016 DE89370400440532013000
|
|
284
|
+
nordic-data verify-invoice 923609016 NO9386011117947
|
|
285
|
+
`,
|
|
286
|
+
resolve: `${c('bold', 'nordic-data resolve')} ${c('dim', '<type> <value>')}
|
|
287
|
+
Resolvers — anything → entity. Types: email, domain, phone, iban, vat, lei, address, person, wikipedia.
|
|
288
|
+
nordic-data resolve email ceo@equinor.com
|
|
289
|
+
nordic-data resolve domain cognite.com
|
|
290
|
+
nordic-data resolve phone "+47 519 90 000"
|
|
291
|
+
nordic-data resolve iban DE89370400440532013000
|
|
292
|
+
`,
|
|
293
|
+
near: `${c('bold', 'nordic-data near')} ${c('dim', '<lat> <lng> [--radius 5] [--profession <slug>] [--limit N]')}
|
|
294
|
+
Geo-radius search around a coordinate. Optional industry filter.
|
|
295
|
+
nordic-data near 58.97 5.73 --radius 5 --profession rorlegger
|
|
296
|
+
`,
|
|
297
|
+
similar: `${c('bold', 'nordic-data similar')} ${c('dim', '<orgnr>')}
|
|
298
|
+
Find up to 20 lookalike companies (same industry + country, prioritised by same-city and closest employee count).
|
|
299
|
+
nordic-data similar 918274758
|
|
300
|
+
`,
|
|
301
|
+
'decision-makers': `${c('bold', 'nordic-data decision-makers')} ${c('dim', '<orgnr>')}
|
|
302
|
+
Synthesised list of key roles (CEO, CFO, chair, board) plus role-labelled named contacts.
|
|
303
|
+
nordic-data decision-makers 923609016
|
|
304
|
+
`,
|
|
305
|
+
trades: `${c('bold', 'nordic-data trades')} ${c('dim', '<slug> [--city <city>] [--limit N]')}
|
|
306
|
+
Vertical shortcut for common Norwegian trades. Slugs include: elektriker, rorlegger, snekker, tomrer, maler, murer, tannlege, lege, advokat, frisor, restaurant, kafe, etc.
|
|
307
|
+
nordic-data trades elektriker --city Oslo
|
|
308
|
+
`,
|
|
309
|
+
diff: `${c('bold', 'nordic-data diff')} ${c('dim', '<orgnr> [--since YYYY-MM-DD]')}
|
|
310
|
+
Typed changelog: board changes, ownership transfers, bankruptcy events etc. since a given date (default 30 days ago).
|
|
311
|
+
nordic-data diff 923609016 --since 2026-01-01
|
|
312
|
+
`,
|
|
313
|
+
'us-search': `${c('bold', 'nordic-data us-search')} ${c('dim', '<query>')}
|
|
314
|
+
Search ~10,000 US public companies (SEC EDGAR) by name or ticker.
|
|
315
|
+
nordic-data us-search Microsoft
|
|
316
|
+
nordic-data us-search AAPL
|
|
317
|
+
`,
|
|
318
|
+
'us-lookup': `${c('bold', 'nordic-data us-lookup')} ${c('dim', '<query>')}
|
|
319
|
+
Resolve a US public company by CIK, ticker, or name. Returns SEC identity (SIC, state, city, address).
|
|
320
|
+
nordic-data us-lookup Tesla
|
|
321
|
+
nordic-data us-lookup 0000789019
|
|
322
|
+
`,
|
|
323
|
+
'us-filings': `${c('bold', 'nordic-data us-filings')} ${c('dim', '<cik> [--type 10-K]')}
|
|
324
|
+
Recent SEC filings for a US public company with direct EDGAR URLs.
|
|
325
|
+
nordic-data us-filings 0000320193 --type 10-K
|
|
326
|
+
`,
|
|
327
|
+
'us-kyb': `${c('bold', 'nordic-data us-kyb')} ${c('dim', '<cik>')}
|
|
328
|
+
US KYB pack: SEC identity + recent filings + sanctions screen + recent federal contracts (USA Spending).
|
|
329
|
+
nordic-data us-kyb 0000789019
|
|
330
|
+
`,
|
|
331
|
+
};
|
|
332
|
+
|
|
75
333
|
const args = process.argv.slice(2);
|
|
76
334
|
let useJson = false;
|
|
77
335
|
let useColor = isTTY;
|
|
336
|
+
let wantsHelp = false;
|
|
78
337
|
const cleanArgs = [];
|
|
79
338
|
for (let i = 0; i < args.length; i++) {
|
|
80
339
|
const a = args[i];
|
|
81
340
|
if (a === '--json') useJson = true;
|
|
82
341
|
else if (a === '--no-color') useColor = false;
|
|
83
342
|
else if (a === '--key') { process.env.NORDIC_DATA_KEY = args[++i] || ''; }
|
|
84
|
-
else if (a === '--help' || a === '-h')
|
|
343
|
+
else if (a === '--help' || a === '-h') wantsHelp = true;
|
|
85
344
|
else if (a === '--version' || a === '-v') {
|
|
86
345
|
const { version } = require('../package.json');
|
|
87
346
|
console.log(version);
|
|
88
347
|
process.exit(0);
|
|
89
348
|
} else cleanArgs.push(a);
|
|
90
349
|
}
|
|
350
|
+
|
|
351
|
+
// Resolve per-command help: `nordic-data <cmd> --help` shows command-specific help.
|
|
352
|
+
// `nordic-data --help` (no command) shows the global help.
|
|
353
|
+
if (wantsHelp) {
|
|
354
|
+
if (cleanArgs.length > 0 && COMMAND_HELP[cleanArgs[0]]) {
|
|
355
|
+
console.log(COMMAND_HELP[cleanArgs[0]]);
|
|
356
|
+
} else {
|
|
357
|
+
console.log(HELP);
|
|
358
|
+
}
|
|
359
|
+
process.exit(0);
|
|
360
|
+
}
|
|
91
361
|
if (cleanArgs.length === 0) { console.log(HELP); process.exit(0); }
|
|
92
362
|
|
|
93
363
|
const [cmd, ...rest] = cleanArgs;
|
|
@@ -104,10 +374,26 @@ async function publicRequest(path) {
|
|
|
104
374
|
|
|
105
375
|
const res = await fetch(`${API_BASE}${path}`, { headers });
|
|
106
376
|
if (!res.ok) {
|
|
107
|
-
if (res.status === 429) die('Rate limited. Free tier is 4 lookups per IP per 24h. Set NORDIC_DATA_KEY=... for higher limits.');
|
|
108
|
-
if (res.status === 401) die('Auth required. Set NORDIC_DATA_KEY=... or
|
|
109
|
-
|
|
110
|
-
|
|
377
|
+
if (res.status === 429) die('Rate limited. Free tier is 4 lookups per IP per 24h. Set NORDIC_DATA_KEY=... for higher limits.\n Sign up at https://nordicdata.cloud/?signup=free (500 requests/month, no card).');
|
|
378
|
+
if (res.status === 401) die('Auth required. Set NORDIC_DATA_KEY=... or pass --key <key>.\n Sign up at https://nordicdata.cloud/?signup=free (500 requests/month, no card).');
|
|
379
|
+
// Try to parse a structured JSON error and surface a useful message + hint.
|
|
380
|
+
let body = null;
|
|
381
|
+
try { body = await res.json(); } catch {}
|
|
382
|
+
const errCode = body && body.error;
|
|
383
|
+
const errMsg = body && body.message;
|
|
384
|
+
if (res.status === 404) {
|
|
385
|
+
if (errCode === 'not_found' && body.orgnr) {
|
|
386
|
+
die(`Company ${body.orgnr} not found in the official register.\n ${c('dim', 'If you do not know the orgnr, try:')} nordic-data search <name>`);
|
|
387
|
+
}
|
|
388
|
+
die(`Not found.\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find the orgnr first.')}`);
|
|
389
|
+
}
|
|
390
|
+
if (res.status === 400 && errCode === 'invalid_orgnr') {
|
|
391
|
+
die(`Invalid Norwegian organisation number — must be 9 digits.\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find the orgnr.')}`);
|
|
392
|
+
}
|
|
393
|
+
if (res.status === 400 && errMsg) die(errMsg);
|
|
394
|
+
if (res.status === 402) die(`Payment required. ${errMsg || 'Plan does not include this endpoint.'}\n See plans: https://nordicdata.cloud/#pricing`);
|
|
395
|
+
if (res.status >= 500) die(`Server error (${res.status}). Try again in a moment. If it persists, email support@nordicdata.cloud.`);
|
|
396
|
+
die(`API error ${res.status}: ${errMsg || (body && JSON.stringify(body)) || 'unknown error'}`);
|
|
111
397
|
}
|
|
112
398
|
return res.json();
|
|
113
399
|
}
|
|
@@ -153,7 +439,16 @@ async function search(query) {
|
|
|
153
439
|
}
|
|
154
440
|
|
|
155
441
|
async function lookup(orgnr) {
|
|
156
|
-
if (!orgnr) die('Usage: nordic-data lookup <orgnr
|
|
442
|
+
if (!orgnr) die('Usage: nordic-data lookup <orgnr>\n Norway: 9-digit orgnr. Sweden: 10-digit orgnr (with or without dash).');
|
|
443
|
+
// Auto-route Swedish orgnrs (10 digits, with or without dash) to the SE flow.
|
|
444
|
+
const cleaned = String(orgnr).replace(/[-\s]/g, '');
|
|
445
|
+
if (/^\d{10}$/.test(cleaned)) {
|
|
446
|
+
console.log(c('dim', `(10-digit orgnr detected — routing to Sweden command)\n`));
|
|
447
|
+
return contactsSe(orgnr);
|
|
448
|
+
}
|
|
449
|
+
if (!/^\d{9}$/.test(cleaned)) {
|
|
450
|
+
die(`Invalid orgnr "${orgnr}".\n Norway uses 9 digits (e.g. 923609016).\n Sweden uses 10 digits (e.g. 5566370985 or 556637-0985).\n ${c('dim', 'Tip:')} nordic-data search <name> ${c('dim', 'to find it.')}`);
|
|
451
|
+
}
|
|
157
452
|
const snap = await publicRequest(`/_/look/${orgnr}`);
|
|
158
453
|
if (useJson) return console.log(JSON.stringify(snap, null, 2));
|
|
159
454
|
|
|
@@ -384,6 +679,296 @@ function fmt(n, currency) {
|
|
|
384
679
|
return `${cur} ${n.toLocaleString('nb-NO')}`;
|
|
385
680
|
}
|
|
386
681
|
|
|
682
|
+
// ── v0.4.0 — Discovery + KYB + AI/NL + US ─────────────────
|
|
683
|
+
// Helper to parse --flag value args from a sub-array.
|
|
684
|
+
function flag(arr, name, def) {
|
|
685
|
+
const i = arr.indexOf(`--${name}`);
|
|
686
|
+
if (i < 0 || i === arr.length - 1) return def;
|
|
687
|
+
return arr[i + 1];
|
|
688
|
+
}
|
|
689
|
+
function bool(arr, name) { return arr.includes(`--${name}`); }
|
|
690
|
+
|
|
691
|
+
// Find: universal lookup (resolves name / orgnr / domain / email)
|
|
692
|
+
async function find(q) {
|
|
693
|
+
if (!q) die('Usage: nordic-data find <query>\n Pass anything — name, orgnr, domain, or email.');
|
|
694
|
+
const data = await publicRequest(`/companies/lookup?q=${encodeURIComponent(q)}`);
|
|
695
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
696
|
+
if (!data.resolved) {
|
|
697
|
+
console.log(c('dim', `No match for "${q}".`));
|
|
698
|
+
if (data.note) console.log(c('dim', ' ' + data.note));
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const r = data.resolved;
|
|
702
|
+
header(`${r.name || 'Match'}${r.orgnr ? c('dim', ` (${r.orgnr})`) : ''}`);
|
|
703
|
+
pretty('Resolved by', data.resolved_by);
|
|
704
|
+
pretty('Country', r.country || (r.business_address && r.business_address.country_code) || '');
|
|
705
|
+
pretty('Status', r.status || '');
|
|
706
|
+
pretty('City', (r.business_address && r.business_address.city) || r.city || '');
|
|
707
|
+
if (r.nace && r.nace[0]) pretty('NACE', `${r.nace[0].code} ${r.nace[0].description || ''}`);
|
|
708
|
+
pretty('Employees', r.employees);
|
|
709
|
+
pretty('Website', r.website);
|
|
710
|
+
if (data.alternates && data.alternates.length) {
|
|
711
|
+
console.log(c('dim', `\n ${data.alternates.length} alternate match(es):`));
|
|
712
|
+
for (const a of data.alternates.slice(0, 5)) console.log(` ${c('dim', a.orgnr || '')} ${a.name}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// List: filter Norwegian companies by industry × city × size
|
|
717
|
+
async function list(args) {
|
|
718
|
+
const profession = args[0];
|
|
719
|
+
if (!profession) die('Usage: nordic-data list <profession> [--city <city>] [--employees-min N] [--employees-max N] [--limit N]\n Examples:\n nordic-data list elektriker --city Stavanger --employees-min 5\n nordic-data list tannlege --city Bergen\n nordic-data list snekker --city Tromsø');
|
|
720
|
+
const params = new URLSearchParams({ profession });
|
|
721
|
+
if (flag(args, 'city')) params.set('city', flag(args, 'city'));
|
|
722
|
+
if (flag(args, 'employees-min')) params.set('min_employees', flag(args, 'employees-min'));
|
|
723
|
+
if (flag(args, 'employees-max')) params.set('max_employees', flag(args, 'employees-max'));
|
|
724
|
+
if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
|
|
725
|
+
if (bool(args, 'active')) params.set('active', 'true');
|
|
726
|
+
const data = await publicRequest(`/companies/no/list?${params}`);
|
|
727
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
728
|
+
header(`${data.count} ${profession}${data.filter && data.filter.city ? ` in ${data.filter.city}` : ''}`);
|
|
729
|
+
for (const r of (data.results || []).slice(0, 50)) {
|
|
730
|
+
const city = (r.business_address && r.business_address.city) || '';
|
|
731
|
+
const emp = r.employees != null ? c('dim', ` · ${r.employees} emp`) : '';
|
|
732
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}${emp}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
// NL: natural-language discovery
|
|
737
|
+
async function nl(query) {
|
|
738
|
+
if (!query) die('Usage: nordic-data nl "<natural language query>"\n Examples:\n nordic-data nl "electricians in Stavanger with 5+ employees"\n nordic-data nl "dentists in Bergen"');
|
|
739
|
+
const data = await publicRequest(`/companies/nl-search?q=${encodeURIComponent(query)}`);
|
|
740
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
741
|
+
if (data.filter_spec) {
|
|
742
|
+
console.log(c('dim', `Interpreted as: ${JSON.stringify(data.filter_spec)}`));
|
|
743
|
+
}
|
|
744
|
+
header(`${data.count} match(es)`);
|
|
745
|
+
for (const r of (data.results || []).slice(0, 50)) {
|
|
746
|
+
const city = (r.business_address && r.business_address.city) || '';
|
|
747
|
+
const emp = r.employees != null ? c('dim', ` · ${r.employees} emp`) : '';
|
|
748
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}${emp}`);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// KYB: one-call due-diligence bundle
|
|
753
|
+
async function kyb(orgnr) {
|
|
754
|
+
if (!orgnr) die('Usage: nordic-data kyb <orgnr>\n Returns: identity + officers + sanctions on company AND officers + sister companies + recent changes + shell-likelihood.');
|
|
755
|
+
const data = await publicRequest(`/companies/${orgnr}/kyb-pack`);
|
|
756
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
757
|
+
const i = data.identity || {};
|
|
758
|
+
header(`KYB pack — ${i.name || orgnr}`);
|
|
759
|
+
pretty('Orgnr', data.orgnr);
|
|
760
|
+
pretty('Status', i.status);
|
|
761
|
+
pretty('Employees', i.employees);
|
|
762
|
+
pretty('Generated', data.generated_at);
|
|
763
|
+
if (data.officers && data.officers.all) {
|
|
764
|
+
console.log(c('bold', `\n Officers (${data.officers.all.length}):`));
|
|
765
|
+
for (const o of data.officers.all.slice(0, 10)) {
|
|
766
|
+
console.log(` ${c('orange', (o.role || '').padEnd(20))} ${c('bold', (o.person && o.person.name) || o.name || '')}`);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
const sa = data.sanctions || {};
|
|
770
|
+
console.log(c('bold', '\n Sanctions:'));
|
|
771
|
+
console.log(` Company matches: ${(sa.company_matches || []).length}`);
|
|
772
|
+
const om = (sa.officer_matches || []).filter(x => (x.matches || []).length).length;
|
|
773
|
+
console.log(` Officers with hits: ${om}`);
|
|
774
|
+
if (data.sister_companies && data.sister_companies.length) pretty('Sister cos', data.sister_companies.length);
|
|
775
|
+
if (data.recent_changes && data.recent_changes.length) pretty('Recent changes', data.recent_changes.length);
|
|
776
|
+
const sh = data.shell_signals || {};
|
|
777
|
+
const shell = [sh.no_employees && 'no employees', sh.fresh && 'fresh<1y', sh.no_website && 'no website'].filter(Boolean).join(', ');
|
|
778
|
+
if (shell) pretty('Shell signals', c('yellow', shell));
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Explain: AI 3-sentence narrative
|
|
782
|
+
async function explain(orgnr) {
|
|
783
|
+
if (!orgnr) die('Usage: nordic-data explain <orgnr>');
|
|
784
|
+
const data = await publicRequest(`/companies/${orgnr}/explain`);
|
|
785
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
786
|
+
header(`Narrative for ${orgnr}`);
|
|
787
|
+
console.log(` ${data.narrative || c('dim', '(no narrative)')}`);
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// Ask: Q&A about a company
|
|
791
|
+
async function ask(args) {
|
|
792
|
+
const orgnr = args[0];
|
|
793
|
+
const question = args.slice(1).join(' ');
|
|
794
|
+
if (!orgnr || !question) die('Usage: nordic-data ask <orgnr> <question>\n Example: nordic-data ask 923609016 "who chairs the board"');
|
|
795
|
+
const data = await publicRequest(`/companies/${orgnr}/q?question=${encodeURIComponent(question)}`);
|
|
796
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
797
|
+
header(`Q: ${question}`);
|
|
798
|
+
console.log(` ${data.answer || c('dim', '(no answer)')}`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Pitch: sales talking points
|
|
802
|
+
async function pitch(orgnr) {
|
|
803
|
+
if (!orgnr) die('Usage: nordic-data pitch <orgnr>\n Returns 4-5 sales talking points referencing concrete facts.');
|
|
804
|
+
const data = await publicRequest(`/companies/${orgnr}/talking-points`);
|
|
805
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
806
|
+
header(`Sales talking points for ${orgnr}`);
|
|
807
|
+
console.log(data.talking_points || c('dim', '(none)'));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Verify invoice
|
|
811
|
+
async function verifyInvoice(args) {
|
|
812
|
+
const orgnr = args[0];
|
|
813
|
+
const iban = args[1];
|
|
814
|
+
if (!orgnr || !iban) die('Usage: nordic-data verify-invoice <orgnr> <iban>\n Flags fraud risk if IBAN country mismatches the company’s registered country.');
|
|
815
|
+
const data = await publicRequest(`/verify/invoice?orgnr=${encodeURIComponent(orgnr)}&iban=${encodeURIComponent(iban)}`);
|
|
816
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
817
|
+
header(`Invoice check — orgnr ${orgnr} → IBAN ${iban}`);
|
|
818
|
+
const risk = data.risk === 'high' ? c('red', 'HIGH') : data.risk === 'low' ? c('green', 'LOW') : data.risk;
|
|
819
|
+
pretty('Risk', risk);
|
|
820
|
+
pretty('IBAN country', data.iban_country);
|
|
821
|
+
pretty('Company country', data.company_country);
|
|
822
|
+
console.log('\n ' + c('dim', data.reason || ''));
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Universal /resolve/* — type one of: email, domain, phone, iban, vat, lei, address, person, wikipedia
|
|
826
|
+
async function resolve(args) {
|
|
827
|
+
const type = args[0];
|
|
828
|
+
const value = args[1];
|
|
829
|
+
const validTypes = ['email','domain','phone','iban','vat','lei','address','person','wikipedia'];
|
|
830
|
+
if (!type || !validTypes.includes(type)) die(`Usage: nordic-data resolve <type> <value>\n Type: ${validTypes.join(' | ')}\n Examples:\n nordic-data resolve email ceo@equinor.com\n nordic-data resolve domain cognite.com\n nordic-data resolve phone "+47 519 90 000"\n nordic-data resolve iban DE89370400440532013000`);
|
|
831
|
+
if (!value) die(`Usage: nordic-data resolve ${type} <value>`);
|
|
832
|
+
const param = type === 'email' ? 'address' : type === 'iban' ? 'iban' : type === 'vat' ? 'vat' : type === 'lei' ? 'lei' : type === 'wikipedia' ? 'title' : type === 'address' ? 'street' : type === 'person' ? 'name' : type === 'phone' ? 'number' : 'domain';
|
|
833
|
+
const data = await publicRequest(`/resolve/${type}?${param}=${encodeURIComponent(value)}`);
|
|
834
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
835
|
+
console.log(JSON.stringify(data, null, 2));
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// Near: geo-radius search
|
|
839
|
+
async function near(args) {
|
|
840
|
+
const lat = args[0];
|
|
841
|
+
const lng = args[1];
|
|
842
|
+
if (!lat || !lng) die('Usage: nordic-data near <lat> <lng> [--radius 5] [--profession <profession>] [--limit N]');
|
|
843
|
+
const params = new URLSearchParams({ lat, lng });
|
|
844
|
+
params.set('radius_km', flag(args, 'radius', '5'));
|
|
845
|
+
if (flag(args, 'profession')) params.set('profession', flag(args, 'profession'));
|
|
846
|
+
if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
|
|
847
|
+
const data = await publicRequest(`/companies/near?${params}`);
|
|
848
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
849
|
+
header(`${data.count} companies within ${params.get('radius_km')} km of ${lat},${lng}`);
|
|
850
|
+
for (const r of (data.results || []).slice(0, 50)) {
|
|
851
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${c('dim', ` · ${r.distance_km} km`)}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Similar companies
|
|
856
|
+
async function similar(orgnr) {
|
|
857
|
+
if (!orgnr) die('Usage: nordic-data similar <orgnr>\n Returns 20 lookalike companies based on industry + geo + size.');
|
|
858
|
+
const data = await publicRequest(`/companies/similar/${orgnr}?limit=20`);
|
|
859
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
860
|
+
header(`Similar to ${orgnr}`);
|
|
861
|
+
for (const r of (data.similar || []).slice(0, 30)) {
|
|
862
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${r.city ? c('dim', ` · ${r.city}`) : ''}${r.employees ? c('dim', ` · ${r.employees} emp`) : ''}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Decision makers
|
|
867
|
+
async function decisionMakers(orgnr) {
|
|
868
|
+
if (!orgnr) die('Usage: nordic-data decision-makers <orgnr>');
|
|
869
|
+
const data = await publicRequest(`/companies/${orgnr}/decision-makers`);
|
|
870
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
871
|
+
header(`Decision-makers for ${orgnr}`);
|
|
872
|
+
for (const o of (data.key_officers || [])) {
|
|
873
|
+
console.log(` ${c('orange', (o.role || '').padEnd(24))} ${c('bold', (o.person && o.person.name) || '')}`);
|
|
874
|
+
}
|
|
875
|
+
if ((data.named_contacts || []).length) {
|
|
876
|
+
console.log(c('dim', '\n Named contacts:'));
|
|
877
|
+
for (const n of data.named_contacts) {
|
|
878
|
+
console.log(` ${c('orange', n.role)} ${c('bold', n.name)}${n.email ? c('dim', ` · ${n.email}`) : ''}`);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// Trades vertical shortcut
|
|
884
|
+
async function trades(args) {
|
|
885
|
+
const slug = args[0];
|
|
886
|
+
if (!slug) die('Usage: nordic-data trades <slug> [--city <city>]\n Examples: nordic-data trades elektriker --city Oslo\n nordic-data trades rorlegger --city Stavanger\n nordic-data trades snekker --city Tromsø');
|
|
887
|
+
const params = new URLSearchParams();
|
|
888
|
+
if (flag(args, 'city')) params.set('city', flag(args, 'city'));
|
|
889
|
+
if (flag(args, 'limit')) params.set('limit', flag(args, 'limit'));
|
|
890
|
+
const data = await publicRequest(`/trades/${slug}/directory?${params}`);
|
|
891
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
892
|
+
header(`${data.count} ${slug}${flag(args, 'city') ? ` in ${flag(args, 'city')}` : ''}`);
|
|
893
|
+
for (const r of (data.results || []).slice(0, 50)) {
|
|
894
|
+
const city = (r.business_address && r.business_address.city) || '';
|
|
895
|
+
console.log(` ${c('orange', r.orgnr)} ${r.name}${city ? c('dim', ` · ${city}`) : ''}`);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Diff: typed changelog
|
|
900
|
+
async function diff(args) {
|
|
901
|
+
const orgnr = args[0];
|
|
902
|
+
if (!orgnr) die('Usage: nordic-data diff <orgnr> [--since YYYY-MM-DD]');
|
|
903
|
+
const params = new URLSearchParams();
|
|
904
|
+
if (flag(args, 'since')) params.set('since', flag(args, 'since'));
|
|
905
|
+
const data = await publicRequest(`/companies/${orgnr}/diff?${params}`);
|
|
906
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
907
|
+
header(`Changes for ${orgnr} since ${data.since}`);
|
|
908
|
+
for (const ch of (data.changes || []).slice(0, 50)) {
|
|
909
|
+
console.log(` ${c('orange', ch.change_date || '')} ${ch.change_type || ''}`);
|
|
910
|
+
}
|
|
911
|
+
if (!data.changes || !data.changes.length) console.log(c('dim', ' (no changes)'));
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// US: search public companies
|
|
915
|
+
async function usSearch(query) {
|
|
916
|
+
if (!query) die('Usage: nordic-data us-search <query>');
|
|
917
|
+
const data = await publicRequest(`/companies/us/search?q=${encodeURIComponent(query)}`);
|
|
918
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
919
|
+
header(`${data.count} US public company matches for "${query}"`);
|
|
920
|
+
for (const r of (data.results || []).slice(0, 25)) {
|
|
921
|
+
console.log(` ${c('orange', r.cik)} ${r.name}${r.ticker ? c('dim', ` · ${r.ticker}`) : ''}`);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async function usLookup(query) {
|
|
926
|
+
if (!query) die('Usage: nordic-data us-lookup <query>\n Resolves a US public company by CIK, ticker, or name.');
|
|
927
|
+
const data = await publicRequest(`/companies/us/lookup?q=${encodeURIComponent(query)}`);
|
|
928
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
929
|
+
if (!data.resolved) return console.log(c('dim', data.note || 'No match.'));
|
|
930
|
+
const r = data.resolved;
|
|
931
|
+
header(`${r.name} ${c('dim', `(CIK ${r.cik}${r.tickers && r.tickers[0] ? ' · ' + r.tickers[0] : ''})`)}`);
|
|
932
|
+
pretty('SIC', `${r.sic || ''} ${r.sic_description || ''}`.trim());
|
|
933
|
+
pretty('State', r.state);
|
|
934
|
+
pretty('City', r.city);
|
|
935
|
+
pretty('Address', r.address);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
async function usFilings(args) {
|
|
939
|
+
const cik = args[0];
|
|
940
|
+
if (!cik) die('Usage: nordic-data us-filings <cik> [--type 10-K]');
|
|
941
|
+
const params = new URLSearchParams();
|
|
942
|
+
if (flag(args, 'type')) params.set('type', flag(args, 'type'));
|
|
943
|
+
const data = await publicRequest(`/companies/us/${cik}/filings?${params}`);
|
|
944
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
945
|
+
header(`${data.company} — ${data.count} filing(s)`);
|
|
946
|
+
for (const f of (data.filings || []).slice(0, 25)) {
|
|
947
|
+
console.log(` ${c('orange', f.date.padEnd(11))} ${c('bold', f.form.padEnd(8))} ${c('dim', f.url)}`);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function usKyb(cik) {
|
|
952
|
+
if (!cik) die('Usage: nordic-data us-kyb <cik>');
|
|
953
|
+
const data = await publicRequest(`/companies/us/${cik}/kyb-pack`);
|
|
954
|
+
if (useJson) return console.log(JSON.stringify(data, null, 2));
|
|
955
|
+
const i = data.identity || {};
|
|
956
|
+
header(`US KYB pack — ${i.name || cik}`);
|
|
957
|
+
pretty('CIK', i.cik);
|
|
958
|
+
pretty('SIC', `${i.sic || ''} ${i.sic_description || ''}`.trim());
|
|
959
|
+
pretty('State', i.state);
|
|
960
|
+
pretty('City', i.city);
|
|
961
|
+
if (data.recent_filings && data.recent_filings.length) {
|
|
962
|
+
console.log(c('bold', '\n Recent filings:'));
|
|
963
|
+
for (const f of data.recent_filings) console.log(` ${c('orange', f.date)} ${f.form}`);
|
|
964
|
+
}
|
|
965
|
+
const sa = data.sanctions || {};
|
|
966
|
+
pretty('Sanctions matches', (sa.company_matches || []).length);
|
|
967
|
+
if (data.recent_federal_contracts && data.recent_federal_contracts.length) {
|
|
968
|
+
pretty('Recent federal contracts', data.recent_federal_contracts.length);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
387
972
|
// ── Dispatch ───────────────────────────────────────────────
|
|
388
973
|
|
|
389
974
|
(async () => {
|
|
@@ -398,6 +983,28 @@ function fmt(n, currency) {
|
|
|
398
983
|
case 'grants': await grants(rest[0]); break;
|
|
399
984
|
case 'sanctions': await sanctions(rest[0]); break;
|
|
400
985
|
case 'shareholders': await shareholders(rest[0]); break;
|
|
986
|
+
case 'contacts-se': await contactsSe(rest[0]); break;
|
|
987
|
+
// v0.4.0 — Discovery, KYB, AI/NL, US
|
|
988
|
+
case 'find': await find(rest.join(' ')); break;
|
|
989
|
+
case 'list': await list(rest); break;
|
|
990
|
+
case 'nl': await nl(rest.join(' ')); break;
|
|
991
|
+
case 'kyb': await kyb(rest[0]); break;
|
|
992
|
+
case 'explain': await explain(rest[0]); break;
|
|
993
|
+
case 'ask': await ask(rest); break;
|
|
994
|
+
case 'pitch': await pitch(rest[0]); break;
|
|
995
|
+
case 'talking-points': await pitch(rest[0]); break;
|
|
996
|
+
case 'verify-invoice': await verifyInvoice(rest); break;
|
|
997
|
+
case 'resolve': await resolve(rest); break;
|
|
998
|
+
case 'near': await near(rest); break;
|
|
999
|
+
case 'similar': await similar(rest[0]); break;
|
|
1000
|
+
case 'decision-makers': await decisionMakers(rest[0]); break;
|
|
1001
|
+
case 'trades': await trades(rest); break;
|
|
1002
|
+
case 'diff': await diff(rest); break;
|
|
1003
|
+
case 'us-search': await usSearch(rest.join(' ')); break;
|
|
1004
|
+
case 'us-lookup': await usLookup(rest.join(' ')); break;
|
|
1005
|
+
case 'us-filings': await usFilings(rest); break;
|
|
1006
|
+
case 'us-kyb': await usKyb(rest[0]); break;
|
|
1007
|
+
// Meta
|
|
401
1008
|
case 'mcp': mcp(); break;
|
|
402
1009
|
case 'signup': signup(); break;
|
|
403
1010
|
default:
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nordic-data",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "CLI for Nordic Data \u2014
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "CLI for Nordic Data \u2014 Norwegian, Swedish, and US business intelligence in one terminal. Discovery (filter by industry \u00d7 city \u00d7 size, natural-language search, geo-radius, similar-companies, trades verticals), universal resolvers (email / domain / phone / IBAN / VAT / LEI \u2192 company), KYB / compliance (one-call due-diligence pack, typed changelog, invoice-fraud heuristic), AI / NL (explain, Q&A, sales talking-points, risk narrative), United States coverage (SEC EDGAR + USA Spending federal contracts), plus the original Nordic core: identity, accounts, officers, sanctions, public procurement, R&D grants, shareholders, AI-enriched contacts.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nordic-data": "./bin/cli.js"
|
|
7
7
|
},
|
|
@@ -16,14 +16,31 @@
|
|
|
16
16
|
"sweden",
|
|
17
17
|
"swedish",
|
|
18
18
|
"nordic",
|
|
19
|
+
"united-states",
|
|
20
|
+
"us",
|
|
21
|
+
"sec-edgar",
|
|
19
22
|
"company",
|
|
20
23
|
"data",
|
|
21
24
|
"cli",
|
|
22
25
|
"kyb",
|
|
26
|
+
"kyc",
|
|
23
27
|
"aml",
|
|
24
28
|
"sanctions",
|
|
29
|
+
"ofac",
|
|
25
30
|
"procurement",
|
|
31
|
+
"tenders",
|
|
32
|
+
"lookup",
|
|
33
|
+
"resolver",
|
|
34
|
+
"enrichment",
|
|
35
|
+
"discovery",
|
|
36
|
+
"natural-language",
|
|
26
37
|
"mcp",
|
|
38
|
+
"model-context-protocol",
|
|
39
|
+
"agent",
|
|
40
|
+
"ai",
|
|
41
|
+
"fraud",
|
|
42
|
+
"invoice-fraud",
|
|
43
|
+
"due-diligence",
|
|
27
44
|
"nordic-data"
|
|
28
45
|
],
|
|
29
46
|
"homepage": "https://nordicdata.cloud",
|