Security details

The specifics, for anyone checking.

The privacy page says what Berm does in plain language. This page says it in the terms you would need to verify it — algorithms, parameters, headers, and what each database actually holds.

Encryption

Content encryption
AES-256-GCM, via the Web Crypto API. A 96-bit random IV per record.
Master key
32 random bytes from the platform CSPRNG. It is generated, never derived, and never leaves the devices of the people who can open the budget — except sealed inside an invite link, as below.
Key derivation
PBKDF2-HMAC-SHA-256, 600,000 iterations, 128-bit random salt. The iteration count is recorded in each record and read back from it, so the cost can be raised without stranding anything already written.
Key management
A keyring. The master key is wrapped once per unlock method — a password, a passkey, a recovery code — so adding a device, dropping one or changing a password rewraps a key rather than re-encrypting the data. The keyring holds no secrets and is stored in the clear beside the ciphertext; each entry is only openable by proving something.
Recovery codes
160 bits, rendered in Crockford base32.
Algorithm agility
Every record names the KDF, the hash and the cipher it was written with, and a reader refuses one it does not recognise rather than feeding it to the wrong primitive. A record from a newer version is refused by name, so it cannot be reported as a wrong password.

Metadata

Ciphertext cannot hide its own length, and a record's size tracks how much history somebody has. So a record is compressed, then padded to the next power of two from 4 KB, then encrypted — in that order, and the order is the point: padding first and compressing afterwards would squeeze the padding back out and leave the true size showing. What is on disk is one of a dozen values rather than a number that follows a person around, and the padding is inside the encryption, so it cannot be stripped back off by whatever is holding the record.

The timestamp written into an exported vault file is coarsened to the minute. It is the one field in that file readable without the password.


What the servers hold

Two Workers, two databases, in one Cloudflare account. They share nothing: the site Worker has no binding to the app's database, which is asserted by the site's build and by the test suite from both directions.

The app, when cloud save is switched on

Cloud save comes on with an account and does not exist without one. When it is on, one row per account per app:

  • envelope — the encrypted payload, verbatim, as written to disk.
  • updated_at — an edit time sent alongside, because the server cannot read one out of ciphertext. Used for ordering only.
  • saved_at — when the server accepted the write.
  • bytes, version — size, and a counter a client must state to avoid two devices clobbering each other.

Alongside it: an account id that is random rather than a provider's identifier, one row per sign-in method holding that provider's subject, and an email address only where a provider volunteered one — used for display, never to decide who someone is — or where the address is the sign-in itself, below.

A total compromise of that database yields ciphertext and the times it was written — and, for anyone who switched notifications on, the addresses of their devices, the times notifications are due, and who asked to hear what whose imports find and whose good news.

Signing in with an email address

One password does two jobs, and the server is told neither the password nor the key it opens. On the device, the password becomes a sign-in key: PBKDF2-HMAC-SHA-256, 600,000 iterations, salted with the SHA-256 of a fixed label and the address, so a new device can make it again from the address and the password alone. The budget's key is wrapped under the same password with a random salt, so neither can be turned into the other. The server keeps a salted SHA-256 of the sign-in key. The stretching was done on the device, and what is kept narrows a guess at the password no more than the keyring already stored beside the ciphertext does.

The address is proved before the account exists. Signing up sends a six-digit code, and the account, its sign-in method and its password row are made only when the code comes back with the same sign-in key that asked for it. A code works once, for fifteen minutes, and wrong guesses are counted against the code and against the address. Signing up with an address that already has an account answers exactly as a new one does and emails the address instead, so the form cannot tell anybody who has an account. A forgotten password can be replaced for signing in, by a code to the address; the budget stays locked with the old one until a passkey or the recovery code opens it, and the app then wraps the key under the new password as well.

  • passwords — per account: the address, the salted hash of the sign-in key, and when it was made, changed and proved.
  • email_codes — per code not yet used: an HMAC-SHA-256 of the address under a server secret (never the address), what the code is for, the code's SHA-256, a sign-up's key hash, when it lapses and how many wrong guesses it has had. Gone once used or lapsed.
  • email_limits — counts over a window, keyed by what is counted and that same keyed hash. Gone within a day of the window closing.

Codes are sent by Cloudflare Email Service, through a send_email binding on the app's Worker: no API key, and no company that does not already run the servers. The app itself never talks to it. What it handles is the address and the message, which is a code and two sentences. Mail from berm.money is DKIM-signed, and the domain publishes a DMARC policy of reject, so mail that only pretends to come from it is refused.

A shared budget

The owner makes an invite link: 32 random bytes, carried after the link's #, which a browser never sends to a server. Two values are derived from it with HKDF-SHA-256 under different labels: a token, whose SHA-256 is what the server stores to find the invite by, and a key under which the budget's master key is sealed with AES-256-GCM. The server keeps the hash and the sealed key, and neither opens anything without the link. An invite works once, lapses after seven days, and can be cancelled until it is used.

Whoever uses it signs in as themselves, presents the token, and is handed the sealed key, which their app opens with the link. Their app then wraps the master key under a password of their own and adds it to the budget's keyring beside the owner's, tagged with their account. Two tables:

  • shares — per member: their account, the owner's, the app, and when they joined. A member's reads and writes of the app's data resolve to the owner's row; their own row, if they have one, is not touched, and joining is refused to an account that already keeps its own copy of that app.
  • invites — per link: the owner, the token's SHA-256, the sealed key, and when it was made and when it lapses.

Every answer about the data names whose row it is, and a write names the row it means to replace, so somebody who has been removed cannot write a copy of the budget into a row of their own. Only the owner can let people in or delete the budget. Removing a member takes effect on their next request, and the owner's app takes their ways in out of the keyring. When two people change the budget at once, the app merges the two copies three ways, against the copy both last agreed on, on the device; the server only ever holds the result, encrypted like everything else. Inside a shared budget, what each person adds carries their account, and each change the account that made it, so the app can say who added what; that is sealed with the rest.

A household

Two people who each keep a budget of their own can plan together in a household: goals each saves a share of, the bills one of them pays for both, single payments shared, payments back, and from those what is owed between them. It is a separate app on the server, shared exactly as a budget is, and sealed under a key of its own: 32 random bytes made on the device that starts it and kept inside each member's budget, so it is as encrypted as the budget and comes back with it. Its invite link carries that key the way a budget's link carries the master key, and the budget's own key is never in it.

A household is two people and one waiting link, and the server holds the same line. Each app writes into it only what its person chose to share, the payments of a shared bill as they reach that person's ledger, and the dates each share of a goal can be had. A change is made to the newest copy and made again on a version conflict, so neither person's change overwrites the other's. Ending it deletes its row, the other person's place in it and any waiting link, at once.

A payment back that is already in somebody's own ledger — money in from the other person, or out to them, sent through PayPal, Venmo, Apple Cash or the like — is recognised on their device, from their own ledger, and offered to them. Only one they confirm is written into the household, as a payment back, the same as one recorded by hand.

A joint budget beside your own

Somebody with a joint account as well as their own can keep a joint budget beside their own budget. It is a third app on the server, shared exactly as a budget is, with a master key, a keyring and a password of its own, and its invite link carries its master key the way a budget's link does. On the device it is stored under names of its own, and which of the two a browser tab holds is decided before anything is read, so the two are never open together and neither can read the other.

Notifications, when you switch them on

Every kind is off by default and switched on by itself. The app works out what it would say over the next five weeks, from the figures on the device, and seals each notice separately for every device on the account that asked for notifications, using Web Push message encryption (RFC 8291, the aes128gcm content coding of RFC 8188): a fresh P-256 key per message, ECDH with that device's key, HKDF-SHA-256 salted with the device's auth secret, and AES-128-GCM. Every notice is padded to one 512-byte record before it is sealed, so the size of a message says nothing about which notice it is, and there is at most one a day, at nine in the morning.

News from an import is the one kind that goes from one person to another. When an import finds something amiss and its person switched telling on, their app seals what it found, the same way and to the same size, for each device of each person who switched hearing on and plans with that budget — the people who share it, and for somebody's own budget the person they keep a household with — and the server posts it at once rather than at nine. It refuses a message for any device whose account did not ask to hear, which is why the two switches are the one thing about notifications it holds in the clear.

Good news about a goal travels as the same kind, on the same switches: a goal within reach or sooner since the last import, read off the Goals card at the import and sent with the rest of what it found; and three things sent at the press that makes them, a card marked paid off, a balance a new statement says is down, and a payment back recorded. Sealed the same way and to the same size, so the server cannot tell good news from a warning, or either from the other.

Three tables, and none holds a word of what a notification says:

  • push_subscriptions — per device: the address its browser's push service issued, the public half of the device's key and its auth secret, whether its owner asked for account notices, and when it was added. What is kept for a device lets a message be sealed for it, not opened.
  • push_messages — per notice per device: the sealed message, verbatim, and when it is due. The app's next schedule replaces its previous one outright, and leaves news alone.
  • push_news — per account: whether it tells the people it plans with what its imports find and its good news, whether it hears theirs, and how many it has told that day.

A scheduled job posts each message when it falls due, signed with VAPID (RFC 8292, an ES256 token whose private key is a Worker secret, never in the database), to the push service the browser named. It will post only to Apple's, Google's, Mozilla's and Microsoft's push services, so a registered address cannot aim it anywhere else. A message a push service refuses is dropped, one for a device that has gone takes the device's row with it, and nothing is kept once it is a day late.

The server writes two messages itself, because they have to be sendable with nobody present: a test, and — to each device whose owner asked for it — that the account has been set to be deleted, at once and again the day before. Neither carries a figure.

Usage counts, when switched on

Off by default, and switched on per device in Settings; the switch lives in the device's own storage and nowhere in the budget, so a shared budget cannot switch the other person on. Switched on, the device keeps a tally in its own storage — a count a day for each of a fixed list of kinds (open, import, pill.safe, plan.kept, go.ledger and so on; app/js/usage.js is the list, and the Worker holds the same list and drops anything not on it) — and once a day posts the days before it, as JSON, to /api/usage, without the session cookie. A device-day is set to one rather than counted, so a total says how many opted-in devices were used on a day and never how often one was. One table:

  • usage_daily — per day per kind: the total. A report adds its numbers to the totals and is not kept: there is no row per report, per device or per account, and nothing to join a total to.

Each count is capped, at most a week is taken from one report, a day outside the last week is dropped, and the request leaves the ordinary request line and nothing more. What the server learns beyond the totals is that some opted-in device posted on a given day. Nothing is counted while the sample is running.

Deleting an account

One press inside the app, and the account is scheduled. Nothing is removed at that moment: the rows stay for thirty days, so that one mis-press — or somebody who reaches a device you are signed in on — cannot destroy years of records in a single irreversible action. What does happen immediately is that the account stops working. Every authenticated request is refused except the one that cancels the deletion, so a session left open on a device you are worried about is worth nothing.

Signing in again inside the window cancels it, and nothing has to be rebuilt — the data was ciphertext the whole time and nothing decrypted it, so cancelling is one row being removed from a queue. After the window a scheduled job deletes the rows: the encrypted payloads for every app, every device's notification address and whatever was queued for it, whether it tells and hears what imports find and the good news, every sign-in method, the password row if there is one, who it shares a budget, a joint budget or a household with and the invite links it made, and the account itself. There is no redaction step and no judgement call about what counts as personal data, because nothing here can read any of it.

One limit worth stating rather than glossing: the database is hosted, and its host keeps its own point-in-time backups on a schedule we do not set and cannot shorten. Our rows are deleted on the date above; a copy can persist in those backups for a further period after it. We would rather say that than imply an erasure we do not control every part of.

A shared budget belongs to the account that shared it, and a joint budget or a household to the account that started it. When that account is set to be deleted, either stops answering its members at once, and at the end of the window their places in it go with its rows. When a member's account is deleted, only their own place goes.

Messages sent to support, through the form or by email, are not joined to any account — deliberately, since the form can be used without identifying yourself at all. That means deleting an account cannot find them. Ask us and we will remove them: the support desk deletes a conversation for good, the form's own row with it.

The app, when a bank is connected

Being tested, not offered. One row per connection:

  • item_id, account_id — which Plaid connection belongs to which account.
  • token — Plaid's access token, sealed with AES-256-GCM under a key held as a Worker secret, with both ids bound in as associated data so a row copied onto another account does not open.
  • created_at.

And in bank_news, one row per connection and word: what Plaid has said about a connection that nothing has yet acted on, as one of three words — something new to read, a sign-in the bank wants, or a connection that stops unless it is renewed. No time, and nothing of what the bank sent. A sync takes the first word away, a finished reconnect the other two, and the rows go with their connection.

Plaid says so by posting to the Worker — a webhook — and the post is trusted only once Plaid's signature checks out, as Plaid sets the check: an ES256 signature, by a key the Worker fetches from Plaid, over the SHA-256 of the body exactly as it arrived, made within the last five minutes. A post that would change nothing is dropped unchecked. A post saying you took the permission back at Plaid disconnects the connection, at Plaid and here, with nobody present.

That key is the server's, not yours, and that is a deliberate choice: Plaid bills for a connection until its token revokes it, and only the token can disconnect it at Plaid. Sealed under your key instead, a forgotten password would leave a connection nobody could end. So the database alone still yields nothing usable; the database and the Worker's secrets together could read a connected account until it is disconnected.

Nothing read from the bank is written down. Transactions and balances pass through one request on their way to your device, which encrypts them into the vault. The sync cursor — how far you have read — goes back with them and is kept in the vault too. The bank's name and the accounts' names are kept only there. A connection still being made is remembered by its link token until it is finished; one older than the six hours Plaid keeps it is discarded the next time it is looked at.

The site's support form

One row per message: a UTC timestamp, which of the four kinds was picked, the message, an optional contact detail, and one flag saying whether the sender arrived from a link inside the app. No IP address, no cookie, no fingerprint, no referrer. This is the one part of Berm that is not encrypted on the sender's device, and it cannot be. A bank asked for on /banks is a row in the same table, of its own kind, holding the bank's name, an optional country and the time, with the contact left empty.

The support desk

Support is read and answered on a private page of our own, behind Cloudflare Access, which lets in the people who answer support and nobody else. The page checks the token Access signs for every request as well, so a gap in that setting closes the page rather than opening it. Email to support@berm.money is received by a Worker of its own, which files it there and forwards a copy to the inbox support was read in before. Neither can reach the app's database. The desk adds four tables to the support database:

  • tickets — one per conversation: when it started and last changed, how it arrived (the form, an email, or us writing first), the form's kind and app flag where there are some, the subject, the address and name it came with, a status and a priority, and the first words of the latest message.
  • entries — everything on a conversation, in order, each with its time: what you wrote and what we wrote, our own notes, a status or priority changed. For an email, the addresses, the subject, the earlier conversation it quoted (kept apart from the new words), its message id and the ids it answers — which is how a reply finds its conversation — and the name, type and size of each attachment. The attachments themselves are never stored.
  • templates — replies we have saved to start from.
  • alerts — for each alert emailed to us about a conversation, its number and the time, kept for a day.

Which of us wrote a reply or changed a ticket is recorded, as the address we signed in to Access with. Replies go out through Cloudflare's own email service, as plain text, with no tracking of any kind.

When a message arrives, a number goes on a Cloudflare queue, support-events: the form's row, or the conversation an email was filed on and whether it began one, added to one or is a delivery report about one. The Worker that receives email reads it, makes a form message a conversation there and then, and when that conversation is urgent or high priority, emails the inbox support is forwarded to with its number, its priority, how it arrived and a link: once an hour for a conversation at most, and ten an hour in all. Neither the queue nor that email carries anything anybody wrote, an address, a name or a subject. A number stays on the queue until it has been read, and no longer than four days if it cannot be.

What an assistant sends, and the keys it sends with

Off until somebody connects one in Settings. A connected app can add transactions, the balance and a bank's export file to one budget, and nothing else: there is no way for it to read anything. The server holds no key to a budget, so what is sent waits for the budget in an inbox. The app makes a P-256 key pair the first time one is connected, keeps both halves inside the encrypted budget, and gives the server the public half. A send is sealed to that key: an ephemeral ECDH key agreement with it, HKDF-SHA-256 over the shared secret with both public keys as the salt, and AES-256-GCM with the key's id as associated data, over the send padded with spaces to a power of two from 1 KB. The app opens it, checks every field again, takes it in as one change it can undo, and has the server delete the send once that change has been saved. A send nobody takes in is deleted after thirty days.

Berm's program for assistants (/agent/berm.mjs, one file with no dependencies, run with Node on the person's own computer, over MCP's stdio transport or from a command line) seals there. It asks for the budget's public key (GET /api/v1/key, with a key made in Settings), seals exactly as above, and posts the envelope (POST /api/v1/sealed). The server cannot check what is inside, so it checks the envelope: its shape, that it is sealed to the key the budget published, and that it is padded as above. A bank's file is read from disk by the program and sealed whole, gzipped, so the assistant need only pass its path; the app inflates it, to 12 MB at most, and reads it with the importer the Import card uses, which leaves it on that card to be checked when it cannot tell which column is which. A connector on the web cannot run anything on the person's device, so the server seals what it sends the moment it arrives, and takes no file that way. One send is at most 1.5 MB sealed.

  • inbox — the budget's public key, and each send, sealed, with the time it arrived. At most fifty waiting for one budget, and 6 MB.
  • api_tokens — the SHA-256 of each token an app holds, never the token: a key made in Settings, or the tokens of an app's sign-in. The account it belongs to, what kind it is, the name it is listed by, the app it was given to, when it was made and last used, and when it lapses. A sign-in's code also keeps, for its ten minutes, the address it may be sent back to and its PKCE challenge.
  • oauth_clients — apps that registered themselves to sign in: the name each gave and the addresses it may be sent back to, nothing about any person. One nobody has let in goes after a week.

An app signs in the way OAuth 2.1 describes for a public client: the code flow, with PKCE using S256 and nothing else, at the endpoints this server publishes at /.well-known/oauth-authorization-server, and for the MCP endpoint at /.well-known/oauth-protected-resource, registering itself first (RFC 7591). Asking to be let in sends the browser to the app, which asks the person once their budget is open; the server never shows a sign-in page of its own. Tokens are 256 random bits. An access token lasts an hour; a refresh token ninety days, and each is used once; a code lasts ten minutes and is used once. Removing an app in Settings ends every token it holds at once.


Headers

The app, at app.berm.money:

default-src 'self'; script-src 'self';
style-src 'self' 'unsafe-inline'; img-src 'self' data:;
font-src 'self'; connect-src 'self'; manifest-src 'self';
worker-src 'self'; base-uri 'none'; form-action 'none';
frame-ancestors 'none'; object-src 'none'

connect-src 'self' is the load-bearing one: the app makes no third-party requests of any kind, so even a compromise has nowhere to send anything. There are no analytics, no embedded fonts, no tag managers and no external scripts.

This site, which is a separate Worker:

default-src 'self'; script-src 'none'; style-src 'self';
img-src 'self' data:; font-src 'self'; connect-src 'self';
manifest-src 'self'; base-uri 'none'; form-action 'self';
frame-ancestors 'none'; object-src 'none'

These pages ship no JavaScript at all — not a bundle, not an inline block — which is what lets script-src be 'none' rather than something looser. The support form is a plain POST, which is why form-action is 'self' and not 'none'. Both sites also send X-Content-Type-Options, Referrer-Policy: no-referrer, a Permissions-Policy denying geolocation, camera, microphone, payment, USB and topics, Cross-Origin-Opener-Policy: same-origin, and HSTS for a year including subdomains.

The support desk is a third Worker with this site's policy word for word, and it too runs no script: every action on it is a plain form. It adds Cache-Control: no-store, so no conversation is kept in a cache, and asks search engines to leave it alone, though Access would stop them first.


Limits

Berm's server ships the JavaScript that does the encrypting. So the encryption protects you against a stolen database, a stolen backup, a subpoena and a curious operator. It does not protect you against a hostile or compromised deployment of the app itself, which could take a key at runtime. That is a real and useful guarantee, and it is narrower than we cannot read your data sounds. We would rather write it down than let the sentence do work it cannot.
  • No key escrow, so no recovery. A lost password cannot be recovered by anyone, including us. There is no reset link for the data, because a reset link is proof that somebody else can get in; the code an email address can be sent resets only the sign-in.
  • An email address is a way in. Whoever can read it can choose a new password for signing in, and so sign in — as whoever holds any other account you sign in with can. Neither opens the budget, which still needs its own password, a passkey or the recovery code. A new password does not end sessions already open: one lasts until it lapses.
  • Browser storage is not durable. Clearing site data deletes the only local copy. On iOS and iPadOS a tab untouched for seven days is cleared by the system; an installed app is exempt and keeps its own storage, which is why the app asks to be installed.
  • Transport is ordinary TLS terminated at Cloudflare, which also sees request metadata — addresses, times and sizes — as any host would.
  • Logs say what broke, never who. Both Workers keep their own lines in Cloudflare's Workers Logs for a few days, with exceptions grouped. The app's Worker writes one line for each request to its API: the route, taken from a fixed list rather than the address as sent, the method, the status, how long it took, the version, and a random reference sent back to the app with the answer, which Settings shows after a failure so it can be quoted to support. It writes one for each error the app reports from a device too: the kind of error, its message with anything in quotes, every number, links and email addresses removed, where in the app's own files it happened, the version, and the browser by name and major version. The app sends those without the sign-in cookie, and the Worker checks each field again. No line carries an account, an email address, an IP address, a cookie, a query string or anything that was sent. Cloudflare's own per-request invocation logs and traces are switched off in both Workers' configuration, query strings are redacted from whatever is kept, and the site writes no line when a page is read or a search is made. The lines are still times: while there are few people using Berm, when requests happened says more than it will. Cloudflare counts requests and errors for its dashboard, as any host does.
  • Notifications leak timing. The sealing hides what a notification says and the padding hides how long it is, but the server and the push service both see the days one is due. At most one a day, at a fixed hour, keeps that to which days have something to say. News from an import is the exception, sent at once: the server sees that one person's import found something to tell another, and when. Good news about a goal is sent the same way, and three kinds of it go at a press rather than an import — a card marked paid off, a balance a new statement says is down, a payment back recorded — so the server sees the time of that press as well. It cannot tell a warning from good news: every message is one size, and none of it is readable.
  • A connected bank tells the server when it has news. Plaid posts to the server whenever a connected account has something new or needs its owner to sign in again, so the server sees roughly when a connected account changes, and whose it is. What changed it never sees until the app asks for it, and then only on its way to be encrypted on the device.
  • What a connector on the web sends is readable to the server as it arrives. A remote MCP server runs the tool where it is hosted, so its arguments arrive as they were written; they come over TLS and are sealed before anything is stored, and nothing from them is logged. That is a promise about what this server does, not a lock it cannot open. What Berm's program sends from a person's computer is sealed there, so this limit is not its. Either way the server sees which keys and apps an account has connected, the name each is listed by, and when each sends and roughly how much.
  • An assistant that can read files can open a bank's file. Berm's program reads the file itself and tells the assistant not to, so its contents need not pass through the conversation, but nothing Berm runs can stop an assistant asked to read it.
  • A token that leaks can add to a budget. Whoever holds one can add transactions and set the balance, which the app takes in with Undo, until the token is removed in Settings. It cannot read anything. A hostile server could seal a send to a budget's public key as well, which is the limit above in another form.
  • Sharing tells the server who shares with whom. Which accounts are in one budget or one joint budget, and since when, is in the database in the clear. What is in the budget is not.
  • A household tells the server who plans with whom, in the same way: which two accounts, and since when, but nothing of what is in it.
  • Removing somebody does not change the key. They stop getting anything new from the server, but they held the master key, and whatever they copied before stays readable to them. The key is not rotated when somebody leaves, so a former member who got hold of the stored ciphertext some other way could read that too.
  • Usage counts are totals, and a total of one is a person. While few people use Berm, a day's totals are mostly one person's doing; the switch is off until that person decides they do not mind. The post itself is a request at a time, like any other, with nothing on it that says who.
  • Support messages are readable by us, and so is what we write back. A message nobody can read is not a support request. Email additionally carries the sender's address and passes through the mail providers at both ends.

Being tested, not offered: an optional bank connection

Reading balances and transactions from your accounts directly, for people who would rather not export a file, is built and switched on only for the people who make Berm, to test. It would be an option, never a requirement. Plaid is reached by whole-page redirect to its Hosted Link page rather than by loading its code into the app, so no Content-Security-Policy directive above changed: the app still fetches from its own origin and nothing else, and the Worker talks to Plaid server to server. Only the transactions product is requested, with a year of history.

One thing did not survive contact with Plaid's terms, and this page changed before the feature did, as it said it would: the access token is held by the server rather than sealed under your key. The section on what the servers hold says why, and what that means.


Reporting something

If you have found a vulnerability, send it through the support page or to the address on it, and say up front that it is a security report. Please include what you did and what you saw. There is no bug bounty. A real report gets read by the person who can fix it, and credit if you want it.