Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 229 additions & 55 deletions packages/backend/src/adapters/intl/snov.json

Large diffs are not rendered by default.

89 changes: 85 additions & 4 deletions packages/backend/src/adapters/intl/snov.live.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,87 @@
import * as adapter from './snov.json';
const a = adapter as unknown as { connector: { baseUrl: string; authType: string; authConfig: Record<string,string> } };
describe('snov adapter — static spec conformance', () => {
it('api.snov.io/v1', () => expect(a.connector.baseUrl).toBe('https://api.snov.io/v1'));
it('Bearer (OAuth2 client-credentials access token)', () => expect(a.connector.authType).toBe('BEARER_TOKEN'));

/**
* Static checks for the Snov.io adapter, pinned against snov.io/api
* (checked 19 Oct 2026) and the errors a live Cloud connector logged:
*
* - The access token lives one hour. The adapter used to ask for it as a
* variable, so a connector stopped working an hour after setup (401
* Unauthenticated). It now signs in with the API User ID and Secret
* (OAuth 2.0 client credentials) and renews the token itself.
* - Check user balance is GET; POST answered 403.
* - v1 get-domain-emails-with-info is gone (404); domain search is the v2
* start + result pair.
* - Creating a list is POST /v1/lists.
*/
type Tool = {
name: string;
description: string;
parameters: { properties?: Record<string, unknown>; required?: string[] };
endpointMapping: { method: string; path: string; bodyMapping?: Record<string, string>; bodyEncoding?: string };
annotations?: { readOnlyHint?: boolean };
};
const a = adapter as unknown as {
requiredEnvVars: string[];
envVarMeta: Record<string, { secret?: boolean }>;
probe: { tool: string };
connector: { baseUrl: string; authType: string; authConfig: Record<string, string> };
tools: Tool[];
};
const tool = (name: string) => {
const t = a.tools.find((x) => x.name === name);
if (!t) throw new Error(`missing tool ${name}`);
return t;
};

describe('snov adapter: static spec conformance', () => {
it('api.snov.io/v1 as the base', () => expect(a.connector.baseUrl).toBe('https://api.snov.io/v1'));

it('signs in with client credentials and renews the hourly token itself', () => {
expect(a.connector.authType).toBe('OAUTH2');
expect(a.connector.authConfig).toEqual({
grant: 'client_credentials',
clientId: '{{SNOV_CLIENT_ID}}',
clientSecret: '{{SNOV_CLIENT_SECRET}}',
tokenUrl: 'https://api.snov.io/v1/oauth/access_token',
tokenAuthMethod: 'client_secret_post',
});
expect(a.requiredEnvVars).toEqual(['SNOV_CLIENT_ID', 'SNOV_CLIENT_SECRET']);
expect(a.envVarMeta.SNOV_CLIENT_SECRET.secret).toBe(true);
expect(JSON.stringify(a)).not.toContain('SNOV_ACCESS_TOKEN');
});

it('balance is a free GET and the probe', () => {
expect(tool('snov_balance').endpointMapping).toEqual({ method: 'GET', path: '/get-balance' });
expect(a.probe.tool).toBe('snov_balance');
});

it('domain search is the v2 start + result pair', () => {
const start = tool('snov_domain_search');
expect(start.endpointMapping.method).toBe('POST');
expect(start.endpointMapping.path).toBe('https://api.snov.io/v2/domain-search/domain-emails/start');
expect(start.endpointMapping.bodyMapping).toEqual({ domain: '$domain', next: '$next' });
const result = tool('snov_get_domain_search_result');
expect(result.endpointMapping.method).toBe('GET');
expect(result.endpointMapping.path).toBe(
'https://api.snov.io/v2/domain-search/domain-emails/result/{task_hash}',
);
expect(result.parameters.required).toEqual(['task_hash']);
expect(JSON.stringify(a.tools)).not.toContain('get-domain-emails-with-info');
});

it('the free count is a read-only POST', () => {
const t = tool('snov_get_domain_emails_count');
expect(t.endpointMapping).toMatchObject({ method: 'POST', path: '/get-domain-emails-count' });
expect(t.annotations?.readOnlyHint).toBe(true);
});

it('creating a list is POST /lists', () => {
expect(tool('snov_create_prospect_list').endpointMapping).toMatchObject({ method: 'POST', path: '/lists' });
});

it('every tool that spends credits says so', () => {
for (const n of ['snov_domain_search', 'snov_email_finder', 'snov_email_verifier', 'snov_email_finder_by_url']) {
expect(tool(n).description).toMatch(/credit/i);
}
});
});
29 changes: 17 additions & 12 deletions packages/backend/src/adapters/intl/twelve-data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"slug": "twelve-data",
"name": "Twelve Data",
"description": "Market data for AI agents from Twelve Data: real-time quotes and prices, historical time series, exchange rates and currency conversion, symbol search, stock, forex and crypto lists, exchanges and market hours, and RSI, MACD, SMA, EMA and Bollinger Bands. 19 read-only tools, API key.",
"instructions": "This connector uses the Twelve Data REST API (`https://api.twelvedata.com`) for market data on stocks, ETFs, forex and cryptocurrencies. All tools only read.\n\n**Setup**:\n1. Create a free account at https://twelvedata.com and open the dashboard's **API Keys** page.\n2. Copy the key into `TWELVE_DATA_API_KEY`. It is sent in the `Authorization: apikey <key>` header, as Twelve Data recommends, so it never appears in a URL or a log.\n\n**Plans and limits**: the free Basic plan allows **8 API credits per minute and 800 per day**. Most tools cost 1 credit per symbol and call; reference lists (stocks, forex pairs, exchanges, symbol search, market state, earliest timestamp, API usage) cost 1 credit per call. Going over returns HTTP 429; wait for the next minute. Free plans cover US equities, forex and crypto; many non-US exchanges, fundamentals and some data need a paid plan and answer 403 otherwise. `twelve_data_get_api_usage` shows the usage of the last minute and of the day against the plan limits.\n\n**Where to start**: resolve a name into a symbol with `twelve_data_search_symbols` (for example Siemens -> SIE on XETR). Then `twelve_data_get_price` for a number, `twelve_data_get_quote` for change, volume and 52-week range, or `twelve_data_get_time_series` for history. For currencies use `twelve_data_get_exchange_rate` or `twelve_data_convert_currency` with a pair such as EUR/USD.\n\n**Symbols**: stocks use the ticker (AAPL); when a ticker is listed on several exchanges pass `exchange` (name or MIC). Forex and crypto pairs use a slash: EUR/USD, BTC/USD, ETH/BTC.\n\n**Time series and indicators**: `interval` is one of 1min, 5min, 15min, 30min, 45min, 1h, 2h, 4h, 8h, 1day, 1week, 1month. `outputsize` is 1 to 5000 data points (the tools default to 30). With `start_date` and `end_date` the API returns all points in that range. Values come newest first unless `order` is asc. Datetimes are in the exchange's timezone unless `timezone` is set (UTC for forex and crypto). Prices are strings in the JSON; convert before computing. Indicators here default to the close price.\n\n**Errors**: errors come back as `{\"code\": 400, \"message\": \"...\", \"status\": \"error\"}` with the same HTTP status: 400 bad parameter (the message lists valid values), 401 bad key, 403 plan does not include the data, 404 no data for the parameters, 429 credits exhausted.\n\n**Demo key**: Twelve Data documents the key `demo` for trying a few symbols (for example AAPL, EUR/USD, BTC/USD); it is not meant for real use.",
"instructions": "This connector uses the Twelve Data REST API (`https://api.twelvedata.com`) for market data on stocks, ETFs, forex and cryptocurrencies. All tools only read.\n\n**Setup**:\n1. Create a free account at https://twelvedata.com and open the dashboard's **API Keys** page.\n2. Copy the key into `TWELVE_DATA_API_KEY`. It is sent in the `Authorization: apikey <key>` header, as Twelve Data recommends, so it never appears in a URL or a log.\n\n**Plans and limits**: the free Basic plan allows **8 API credits per minute and 800 per day**. Most tools cost 1 credit per symbol and call; reference lists (stocks, forex pairs, exchanges, symbol search, market state, earliest timestamp, API usage) cost 1 credit per call. Going over returns HTTP 429; wait for the next minute. Free plans cover US equities, forex and crypto; many non-US exchanges, fundamentals and some data need a paid plan and answer 403 otherwise. `twelve_data_get_api_usage` shows the usage of the last minute and of the day against the plan limits.\n\n**Where to start**: resolve a name into a symbol with `twelve_data_search_symbols` (for example Siemens -> SIE on XETR). Then `twelve_data_get_price` for a number, `twelve_data_get_quote` for change, volume and 52-week range, or `twelve_data_get_time_series` for history. For currencies use `twelve_data_get_exchange_rate` or `twelve_data_convert_currency` with a pair such as EUR/USD.\n\n**Symbols**: stocks use the ticker (AAPL); when a ticker is listed on several exchanges pass `exchange` (name or MIC). Forex and crypto pairs use a slash: EUR/USD, BTC/USD, ETH/BTC.\n\n**Time series and indicators**: `interval` is one of 1min, 5min, 15min, 30min, 45min, 1h, 2h, 4h, 8h, 1day, 1week, 1month. `outputsize` is 1 to 5000 data points (the tools default to 30). With `start_date` and `end_date` the API returns all points in that range. Values come newest first unless `order` is asc. Datetimes are in the exchange's timezone unless `timezone` is set (UTC for forex and crypto). Prices are strings in the JSON; convert before computing. Indicators here default to the close price.\n\n**Errors**: errors come back as `{\"code\": 400, \"message\": \"...\", \"status\": \"error\"}` with the same HTTP status: 400 bad parameter (the message lists valid values), 401 bad key, 403 plan does not include the data, 404 no data for the parameters or a symbol outside the plan (\"This symbol is available starting with the Grow plan\"), 429 credits exhausted.\n\n**Indices and CFDs**: index symbols such as NDX (Nasdaq-100) or SPX need a paid plan, and broker CFD names such as NAS100, US100 or US30 are not Twelve Data symbols at all. On the Basic plan use an ETF that tracks the index instead (QQQ for the Nasdaq-100, SPY for the S&P 500, DIA for the Dow Jones) and say so in the answer. When unsure, call `twelve_data_search_symbols` with `show_plan` true, which tells for each match which plan it needs.\n\n**Demo key**: Twelve Data documents the key `demo` for trying a few symbols (for example AAPL, EUR/USD, BTC/USD); it is not meant for real use.",
"region": "intl",
"category": "finance",
"icon": "twelve-data",
Expand Down Expand Up @@ -62,7 +62,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"exchange": {
"type": "string",
Expand Down Expand Up @@ -126,7 +126,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"exchange": {
"type": "string",
Expand Down Expand Up @@ -161,7 +161,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -260,7 +260,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"exchange": {
"type": "string",
Expand Down Expand Up @@ -395,6 +395,10 @@
"maximum": 120,
"default": 10,
"description": "Number of data points, 1 to 120. Default 10 here; keep it small to save context."
},
"show_plan": {
"type": "boolean",
"description": "true adds, for every match, the plan it needs (access.plan), so you can pick a symbol the account can read."
}
},
"required": [
Expand All @@ -406,7 +410,8 @@
"path": "/symbol_search",
"queryParams": {
"symbol": "$symbol",
"outputsize": "$outputsize"
"outputsize": "$outputsize",
"show_plan": "$show_plan"
}
}
},
Expand Down Expand Up @@ -578,7 +583,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -662,7 +667,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -739,7 +744,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -829,7 +834,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -906,7 +911,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down Expand Up @@ -983,7 +988,7 @@
"properties": {
"symbol": {
"type": "string",
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure."
"description": "Ticker, for example AAPL, EUR/USD or BTC/USD. Use twelve_data_search_symbols when unsure. Indices such as NDX need a paid plan and CFD names such as NAS100 or US100 do not exist here; on the Basic plan use an ETF such as QQQ."
},
"interval": {
"type": "string",
Expand Down
11 changes: 11 additions & 0 deletions packages/backend/src/adapters/intl/twelve-data.live.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ describe('twelve-data adapter: static spec conformance', () => {
}
});

it('steers index and CFD names to a symbol the plan can read', () => {
// Cloud log, 17 Oct 2026: NDX answered "available starting with the Pro
// plan" (404), US100 "Grow plan", NAS100 "symbol missing or invalid".
const a2 = a as unknown as { instructions: string };
expect(a2.instructions).toMatch(/NAS100/);
expect(a2.instructions).toMatch(/QQQ/);
const symbol = tool('twelve_data_get_time_series').parameters.properties!.symbol as { description: string };
expect(symbol.description).toMatch(/QQQ/);
expect(tool('twelve_data_search_symbols').endpointMapping.queryParams).toMatchObject({ show_plan: '$show_plan' });
});

it('time series and indicators require symbol and interval', () => {
for (const name of ['twelve_data_get_time_series', 'twelve_data_get_rsi', 'twelve_data_get_macd', 'twelve_data_get_bbands']) {
expect(tool(name).parameters.required).toEqual(['symbol', 'interval']);
Expand Down
Loading
Loading