Documentation · v0.9 beta
Dimpsy Docs
Everything you need to hold, send and prove stablecoin payments privately on public chains: from a five-minute quickstart to the contract guarantees.
Dimpsy is a concept. Package names, contract addresses and APIs on this page are illustrative.
Introduction
Dimpsy gives stablecoins a private balance on public chains. You deposit USDG, USDC or USDT into the Dimpsy Vault, and from then on your balance, what you pay, and who you pay are hidden from everyone except you and the people you pay. Settlement still happens on a public chain, so anyone can check that every payment was valid without learning what it was.
There are three building blocks: a private balance, private payments between Dimpsy addresses, and proofs that answer one question about your money (for example “holds at least 10,000 USDG”) without showing the rest.
What stays private
- Your balance, and how it changes over time.
- The amount of every private payment.
- Who sent it and who received it.
- Memos, which are encrypted to the recipient.
What stays public
- That a deposit, transfer or withdrawal settled, and when.
- Amounts at the edges: how much went into the Vault on deposit and how much came out on withdrawal, and the public addresses involved.
- The total held by the Vault contract.
The full breakdown is in What the public can still see.
Self-custody, proofs on your device
Your Dimpsy keys are derived on your device from a signature by the wallet you already use. They never leave it. Every payment is a zero-knowledge proof made locally, in the browser, on a phone or on your own server for agents. Dimpsy, the company in this concept, cannot move, freeze or see your funds, and the Vault contract has no admin withdraw.
Where to go next
- Quickstart: install the SDK and make a private payment.
- Concepts: notes, nullifiers, relayers, and what the chain still shows.
- Payroll: pay a whole team from one CSV.
- Agents and MCP: let an AI agent pay per call, within a budget.
Quickstart
Five steps from install to a private payment. The examples use Robinhood Chain and USDG. To try it without real money, use network: "robinhood-testnet" and test USDG.
1. Install the SDK
$ npm i @dimpsy/sdk
$ pnpm add @dimpsy/sdk
$ yarn add @dimpsy/sdk
Works in Node 20+ and any modern browser. Proofs run in WebAssembly; in the browser they run in a Web Worker so the page stays responsive.
2. Create a client
The client connects Dimpsy to the wallet you already use. Your private keys are derived from one signature and stay on this device.
import { createClient } from "@dimpsy/sdk"; import { createWalletClient, custom } from "viem"; import { robinhoodChain } from "@dimpsy/sdk/chains"; const wallet = createWalletClient({ chain: robinhoodChain, transport: custom(window.ethereum) }); export const dimpsy = await createClient({ network: "robinhood", signer: wallet, }); // dimpsy.address is your private address, e.g. "dimpsy1q7…9xWd"
3. Deposit
A deposit moves tokens from your public wallet into your private balance.
import { deposit } from "@dimpsy/sdk"; const receipt = await deposit({ amount: "1000", asset: "USDG" }); // public: 1,000 USDG went from your wallet into the Vault. // from here on, what you do with it is private.
Deposits are visible at the edge: the amount and your public wallet address are on the record. See what the public can still see.
4. Pay someone
Build the payment on your device, then hand it to a relayer, which submits it so your wallet never appears as the sender.
import { createPayment, relay } from "@dimpsy/sdk"; const payment = await createPayment({ to: "dimpsy1q7…9xWd", amount: "250", asset: "USDG", }); const receipt = await relay(payment); // settled. the explorer shows a valid transfer, no amount, no wallet
5. Check your balance
import { getBalance } from "@dimpsy/sdk"; const { available, pending } = await getBalance({ asset: "USDG" }); // "750.00", "0.00": decrypted on this device, never sent anywhere
Concepts
You don’t need any of this to use Dimpsy, but it helps to know what is hidden, what isn’t, and why.
Private balance
Your private balance is not a number stored anywhere. It is the sum of the notes you own: encrypted records in the Vault that only your viewing key can open. The SDK scans for your notes, decrypts them locally and adds them up. Nobody else, including the relayer, can do that sum.
Notes and commitments
A note is a small private record: an asset, an amount, an owner and a random blinding value. When you receive money, a new note is created for you. The chain only stores the note’s commitment, a hash that hides everything inside it, in an append-only Merkle tree held by the Vault.
To spend, you prove in zero knowledge that you own notes whose commitments are in the tree and that the amounts balance: inputs equal outputs plus any fee. The proof says that; it doesn’t say which notes.
Nullifiers
Spending a note publishes its nullifier, a tag derived from the note and your key. The Vault rejects any nullifier it has seen before, which is what stops double spending. A nullifier can’t be linked back to its commitment, so observers learn that some note was spent, never which one.
Relayer
If you submitted payments from your own wallet, your public address would show up as the sender every time. A relayer submits the proof for you and pays the gas, taking a small fee that is part of the proven payment. The relayer sees the proof and the fee, not the amount or the people. It can’t change a payment: any edit breaks the proof.
A relayer can refuse to submit. If that happens, use another relayer or submit the proof yourself with relay(payment, { relayer: "self" }), at the cost of linking that one transaction to your wallet.
What the public can still see
| Event | Visible on chain | Hidden |
|---|---|---|
| Deposit | That a deposit settled, when, the amount, the asset and the depositing wallet | Which private balance it went into |
| Private transfer | That a valid transfer settled, when, and which relayer submitted it | Amount, sender, recipient, both balances, memo |
| Withdrawal | That a withdrawal settled, when, the amount, the asset and the receiving wallet | Which private balance it came from |
Edges are where privacy leaks. If you deposit 1,337.42 USDG and withdraw 1,337.42 USDG an hour later, anyone can guess those are the same money. Withdraw round amounts, leave time between deposit and withdrawal, and keep paying people inside Dimpsy when you can.
Payments
A private payment has two steps. createPayment builds and proves it on your device; nothing leaves the device yet. relay submits it. Splitting them lets you build a payment offline, show a confirmation screen, or hand it to a relayer of your choice.
Create and relay a payment
import { createPayment, relay } from "@dimpsy/sdk"; const payment = await createPayment({ to: "dimpsy1q7…9xWd", amount: "250", asset: "USDG", memo: "Invoice 0042", // encrypted to the recipient }); // payment.fee is the relayer fee, already inside the proof const receipt = await relay(payment); console.log(receipt.txHash, receipt.settledAt);
A built payment expires after 10 minutes because it proves against a recent state of the Vault. If it expires, call createPayment again; nothing was spent.
Dimpsy addresses
A Dimpsy address starts with dimpsy1, for example dimpsy1q7…9xWd. It encodes two public keys: one that lets the recipient own the note, and one used to encrypt it to them. It is checksummed, so a typo is caught before anything is sent.
- It is not an
0xaddress and never appears on chain, so sharing it or reusing it doesn’t link your payments together. - Read yours from
dimpsy.addressaftercreateClient, or copy it from the app. - Withdrawals go to ordinary
0xaddresses; private payments only go todimpsy1addresses or claim links.
Claim links
To pay someone who doesn’t have Dimpsy yet, create a claim link. The funds move into a one-time note whose key lives in the link itself. Whoever opens the link first can claim it into a new or existing private balance.
import { createClaimLink } from "@dimpsy/sdk"; const link = await createClaimLink({ amount: "80", asset: "USDG", expires: "14d", // unclaimed funds return to you after this }); // link.url looks like https://app.dimpsy.example/claim#k=… // the key is after the "#", so it is never sent to a server
- Treat a claim link like cash: anyone who has it can claim it. Send it over a channel you trust.
- You can cancel an unclaimed link from the app or with
link.cancel().
Errors
| Code | Meaning | What to do |
|---|---|---|
INSUFFICIENT_BALANCE | Available balance is lower than amount plus fee | Deposit more, or wait for pending notes |
INVALID_ADDRESS | The to value is not a valid dimpsy1 address | Check for a typo; the checksum caught it |
PAYMENT_EXPIRED | The payment was built more than 10 minutes ago | Call createPayment again |
NULLIFIER_SPENT | A note in this payment was already spent | Usually a retry of a payment that settled; check the receipt |
RELAYER_UNAVAILABLE | The relayer refused or timed out | Retry with another relayer; nothing was spent |
Payroll
Pay a whole team from one file. Each person gets a private payment, the chain sees a batch of valid transfers with no amounts or names, and you can hand your accountant a proof of the total.
CSV format
A UTF-8, comma-separated file with a header row and three columns:
| Column | Format | Notes |
|---|---|---|
name | Text | Shown only to you, in the app and in your receipts. Never on chain. |
address | A dimpsy1… address, or the word link | link creates a claim link for that row instead of a direct payment |
amount | Decimal, e.g. 4200.00 | In the run’s asset. No currency symbols or thousands separators. |
Example file
name,address,amount Ana Lima,dimpsy1q7f3…9xWd,4200.00 Jonas Berg,dimpsy1z9k2…m4Ta,3850.00 Priya Nair,dimpsy1h4v8…c2Lp,3850.00 Tomás Ruiz,link,2400.00 Mei Chen,dimpsy1r6d1…u8Qe,1950.50
How a run works
- Check. Every row is validated: addresses checksummed, amounts parsed, and the total compared with your available balance. Nothing is sent if any row fails.
- Prove. Each payment is proved on your device. Rows marked
linkget a claim link you can copy or export. - Relay. Payments are submitted through the relayer and settle independently. If one fails, retrying it can’t pay twice: its nullifiers are already spent or still unused.
- Prove the total (optional). Make one proof that the run added up to the total, for your accountant or auditor.
Run payroll from code
import { createPayment, createClaimLink, relay, proveTotal } from "@dimpsy/sdk"; const receipts = []; for (const row of rows) { // rows parsed from the CSV if (row.address === "link") { const link = await createClaimLink({ amount: row.amount, asset: "USDG", expires: "14d" }); receipts.push(link.receipt); // send link.url to row.name yourself } else { const payment = await createPayment({ to: row.address, amount: row.amount, asset: "USDG" }); receipts.push(await relay(payment)); } } const total = await proveTotal({ receipts, audience: "kestrel-accounting", expires: "30d", });
Payroll total proof
A total proof shows that a set of payments you made added up to a stated total, without showing any single amount or recipient. One proof covers up to 32 payments, which is also the most a single payroll run can hold, so one proof always covers a whole run. To pay more people, split them across runs and make one proof per run.
Total proofs are bound to an audience and an expiry, like every other proof. See Audience and expiry.
Proofs
A proof answers one question about your private balance or payments, yes or no, and shows nothing else. A lender can learn that you hold at least 10,000 USDG without learning that you hold 61,204.
Prove a minimum balance
import { proveAtLeast } from "@dimpsy/sdk"; const proof = await proveAtLeast({ asset: "USDG", threshold: "10000", audience: "ridge-credit", // only this reader can verify expires: "24h", }); // share proof.url. it fails if the reader or date changes.
Audience and expiry
Every proof is bound to two things inside the zero-knowledge statement itself, not in a wrapper that could be swapped:
- Audience: a name for the one reader the proof is for, such as
"ridge-credit". Verifying with any other audience fails. - Expiry: a time after which the proof no longer verifies. Use short windows:
"24h"for a loan check,"30d"for an audit.
A proof also records the moment it was made. It says your balance was at least the threshold then, not that it still is.
Verifying a proof
The reader verifies with their own audience name. Verification reads public chain state only, so it needs no wallet and no Dimpsy account.
import { verifyProof } from "@dimpsy/sdk"; const result = await verifyProof(proofUrl, { audience: "ridge-credit" }); if (result.valid) { // result.claim: "balance >= 10000 USDG", result.provedAt, result.expiresAt } else { // result.reason: "audience_mismatch" | "expired" | "invalid" }
Proofs for one reader
Because of audience binding, a proof you give a lender can’t be reused by someone else to learn the same thing about you, and it stops working after it expires. Two honest limits:
- The intended reader can still tell others what they learned. What they learned is only the yes/no claim, never your balance.
- Audience names are labels, not identities. Use a name the reader gave you, so a stranger can’t pick it by guessing.
Agents and MCP
The @dimpsy/mcp package is an MCP server that gives an AI agent its own private balance with spending limits. The agent can pay for APIs per call over x402. Providers get paid; nobody can read your agent’s treasury from the chain.
The MCP server
import { dimpsyMcp } from "@dimpsy/mcp"; const server = dimpsyMcp({ budget: { daily: "25", perCall: "0.10", asset: "USDG" }, allow: ["api.search.example", "api.maps.example"], }); // your agent pays per call over x402. // providers get paid, nobody sees the treasury. server.listen();
The server exposes three tools to the agent:
| Tool | What it does |
|---|---|
dimpsy_balance | Returns the remaining daily budget. It does not reveal the full balance unless you allow it. |
dimpsy_fetch | Fetches a URL on the allow-list and pays any x402 challenge within budget. |
dimpsy_pay | Sends a private payment to a dimpsy1 address. Off by default; enable with tools: { pay: true }. |
Connect an MCP client
Most MCP clients read a JSON config. Point one at the server with npx:
{
"mcpServers": {
"dimpsy": {
"command": "npx",
"args": ["-y", "@dimpsy/mcp"],
"env": {
"DIMPSY_NETWORK": "robinhood",
"DIMPSY_BUDGET_DAILY": "25",
"DIMPSY_BUDGET_PER_CALL": "0.10",
"DIMPSY_ASSET": "USDG",
"DIMPSY_ALLOW": "api.search.example,api.maps.example"
}
}
}
}
On first run the server creates a separate agent key in ~/.dimpsy/agent.key and prints its dimpsy1 address. Fund that address from your own balance with a private payment. The agent never holds your main key.
Budgets and allow-list
| Option | Type | Meaning |
|---|---|---|
budget.daily | string | Most the agent can spend per UTC day. Resets at 00:00 UTC. |
budget.perCall | string | Most it can pay for any single request. |
budget.asset | "USDG" | "USDT" | The asset budgets are counted in. |
allow | string[] | Hostnames the agent may pay. Exact match; anything else is refused before a payment is built. |
When a call would go over a limit, the tool returns BUDGET_EXCEEDED to the agent and nothing is paid.
x402 per-call payments
x402 uses the HTTP 402 Payment Required status to ask for payment inside a normal request. With Dimpsy the payment is private:
- The agent requests a paid endpoint through
dimpsy_fetch. - The provider answers
402with a price and adimpsy1address to pay. - The server checks the host against the allow-list and the price against the budget, then builds a private payment on the spot.
- It retries the request with the payment in the
X-PAYMENTheader. The provider verifies it and returns the data.
GET /v1/search?q=harbour+cranes HTTP/1.1 Host: api.search.example HTTP/1.1 402 Payment Required { "accepts": [{ "scheme": "dimpsy", "asset": "USDG", "amount": "0.002", "payTo": "dimpsy1x4…r7Qe" }] } GET /v1/search?q=harbour+cranes HTTP/1.1 Host: api.search.example X-PAYMENT: eyJwcm9vZiI6… HTTP/1.1 200 OK
SDK reference
All functions are exported from @dimpsy/sdk, are async, and use the default client set by createClient unless you pass client. Amounts are decimal strings, such as "250" or "0.10", so they never pass through floating point. Assets are "USDG" or "USDT".
createClient
Connects Dimpsy to a wallet, derives your private keys on this device and sets the default client.
function createClient(options: { network: "robinhood" | "robinhood-testnet"; signer: WalletClient | EIP1193Provider; relayer?: string; prover?: "worker" | "main"; }): Promise<DimpsyClient>
const dimpsy = await createClient({ network: "robinhood", signer: wallet }); console.log(dimpsy.address); // "dimpsy1q7…9xWd"
| Param | Type | Description |
|---|---|---|
network | string | Chain to use. See Networks. |
signer | WalletClient | EIP1193Provider | The wallet that signs once to derive your keys, and pays for deposits. |
relayer | string, optional | Relayer URL. Defaults to the public relayer for the network. |
prover | "worker" | "main", optional | Where proofs run. "worker" (default) keeps the UI responsive. |
ReturnsDimpsyClient with address, network and every function below as a method.
deposit
Moves tokens from the signer’s public wallet into your private balance. The amount and wallet are public.
function deposit(params: { amount: string; asset: Asset; }): Promise<DepositReceipt>
const { txHash } = await deposit({ amount: "1000", asset: "USDG" });
| Param | Type | Description |
|---|---|---|
amount | string | Amount to deposit. Asks for a token approval first if needed. |
asset | Asset | "USDG" or "USDT". |
ReturnsDepositReceipt: { txHash, amount, asset, settledAt }.
createPayment
Builds and proves a private payment on this device. Nothing is sent until you call relay.
function createPayment(params: { to: string; amount: string; asset: Asset; memo?: string; }): Promise<Payment>
const payment = await createPayment({ to: "dimpsy1q7…9xWd", amount: "250", asset: "USDG", });
| Param | Type | Description |
|---|---|---|
to | string | Recipient’s dimpsy1 address. |
amount | string | Amount the recipient receives. The relayer fee is added on top. |
asset | Asset | "USDG" or "USDT". |
memo | string, optional | Up to 140 bytes, encrypted to the recipient. |
ReturnsPayment: { proof, fee, expiresAt }. Valid for 10 minutes.
relay
Submits a built payment through a relayer and waits for it to settle.
function relay( payment: Payment, options?: { relayer?: string | "self" } ): Promise<Receipt>
const receipt = await relay(payment); console.log(receipt.txHash);
| Param | Type | Description |
|---|---|---|
payment | Payment | From createPayment. |
options.relayer | string, optional | Another relayer URL, or "self" to submit from your own wallet (links that transaction to it). |
ReturnsReceipt: { txHash, block, settledAt }.
withdraw
Moves tokens from your private balance to a public 0x address. The amount and address are public.
function withdraw(params: { to: `0x${string}`; amount: string; asset: Asset; }): Promise<Receipt>
await withdraw({ to: "0x8c1d…4e2A", amount: "500", asset: "USDG" });
| Param | Type | Description |
|---|---|---|
to | 0x… | Any public address, including an exchange deposit address. |
amount | string | Amount to withdraw. Round amounts leak less. |
asset | Asset | "USDG" or "USDT". |
ReturnsReceipt: { txHash, block, settledAt }.
getBalance
Scans for your notes and decrypts them on this device.
function getBalance(params?: { asset?: Asset; }): Promise<Balance>
const { available, pending } = await getBalance({ asset: "USDG" });
| Param | Type | Description |
|---|---|---|
asset | Asset, optional | Leave out to get every asset. |
ReturnsBalance: { asset, available, pending }, or an array of them when no asset is given.
createClaimLink
Moves funds into a one-time note whose key is in the link. See Claim links.
function createClaimLink(params: { amount: string; asset: Asset; expires?: Duration; }): Promise<ClaimLink>
const { url } = await createClaimLink({ amount: "80", asset: "USDG", expires: "14d" });
| Param | Type | Description |
|---|---|---|
amount | string | Amount the claimer receives. |
asset | Asset | "USDG" or "USDT". |
expires | Duration, optional | Such as "7d". Default "30d", maximum "90d". Unclaimed funds return to you. |
ReturnsClaimLink: { url, expiresAt, receipt, cancel() }.
proveAtLeast
Proves your private balance of one asset is at or above a threshold, for one reader, until an expiry.
function proveAtLeast(params: { asset: Asset; threshold: string; audience: string; expires: Duration; }): Promise<Proof>
const proof = await proveAtLeast({ asset: "USDG", threshold: "10000", audience: "ridge-credit", expires: "24h", });
| Param | Type | Description |
|---|---|---|
asset | Asset | Which balance to prove. |
threshold | string | The minimum to prove. The real balance stays hidden. |
audience | string | The one reader who can verify. See Audience and expiry. |
expires | Duration | Such as "24h" or "30d". |
ReturnsProof: { id, url, claim, audience, provedAt, expiresAt }.
verifyProof
Checks a proof against public chain state. Needs no wallet and no client.
function verifyProof( proof: Proof | string, options: { audience: string } ): Promise<Verification>
const { valid, claim } = await verifyProof(proofUrl, { audience: "ridge-credit" });
| Param | Type | Description |
|---|---|---|
proof | Proof | string | A proof object or its URL. |
options.audience | string | Your own audience name. Must match the one the proof was made for. |
ReturnsVerification: { valid, claim, provedAt, expiresAt, reason? }.
proveTotal
Proves that up to 32 of your payments added up to a total, without showing any single one.
function proveTotal(params: { receipts: Receipt[]; // 1 to 32 audience: string; expires: Duration; }): Promise<Proof>
const proof = await proveTotal({ receipts: run.receipts, audience: "kestrel-accounting", expires: "30d", }); // proof.claim: "total = 16250.50 USDG over 5 payments"
| Param | Type | Description |
|---|---|---|
receipts | Receipt[] | Receipts of payments you sent, including claim links. 32 at most. |
audience | string | The one reader who can verify. |
expires | Duration | Such as "30d". |
ReturnsProof, with claim stating the total and the number of payments.
Contracts
Each network has two contracts: the Vault, which holds deposits and the note tree, and the Verifier, which checks proofs.
Vault guarantees
- No admin withdraw. No key, multisig or upgrade path can move funds out of the Vault. Only a valid proof from the owner of a note can.
- 72-hour timelock on verifier upgrades. If the Verifier ever needs a fix, the new version is announced on chain and can’t take effect for 72 hours, so anyone who disagrees can withdraw first.
- Open source. Contracts, circuits and the SDK are public, and the deployed bytecode can be checked against the source.
- Withdrawals can’t be paused. There is no switch that stops you leaving.
Networks
| Network | Type | Vault | Verifier | Status |
|---|---|---|---|---|
| Mainnet | 0x0000…TBD | 0x0000…TBD | Planned | |
| Testnet | 0x0000…TBD | 0x0000…TBD | Planned |
Addresses are published at launch. Until then, any address claiming to be the Dimpsy Vault is not. Never send funds to a contract address you found anywhere except this page.
Supported assets
| Asset | Issuer | Decimals | Networks |
|---|---|---|---|
| Paxos · Global Dollar Network | 6 | ||
| Circle | 6 | ||
| Tether | 6 |
Source and audits
Source links and audit reports will be listed here at launch, with the exact commit each deployment was built from.
Security
What Dimpsy protects you from, what it doesn’t, and what to do when something goes wrong, in plain language.
Threat model
Dimpsy is built to keep your payments private from people watching the chain: block explorers, analytics firms, competitors, curious coworkers, and anyone who knows one of your public addresses. It also keeps them private from the relayer and from Dimpsy itself, which never sees your keys, your balance or who you pay.
What it doesn’t protect against
- Your own device. If malware can read your browser or files, it can read what you can read.
- The people you pay. A recipient knows what they received from you, and can share it.
- Edges and timing. Deposits and withdrawals are public. Matching amounts and times can link them. See what the public can still see.
- Network metadata. The relayer sees the IP address a proof came from. Use a VPN or Tor if that matters to you.
If your device is compromised
Someone with your Dimpsy keys can spend your private balance and read your payment history. Dimpsy can’t freeze or reverse anything; that is the cost of no admin key. If you think a device is compromised:
- On a clean device, create a new account with a new wallet.
- From the old account, send your whole balance to the new
dimpsy1address as a private payment. Speed matters more than privacy here. - Revoke any agent keys funded from the old account, and stop using it.
Your past history stays readable by whoever has the old keys. Your new account’s history doesn’t.
If an agent key leaks
Agent keys are separate from yours and only hold what you funded them with. The most a leaked agent key can lose is its own balance; budgets limit how fast an agent spends, not what a thief with the key can take. Keep agent balances small and top them up often.
Reporting a vulnerability
Please report security issues privately through the contact address rather than in public. We will reply within two working days.
Roadmap
What we are building, in order. None of this is live yet: today Dimpsy runs as a demo with test money.
v0.9: Mainnet beta on Robinhood Chain
Planned
- Deploy the Vault and Verifier to Robinhood Chain mainnet after an external audit, with deposit limits during the beta.
relayaccepts{ relayer: "self" }to submit from your own wallet.- New
RELAYER_UNAVAILABLEerror, raised instead of a timeout.
v0.8: Payroll and total proofs
Planned
- Payroll runs from CSV in the app, with
linkrows for claim links. - New
proveTotal: prove the total of up to 32 payments. createClaimLinkwill return acancel()method.
v0.7: MCP server and x402
Planned
- First release of
@dimpsy/mcp, with daily and per-call budgets and a host allow-list. - x402 per-call payments through the
dimpsy_fetchtool. - Agent keys are created separately from your main key.