SDK reference
@budvo/sdk is a tiny library with no dependencies for talking to Budvo from an app@1 add-on. It handles the bridge's request ids, timeouts and errors, and gives you typed methods for everything Budvo offers. It speaks bridge protocol 1.x, so put "sdk": "^1.0.0" in your manifest's compat.
Install
Add-ons can't load outside scripts, so the SDK has to be a file in your folder.
One file. Download budvo-sdk.js, put it next to your page and load it before your own script. It sets window.Budvo.
<script src="budvo-sdk.js"></script><script src="app.js"></script>With npm. The package has the same file, an ES module build with TypeScript types, the manifest schema and the budvo-addon command line tool.
npm install --save-dev https://budvo.nulldaily.com/sdk/budvo-sdk-1.0.0.tgzcp node_modules/@budvo/sdk/budvo-sdk.js my-addon/app/If you bundle your add-on, import it instead and let your bundler include it:
import { Budvo } from "@budvo/sdk";Connect
const pc = await Budvo.connect();connect() says hello with context.get and resolves a client once Budvo answers. Outside Budvo (opened directly in a browser) it fails with a no_host error, so you can show a friendly message instead.
let pc;try { pc = await Budvo.connect();} catch (err) { document.body.textContent = "Open this add-on from Budvo."; throw err;}| Option | Default | |
|---|---|---|
timeout | 10000 | Milliseconds to wait for each answer. net.fetch waits at least 30 seconds. |
parent | window.parent | Where requests go. Tests and custom hosts pass their own. |
self | window | Where answers arrive. |
Context
pc.context holds what Budvo said at connect time. Theme events keep appearance and tokens current; await pc.refresh() asks again.
{ "addon": { "id": "pixel-pets", "version": "1.2.0" }, "sdk": "1.0.0", "budvo": "0.4.0", "appearance": "dark", "tokens": { "bg": "#0a0b0d", "accent": "oklch(0.8 0.11 182)" }, "currency": "USD", "locale": "en-US", "debt_mode": true, "privacy": false}| Field | |
|---|---|
addon | Your add-on's id and the version that's running |
sdk, budvo | The bridge protocol and Budvo server versions |
appearance | light or dark |
tokens | Budvo's current design tokens, including the owner's theme. See Runtimes. |
currency, locale | For formatting money: USD, en-US |
debt_mode | The household is focused on paying off debt |
privacy | Privacy mode is on: amounts are hidden, and data methods may answer not_allowed |
Methods
Every method returns a promise. Money is always an integer number of cents in the household's currency; dates are YYYY-MM-DD. Nothing returns account numbers or transactions.
| Method | Bridge method | Permission |
|---|---|---|
pc.ui.toast | ui.toast | none |
pc.ui.resize, pc.ui.autoResize | ui.resize | none |
pc.summary | summary.read | summary:read |
pc.debts | debts.read | debts:read |
pc.goals | goals.read | goals:read |
pc.jar.read | jar.read | jar:read |
pc.jar.pledge | jar.pledge | jar:pledge |
pc.storage.get, pc.storage.set | storage.get, storage.set | storage |
pc.net.fetch, pc.net.json | net.fetch | net |
pc.league.* | league.create, league.join, league.list, league.publish, league.read, league.invite, league.leave, league.delete | league:share |
pc.call(method, params) | any | whatever that method needs |
ui.toast
await pc.ui.toast("Biscuit loved that.");Shows a short message in Budvo. Params: { message }. Result: {}.
ui.resize
await pc.ui.resize(420); // the frame's height in CSS pixelsconst stop = pc.ui.autoResize(); // follow the page's height from now onAsks Budvo to size the frame, for the iOS app and inline cards. Params: { height }. Result: {}. autoResize(element) watches element (the whole page by default) with a ResizeObserver and returns a function that stops it.
summary
const s = await pc.summary();{ "safe_to_spend_cents": 42350, "debt_total_cents": 1284700, "debt_free_on": "2027-11-01", "next_payday": "2026-10-02" }debt_free_on and next_payday can be null. In privacy mode it answers not_allowed unless the owner allowed your add-on.
debts
const debts = await pc.debts();[{ "id": "debt_1", "name": "Visa", "kind": "credit_card", "balance_cents": 590962, "apr_bps": 2499, "payoff_on": "2027-01-28" }]apr_bps is the APR in basis points: 2499 is 24.99%. Privacy mode applies. The bridge answers { "debts": [...] }; the SDK returns the list.
goals
const goals = await pc.goals();[{ "id": "goal_1", "name": "Emergency fund", "target_cents": 300000, "progress_cents": 112500, "target_on": "2027-04-17" }]Privacy mode applies. The bridge answers { "goals": [...] }; the SDK returns the list.
jar.read
const jar = await pc.jar.read();{ "balance_cents": 1250, "target": { "id": "debt_1", "name": "Visa" }, "smash_day": "2026-10-04", "smashed_total_cents": 8300 }The jar's balance, where it goes when it's smashed, and how much has been smashed so far.
jar.pledge
const r = await pc.jar.pledge(150, "Fresh fish for Biscuit");if (r.pledged_cents === 0) showMessage("The jar is full for today. See you tomorrow!");else if (r.capped) showMessage("That reached today's limit.");{ "pledged_cents": 150, "capped": false, "left_today_cents": 350 }Adds to the jar. Never moves money. Params: { amount_cents, note }, where amount_cents is a whole number above 0 and note is a short line the owner sees, like what the pledge was for.
- The daily limit is the smaller of your manifest's
pledge.daily_max_centsand the owner's setting. - Above the limit, the rest of the pledge is dropped and
cappedistrue.pledged_centsis what actually went in, possibly0. - When
cappedis true, don't retry. Tell the owner kindly; the limit is doing its job. left_today_centsis what your add-on may still pledge today.
storage
const pet = await pc.storage.get("pet", { name: "Biscuit", mood: 3 }); // fallback when nothing is savedawait pc.storage.set("pet", { ...pet, mood: 4 });await pc.storage.set("pet", null); // clears itAny JSON value, per add-on and household: everyone in the household sees the same data. Up to 64 KB for your whole add-on; the SDK refuses a single value over that with too_large before sending it. Bridge params: { key } and { key, value }; results { value } (null when nothing is saved) and {}.
net
const r = await pc.net.fetch("https://api.example.com/v1/things?limit=5");// { status: 200, body: "..." }const data = await pc.net.json("https://api.example.com/v1/things?limit=5");A GET through Budvo to a host in your manifest's network list. The response must be JSON or text, up to 1 MB. net.fetch resolves with { status, body } for any status; net.json parses the body and fails with network_error unless the status is 200 to 299.
league
const { league, invite } = await pc.league.create({ name: "Sunday crew", alias: "Ron" }); // invite.code: "ABCD-EFGH"await pc.league.join("ABCD-EFGH", "Sam"); // on a friend's Budvoconst leagues = await pc.league.list();// At pick time: commit (keep side and salt in storage until kickoff).const { commit, salt } = Budvo.commitPick("nfl:401547417", "home");await pc.league.publish(league.id, [{ ref: "nfl:401547417", season: "2026", locked_at: kickoff, commit }]);// After kickoff: reveal, then score once it settles.await pc.league.publish(league.id, [{ ref: "nfl:401547417", side: "home", salt, points: 15, result: "win" }]);const { leaderboard, members, entries } = await pc.league.read(league.id);Leagues with friends on other Budvo servers. Budvo keeps them on the Budvo website and sends only the display name a person chose, a random member id per league and entries with exactly these fields: ref, commit or side and salt, points, season, locked_at (unix seconds) and result (win, loss, push, void). Anything else is refused with invalid, so amounts, stakes and the jar can't be sent by mistake.
- A pick is committed as a hash before
locked_atand revealed after it; a reveal that doesn't match, a commit after the lock or a reveal before it are refused per entry (results[i].error). Others see your side only after the lock. - It works only when the server is linked to a Budvo account (
not_linked) and the owner switched on "Share leagues" for your add-on (leagues_off). The firstcreate,joinorpublishmakes Budvo ask the person once; if they say no, the call fails withdeclined. - Invite codes are single use unless
reusable: true, and expire after 7 days (expires_in_days, 1 to 30). Display names are 1 to 24 characters and unique in a league. - Rows expire 90 days after a season's last lock.
pc.league.leave(id)removes you; the owner canpc.league.delete(id).
Events
const stop = pc.on("theme", ({ appearance, tokens }) => { console.log("now", appearance);});stop(); // stop listeningBudvo pushes theme when the owner switches light and dark mode or picks another theme. pc.context is updated before your handler runs. A handler that throws doesn't stop the others.
Theme helpers
pc.syncTheme(); // apply Budvo's look now and whenever it changessyncTheme() sets every token as a CSS variable on the root element (--bg, --surface, --accent and so on, the same names as the token table), sets color-scheme, a data-appearance attribute and a dark class in dark mode, and follows theme events. It returns a function that stops following.
For more control, Budvo.applyTheme(tokens, options) does one update:
| Option | Default | |
|---|---|---|
appearance | light | light or dark |
root | document.documentElement | Where the variables go |
prefix | -- | --pc- gives --pc-accent, if your own CSS already uses --accent |
defaults | true | Fill tokens the host didn't send with Budvo's defaults |
Values that aren't plain colors are ignored. Budvo.DEFAULT_TOKENS has Budvo's own light and dark tokens, handy for previews outside Budvo.
Money
Budvo.formatMoney(1250); // "$12.50"Budvo.formatMoney(500, { compact: true }); // "$5"Budvo.formatMoney(100, { sign: true }); // "+$1.00"Budvo.formatMoney(123456, { currency: "EUR", locale: "de-DE" }); // "1.234,56 €"pc.formatMoney(150); // in the household's currency and localeErrors
Every failure rejects with a BudvoError. Check code; message is written for people.
try { await pc.debts();} catch (err) { if (Budvo.isBudvoError(err, "not_allowed")) hideDebtCard(); else throw err;}| Code | From | When |
|---|---|---|
not_allowed | Budvo | The add-on lacks the permission, or privacy mode hides the data |
unknown_method | Budvo | The server doesn't know the method, for example an older server |
bad_request | Budvo or SDK | Missing or invalid params, like a pledge of 0 |
too_large | Budvo or SDK | Storage over 64 KB, or a response over 1 MB |
network_error | Budvo or SDK | net.fetch couldn't reach the host, or net.json got a bad status |
not_linked, leagues_off | Budvo | League methods: the server isn't linked, or the owner hasn't switched on league sharing |
declined | Budvo | The person said no to Budvo's question (a pledge, or sharing with a league) |
timeout | SDK | No answer in time |
no_host | SDK | Not running inside Budvo, or the connection was closed |
bad_response | SDK | An answer that doesn't make sense, like net.json on a body that isn't JSON |
err.method names the bridge method that failed.
The protocol
You don't need this with the SDK, but here's what goes over the wire. Messages are plain objects posted with window.parent.postMessage, and Budvo answers the frame the same way.
request: { "budvo": 1, "id": "<unique>", "method": "jar.pledge", "params": { ... } }response: { "budvo": 1, "id": "<same>", "result": { ... } } or { "budvo": 1, "id": "<same>", "error": { "code": "not_allowed", "message": "..." } }event: { "budvo": 1, "event": "theme", "data": { ... } }The SDK ignores messages that don't come from the parent window, matches answers to requests by id, and times out requests that get no answer.
Command line
The npm package includes budvo-addon:
npx budvo-addon init my-addon # a working app@1 add-on to start fromnpx budvo-addon dev my-addon # run it at http://localhost:5190 in a stand-in for Budvonpx budvo-addon validate my-addon # check it the way Budvo and the marketplace willdev serves your add-on the way Budvo does, in a sandboxed frame with the same content security policy, next to a panel of made-up household data: a jar with a daily limit you can lower, a new-day button, light and dark mode, privacy mode, debt mode, an offline switch for net.fetch, and a log of every bridge message. net.fetch really fetches, but only from the hosts in your manifest. Storage lives in your browser.
TypeScript
The npm package ships types for everything: Context, Summary, Debt, Goal, Jar, PledgeResult, FetchResult, ThemeTokens, ErrorCode and the rest.
import { Budvo, type PledgeResult } from "@budvo/sdk";const pc = await Budvo.connect();const r: PledgeResult = await pc.jar.pledge(100, "A claw machine play");