
Exchange Rate
- 93 installs
- 20 repo stars
- Updated August 4, 2026
- qverisai/open-qveris-skills
Query real-time exchange rates for multi-currency transactions and reporting
About
A Qveris open skills tool providing real-time currency exchange rate queries for multi-currency financial operations. Solo builders use it to handle international payments, pricing conversions, and financial reporting in products with global audiences.
- Real-time exchange rates
- Multi-currency support
- Financial integration
- 89 active installs
Exchange Rate by the numbers
- 93 all-time installs (skills.sh)
- Ranked #526 of 1,106 Finance & Trading skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/qverisai/open-qveris-skills --skill exchange-rateAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 93 |
|---|---|
| repo stars | ★ 20 |
| Last updated | August 4, 2026 |
| Repository | qverisai/open-qveris-skills ↗ |
What it does
Query real-time exchange rates for multi-currency transactions and reporting
Files
Exchange Rate
Real-time currency exchange rate and conversion using QVeris tools.
What This Skill Does
Exchange Rate provides:
1. Rate lookup – Current exchange rate between two currencies (e.g. USD/EUR, CNY/JPY). 2. Amount conversion – Convert an amount from one currency to another at current rates.
Supported via QVeris: forex and common fiat pairs; optional historical date for rate/conversion when the tool supports it.
Key Advantages
- Uses only QVeris API: search for tools by capability, then execute; no hardcoded provider list.
- Fallback across providers (e.g. Alpha Vantage, Twelve Data) when one fails or is unavailable.
- Same credential as other skills:
QVERIS_API_KEYonly. - Read-only, no side effects; suitable for travel, trade, and reporting.
Core Workflow
1. Parse user intent: rate (from_currency, to_currency) or convert (from_currency, to_currency, amount). 2. Search QVeris for tools: e.g. "currency exchange rate real-time", "currency conversion". 3. Rank results by success_rate, latency, and parameter fit (rate vs conversion). 4. Build request parameters: for rate use from_currency/to_currency or symbol (e.g. EUR/USD); for conversion add amount. 5. Execute chosen tool with 5s timeout; on failure try next candidate. 6. Return formatted rate and/or converted amount (markdown or JSON).
Command Surface
Primary script: scripts/exchange_rate.mjs
- Get rate only:
node scripts/exchange_rate.mjs rate --from USD --to EURnode scripts/exchange_rate.mjs rate --from CNY --to USD- Convert amount:
node scripts/exchange_rate.mjs convert --from USD --to JPY --amount 1000node scripts/exchange_rate.mjs convert --from EUR --to GBP --amount 500
Optional: --date YYYY-MM-DD for historical rate/conversion when the tool supports it; --format json for machine-readable output.
Safety and Disclosure
- Uses only
QVERIS_API_KEY; no other secrets. - Calls only QVeris over HTTPS; no package install or arbitrary commands.
- Output is for reference only; not financial or contractual advice.
.env
.env.local
Exchange Rate
Real-time currency exchange rate and conversion skill for OpenClaw/ClawHub agents, powered by QVeris.
Highlights
- Rate lookup: Current exchange rate for any supported currency pair (e.g. USD/EUR, CNY/JPY).
- Amount conversion: Convert an amount from one currency to another at current rates.
- QVeris-only: Discovers and calls tools via QVeris search + execute (Alpha Vantage, Twelve Data, etc.) with fallback.
- Optional historical: Use
--date YYYY-MM-DDwhen the underlying tool supports it. - Output: Human-readable markdown or JSON.
Requirements
- Node.js 18+
QVERIS_API_KEY
export QVERIS_API_KEY="your-api-key"Install as an Independent Skill
npx skills add <repo-url> --skill exchange-rateOr copy the exchange-rate folder into your agent skill directory.
Usage
Get exchange rate (no amount)
node scripts/exchange_rate.mjs rate --from USD --to EUR
node scripts/exchange_rate.mjs rate --from CNY --to USDConvert amount
node scripts/exchange_rate.mjs convert --from USD --to JPY --amount 1000
node scripts/exchange_rate.mjs convert --from EUR --to GBP --amount 500Optional options
--date YYYY-MM-DD– Use rate from this date (if tool supports it).--format json– Output machine-readable JSON.--timeout N– Request timeout in seconds (default 5).
Prompt Examples
- "What is the USD to EUR exchange rate?"
- "Convert 1000 USD to JPY"
- "CNY to USD rate"
- "100 EUR to GBP"
Notes
- Currency codes are normalized to uppercase (USD, EUR, CNY, GBP, JPY, etc.).
- If one provider fails, the script tries the next candidate from the search results.
- Data is for reference only; not financial or contractual advice.
Security
- Uses only
QVERIS_API_KEY; never hardcode in committed files. - All requests go to
qveris.aiover HTTPS; no extra packages or arbitrary commands.
#!/usr/bin/env node
/**
* Exchange Rate Skill – QVeris-powered rate lookup and conversion.
* Search for currency tools, then execute with fallback. No hardcoded tool IDs.
*/
const BASE_URL = "https://qveris.ai/api/v1";
const SEARCH_QUERIES = [
"currency exchange rate real-time forex",
"currency conversion amount convert",
];
const DEFAULT_TIMEOUT_MS = 5000;
const MAX_RESPONSE_SIZE = 20480;
function getApiKey() {
const key = process.env.QVERIS_API_KEY;
if (!key) {
console.error("Error: QVERIS_API_KEY environment variable is required.");
process.exit(1);
}
return key;
}
function timeoutSignal(ms) {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
return { signal: controller.signal, cleanup: () => clearTimeout(timer) };
}
async function searchTools(query, limit = 15, timeoutMs = 15000) {
const apiKey = getApiKey();
const { signal, cleanup } = timeoutSignal(timeoutMs);
try {
const res = await fetch(`${BASE_URL}/search`, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, limit }),
signal,
});
if (!res.ok) throw new Error(`Search failed (${res.status}): ${await res.text()}`);
return await res.json();
} finally {
cleanup();
}
}
async function executeTool(toolId, searchId, parameters, maxResponseSize, timeoutMs) {
const apiKey = getApiKey();
const { signal, cleanup } = timeoutSignal(timeoutMs);
try {
const url = new URL(`${BASE_URL}/tools/execute`);
url.searchParams.set("tool_id", toolId);
const res = await fetch(url.toString(), {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
search_id: searchId,
parameters,
max_response_size: maxResponseSize,
}),
signal,
});
if (!res.ok) throw new Error(`Execute failed (${res.status}): ${await res.text()}`);
return await res.json();
} finally {
cleanup();
}
}
function paramNames(tool) {
const params = tool?.params ?? [];
return new Set(params.map((p) => (p && p.name ? String(p.name).toLowerCase() : "").trim()).filter(Boolean));
}
function isRateTool(tool) {
const names = paramNames(tool);
const hasPair =
(names.has("from_currency") && names.has("to_currency")) ||
names.has("symbol") ||
(names.has("from_symbol") && names.has("to_symbol"));
return hasPair;
}
function isConversionTool(tool) {
const names = paramNames(tool);
return names.has("amount") && (names.has("symbol") || names.has("from_currency") || names.has("from_symbol"));
}
function scoreTool(tool, needAmount) {
const stats = tool?.stats ?? {};
const successRate = Number(stats.success_rate);
const latency = Number(stats.avg_execution_time_ms);
const latencyScore = latency > 0 ? 1 / (1 + latency) : 0;
let fit = 0;
if (needAmount && isConversionTool(tool)) fit += 0.2;
else if (!needAmount && isRateTool(tool)) fit += 0.2;
if (isRateTool(tool)) fit += 0.1;
return (successRate || 0) * 0.6 + latencyScore * 0.2 + fit;
}
function buildParameters(tool, from, to, amount, date) {
const fromU = String(from || "").toUpperCase().trim();
const toU = String(to || "").toUpperCase().trim();
const params = {};
const names = paramNames(tool);
if (names.has("function")) {
params.function = "CURRENCY_EXCHANGE_RATE";
}
if (names.has("from_currency")) params.from_currency = fromU;
if (names.has("to_currency")) params.to_currency = toU;
if (names.has("from_symbol")) params.from_symbol = fromU;
if (names.has("to_symbol")) params.to_symbol = toU;
if (names.has("symbol")) params.symbol = `${fromU}/${toU}`;
if (names.has("amount") && amount != null && amount !== "") {
const n = Number(amount);
params.amount = Number.isFinite(n) ? n : 1;
}
if (names.has("date") && date) params.date = date;
return params;
}
function isExecutionSuccess(result) {
if (!result || typeof result !== "object" || result.success === false) return false;
const data = result?.result?.data ?? result?.result ?? result?.data ?? result;
if (data?.status === "error") return false;
if (typeof data?.["Error Message"] === "string") return false;
if (typeof data?.Information === "string" && data.Information.toLowerCase().includes("burst")) return false;
return true;
}
function extractRateAndConverted(data, from, to, amount) {
const d0 = data?.result?.data ?? data?.data ?? data ?? {};
let rate = null;
let converted = null;
let pair = null;
const d = d0['Realtime Currency Exchange Rate']??{};
if (typeof d["5. Exchange Rate"] === "string") {
rate = parseFloat(d["5. Exchange Rate"]);
pair = `${d["2. From_Currency Code"]}/${d["4. To_Currency Code"]}`;
}
if (typeof d["8. Bid Price"] === "string") rate = rate ?? parseFloat(d["8. Bid Price"]);
if (typeof d["9. Ask Price"] === "string" && rate == null) rate = parseFloat(d["9. Ask Price"]);
if (d.rate != null) rate = rate ?? Number(d.rate);
if (d.exchange_rate != null) rate = rate ?? Number(d.exchange_rate);
if (d.converted_amount != null) converted = Number(d.converted_amount);
if (d.amount != null && d.currency != null && String(d.currency).toUpperCase() === String(to).toUpperCase()) {
converted = Number(d.amount);
}
if (rate != null && amount != null && Number.isFinite(amount) && converted == null) {
converted = rate * amount;
}
if (d.symbol && rate == null && d.rate != null) rate = Number(d.rate);
if (d.rate != null) rate = rate ?? Number(d.rate);
return { rate, converted, pair: pair || `${from}/${to}` };
}
function parseArgs(argv) {
const args = argv.slice(2);
const parsed = {
command: null,
from: null,
to: null,
amount: null,
date: null,
format: "markdown",
timeoutMs: DEFAULT_TIMEOUT_MS,
};
for (let i = 0; i < args.length; i++) {
if (args[i] === "rate" || args[i] === "convert") parsed.command = args[i];
else if (args[i] === "--from" && i + 1 < args.length) parsed.from = args[++i];
else if (args[i] === "--to" && i + 1 < args.length) parsed.to = args[++i];
else if (args[i] === "--amount" && i + 1 < args.length) parsed.amount = args[++i];
else if (args[i] === "--date" && i + 1 < args.length) parsed.date = args[++i];
else if (args[i] === "--format" && i + 1 < args.length) parsed.format = args[++i];
else if (args[i] === "--timeout" && i + 1 < args.length) parsed.timeoutMs = (Number(args[++i]) || 5) * 1000;
else if (args[i] === "--help" || args[i] === "-h") parsed.help = true;
}
return parsed;
}
function formatMarkdown(output) {
const { from, to, rate, converted, amount, pair, tool_id, error } = output;
if (error) return `## Exchange Rate\n\nError: ${error}\n`;
let md = `## Exchange Rate\n\n**Pair:** ${pair || `${from}/${to}`}\n`;
if (rate != null) md += `**Rate:** ${rate}\n`;
if (amount != null && converted != null) md += `**${amount} ${from}** = **${converted} ${to}**\n`;
if (tool_id) md += `\n*(via ${tool_id})*\n`;
return md;
}
async function runRate(args) {
const { from, to, date, format, timeoutMs } = args;
if (!from || !to) throw new Error("rate requires --from and --to");
let searchId = null;
let candidates = [];
for (const query of SEARCH_QUERIES) {
const searchResult = await searchTools(query, 15, timeoutMs + 2000);
searchId = searchId || searchResult.search_id;
const results = searchResult.results ?? [];
for (const tool of results) {
if (!tool?.tool_id || !isRateTool(tool)) continue;
if (candidates.some((c) => c.tool_id === tool.tool_id)) continue;
candidates.push({ ...tool, search_id: searchResult.search_id });
}
}
if (candidates.length === 0) {
return { from, to, rate: null, converted: null, amount: null, pair: `${from}/${to}`, error: "No rate tools found." };
}
candidates.sort((a, b) => scoreTool(b, false) - scoreTool(a, false));
const amount = null;
for (const tool of candidates) {
const params = buildParameters(tool, from, to, amount, date);
if (Object.keys(params).length < 2) continue;
try {
const result = await executeTool(
tool.tool_id,
tool.search_id,
params,
MAX_RESPONSE_SIZE,
timeoutMs,
);
if (!isExecutionSuccess(result)) continue;
const data = result?.result ?? result;
const extracted = extractRateAndConverted(data, from, to, amount);
return {
from,
to,
rate: extracted.rate,
converted: extracted.converted,
amount: null,
pair: extracted.pair,
tool_id: tool.tool_id,
};
} catch (_) {
continue;
}
}
return {
from,
to,
rate: null,
converted: null,
amount: null,
pair: `${from}/${to}`,
error: "All rate lookups failed.",
};
}
async function runConvert(args) {
const { from, to, amount, date, format, timeoutMs } = args;
if (!from || !to) throw new Error("convert requires --from and --to");
const amountNum = amount != null && amount !== "" ? Number(amount) : null;
if (amountNum == null || !Number.isFinite(amountNum)) throw new Error("convert requires --amount");
let searchId = null;
let candidates = [];
for (const query of SEARCH_QUERIES) {
const searchResult = await searchTools(query, 15, timeoutMs + 2000);
searchId = searchId || searchResult.search_id;
const results = searchResult.results ?? [];
for (const tool of results) {
if (!tool?.tool_id) continue;
if (!isConversionTool(tool) && !isRateTool(tool)) continue;
if (candidates.some((c) => c.tool_id === tool.tool_id)) continue;
candidates.push({ ...tool, search_id: searchResult.search_id });
}
}
if (candidates.length === 0) {
return {
from,
to,
rate: null,
converted: null,
amount: amountNum,
pair: `${from}/${to}`,
error: "No conversion tools found.",
};
}
candidates.sort((a, b) => scoreTool(b, true) - scoreTool(a, true));
for (const tool of candidates) {
const params = buildParameters(tool, from, to, amountNum, date);
if (Object.keys(params).length < 2) continue;
try {
const result = await executeTool(
tool.tool_id,
tool.search_id,
params,
MAX_RESPONSE_SIZE,
timeoutMs,
);
if (!isExecutionSuccess(result)) continue;
const data = result?.result ?? result;
const extracted = extractRateAndConverted(data, from, to, amountNum);
return {
from,
to,
rate: extracted.rate,
converted: extracted.converted,
amount: amountNum,
pair: extracted.pair,
tool_id: tool.tool_id,
};
} catch (_) {
continue;
}
}
return {
from,
to,
rate: null,
converted: null,
amount: amountNum,
pair: `${from}/${to}`,
error: "All conversions failed.",
};
}
function printHelp() {
console.log(`Exchange Rate (QVeris)
Usage:
node scripts/exchange_rate.mjs rate --from USD --to EUR [options]
node scripts/exchange_rate.mjs convert --from USD --to JPY --amount 1000 [options]
Options:
--from CODE Base currency code (e.g. USD, EUR, CNY)
--to CODE Quote currency code
--amount N Amount to convert (required for convert)
--date YYYY-MM-DD Optional date for historical rate
--format markdown|json Default markdown
--timeout N Timeout in seconds (default 5)
--help Show this message
`);
}
async function main() {
const args = parseArgs(process.argv);
if (args.help || !args.command) {
printHelp();
return;
}
let output;
if (args.command === "rate") output = await runRate(args);
else if (args.command === "convert") output = await runConvert(args);
else throw new Error(`Unknown command: ${args.command}`);
if (args.format === "json") {
console.log(JSON.stringify(output, null, 2));
} else {
console.log(formatMarkdown(output));
}
}
main().catch((err) => {
if (err.name === "AbortError") console.error("Error: request timeout");
else console.error(`Error: ${err.message}`);
process.exit(1);
});