The real one: the Banco Central de la República Dominicana's reference rate for the dollar, read from its own workbook with no dependency, every business day.
DOP Economy is Kanboombo's own plug-in, running on Kanboombo's Server today. It reads the workbook in which the Banco Central de la República Dominicana publishes the dollar's reference rate, buy and sell, and puts each business day's rate on its day. It is small on purpose: one source, one series, two files of code, nothing to install.
One workbook of at most 5 MB; one value series whose change readers see on venta, with DOP read as pesos; observation dates, so a revised rate replaces its day and the old one is kept, and weekends show the rate in force; a 60-day snapshot; 09:30 and 17:30 on weekdays in Santo Domingo, which keeps no daylight time; public, so on your days once you install it.
fetch.mjs and xlsx.mjs
plugins/dop-economy/fetch.mjs
#!/usr/bin/env node
// DOP Economy's fetcher (contract 1, docs/kanboombo/PLUGIN-CONTRACT.md).
//
// Reads the run context on stdin, opens the Banco Central's reference-rate
// workbook the host downloaded (source "rate"), and prints one value record
// per business day the sheet holds: the buy (compra) and sell (venta) rate.
// It prints the whole sheet every run; the runner writes only what changed,
// so a revised past rate is caught wherever it is.
//
// Exit 0 when the sheet reads as expected, even with nothing new. Exit 2 when
// the workbook is not what this plug-in knows (a renamed sheet, a moved
// column): the run then fails with that reason and nothing is written.
import { readFileSync } from "node:fs";
import { readSheet, WorkbookError } from "./xlsx.mjs";
const SHEET = "Diaria";
const MONTHS = {
ene: 1, feb: 2, mar: 3, abr: 4, may: 5, jun: 6, jul: 7, ago: 8, sep: 9, set: 9, oct: 10, nov: 11, dic: 12,
jan: 1, apr: 4, aug: 8, dec: 12,
};
function fail(reason) {
process.stderr.write(`dop-economy: ${reason}\n`);
process.exit(2);
}
/** "Año" → "ano": header cells compared without accents or case. */
function plain(value) {
return String(value ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").trim().toLowerCase();
}
function readStdin() {
try {
return JSON.parse(readFileSync(0, "utf8"));
} catch (error) {
fail(`the run context is not JSON (${error.message})`);
}
}
const context = readStdin();
const path = context?.sources?.rate;
if (!path) fail("the run context names no source called rate");
let rows;
try {
rows = readSheet(readFileSync(path), SHEET);
} catch (error) {
fail(error instanceof WorkbookError ? `the workbook changed: ${error.message}` : `cannot read the workbook: ${error.message}`);
}
const headerIndex = rows.findIndex((r) => {
const names = r.cells.map(plain);
return ["ano", "mes", "dia", "compra", "venta"].every((h) => names.includes(h));
});
if (headerIndex < 0) fail(`sheet ${SHEET} has no header row with Año, Mes, Día, Compra and Venta`);
const header = rows[headerIndex].cells.map(plain);
const col = Object.fromEntries(["ano", "mes", "dia", "compra", "venta"].map((h) => [h, header.indexOf(h)]));
const today = typeof context.today === "string" ? context.today : null;
const seen = new Set();
let printed = 0;
for (const { cells } of rows.slice(headerIndex + 1)) {
const year = cells[col.ano];
const month = MONTHS[plain(cells[col.mes]).slice(0, 3)];
const day = cells[col.dia];
const compra = cells[col.compra];
const venta = cells[col.venta];
// Notes and blank rows under the table carry no year and day; skip them.
if (!Number.isInteger(year) || !Number.isInteger(day) || !month) continue;
if (typeof compra !== "number" || typeof venta !== "number" || !(compra > 0) || !(venta > 0)) continue;
const date = `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
if (today && date > today) continue;
if (seen.has(date)) fail(`sheet ${SHEET} lists ${date} twice`);
seen.add(date);
process.stdout.write(`${JSON.stringify({ kind: "value", series: "usd-dop", date, fields: { compra, venta } })}\n`);
printed += 1;
}
if (printed === 0) fail(`sheet ${SHEET} has a header but no rates under it`);
process.stderr.write(`dop-economy: ${printed} business days read from ${SHEET}\n`);
It finds the header by its names, without accents or case, reads Spanish and English months, never prints a date after today, and stops on a date listed twice. It prints the whole sheet every run, so a revised past rate is caught wherever it is. xlsx.mjs reads the workbook with nothing but Node, so the digest covers every line that runs. Run it by hand, as the Server does:
dop-economy: the workbook changed: no sheet named Diaria
On the Server that line is the failed run's reason: nothing is written, stored rates stay, and readers see Couldn't refresh after the last rate until a run succeeds.
The fixtures
fixtures/sources/rate.xlsx holds real rows, 3 August to 28 September 2026 and March 2008, and records.jsonl the 60 rates they give. With no context.json, check runs now: every rate is past, so the day does not matter. runs/renamed-sheet/ must fail; lab/ holds four more workbooks for Kanboombo's own lab.
The holder is the Banco Central de la República Dominicana, shown as Banco Central; terms is the Open Database License (ODbL) 1.0, under which it publishes the same rate on the Dominican open data portal; retain is yes. Its listing links the source's page and that licence, so you read both before you install it:
plugins/dop-economy/listing.json
{
"listing": 1,
"plugin": "dop-economy",
"publisher": {
"domain": "kanboombo.com"
},
"tagline": {
"en": "The Banco Central's dollar rate, buy and sell, on every day since 1991.",
"es": "La tasa del dólar del Banco Central, compra y venta, en cada día desde 1991."
},
"description": {
"en": "You see the US dollar's reference rate in Dominican pesos, what the Banco Central buys and sells it at, on every business day back to 1991. On a weekend or a holiday you see the rate in force and the day it was set.\n\nDOP Economy reads the Banco Central's own published file twice each business day, at 9:30 and 17:30 in Santo Domingo, and keeps every revision. A higher dollar is never shown as good or bad: you see the change, and you decide what it means.\n\nIt runs on Kanboombo's Server with no network of its own. It reads nothing about you and never sees who installed it.",
"es": "Ves la tasa de referencia del dólar en pesos dominicanos, a cuánto lo compra y lo vende el Banco Central, en cada día laborable desde 1991. En un fin de semana o un feriado ves la tasa vigente y el día en que se fijó.\n\nEconomía RD lee el archivo que publica el propio Banco Central dos veces cada día laborable, a las 9:30 y a las 17:30 en Santo Domingo, y guarda cada revisión. Un dólar más alto nunca se muestra como bueno o malo: ves el cambio, y tú decides qué significa.\n\nCorre en el Servidor de Kanboombo, sin red propia. No lee nada sobre ti y nunca sabe quién lo instaló."
},
"category": "economy",
"keywords": [
"dollar",
"peso",
"DOP",
"exchange rate",
"tasa de cambio",
"dólar",
"Banco Central",
"República Dominicana"
],
"links": {
"privacy": "https://kanboombo.com/privacy/",
"support": {
"url": "https://kanboombo.com/about/"
}
},
"sources": [
{
"name": "rate",
"title": "Tasa de cambio de referencia del dólar",
"page": "https://www.bancentral.gov.do/a/d/2538-mercado-cambiario",
"licence": {
"name": "Open Database License (ODbL) 1.0",
"url": "https://opendatacommons.org/licenses/odbl/1-0/"
}
}
],
"examples": [
{
"date": "2026-09-28",
"caption": {
"en": "A business day",
"es": "Un día laborable"
}
},
{
"date": "2026-09-27",
"caption": {
"en": "A Sunday: the rate in force",
"es": "Un domingo: la tasa vigente"
}
},
{
"date": "2008-03-04",
"caption": {
"en": "March 2008",
"es": "Marzo de 2008"
}
}
]
}