Payroll Payments Agents Developers FAQ
Read the docsOpen app

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.

dimpsy.ts
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.

deposit.ts
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.

pay.ts
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

balance.ts
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

EventVisible on chainHidden
DepositThat a deposit settled, when, the amount, the asset and the depositing walletWhich private balance it went into
Private transferThat a valid transfer settled, when, and which relayer submitted itAmount, sender, recipient, both balances, memo
WithdrawalThat a withdrawal settled, when, the amount, the asset and the receiving walletWhich 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

pay.ts
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 0x address and never appears on chain, so sharing it or reusing it doesn’t link your payments together.
  • Read yours from dimpsy.address after createClient, or copy it from the app.
  • Withdrawals go to ordinary 0x addresses; private payments only go to dimpsy1 addresses or 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.

claim.ts
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

CodeMeaningWhat to do
INSUFFICIENT_BALANCEAvailable balance is lower than amount plus feeDeposit more, or wait for pending notes
INVALID_ADDRESSThe to value is not a valid dimpsy1 addressCheck for a typo; the checksum caught it
PAYMENT_EXPIREDThe payment was built more than 10 minutes agoCall createPayment again
NULLIFIER_SPENTA note in this payment was already spentUsually a retry of a payment that settled; check the receipt
RELAYER_UNAVAILABLEThe relayer refused or timed outRetry 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:

ColumnFormatNotes
nameTextShown only to you, in the app and in your receipts. Never on chain.
addressA dimpsy1… address, or the word linklink creates a claim link for that row instead of a direct payment
amountDecimal, e.g. 4200.00In the run’s asset. No currency symbols or thousands separators.

Example file

payroll-october.csv
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

  1. Check. Every row is validated: addresses checksummed, amounts parsed, and the total compared with your available balance. Nothing is sent if any row fails.
  2. Prove. Each payment is proved on your device. Rows marked link get a claim link you can copy or export.
  3. 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.
  4. Prove the total (optional). Make one proof that the run added up to the total, for your accountant or auditor.

Run payroll from code

payroll.ts
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

prove.ts
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.

verify.ts
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

agent.ts
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:

ToolWhat it does
dimpsy_balanceReturns the remaining daily budget. It does not reveal the full balance unless you allow it.
dimpsy_fetchFetches a URL on the allow-list and pays any x402 challenge within budget.
dimpsy_paySends 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:

mcp.json
{
  "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

OptionTypeMeaning
budget.dailystringMost the agent can spend per UTC day. Resets at 00:00 UTC.
budget.perCallstringMost it can pay for any single request.
budget.asset"USDG" | "USDT"The asset budgets are counted in.
allowstring[]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:

  1. The agent requests a paid endpoint through dimpsy_fetch.
  2. The provider answers 402 with a price and a dimpsy1 address to pay.
  3. The server checks the host against the allow-list and the price against the budget, then builds a private payment on the spot.
  4. It retries the request with the payment in the X-PAYMENT header. The provider verifies it and returns the data.
x402.http
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"
ParamTypeDescription
networkstringChain to use. See Networks.
signerWalletClient | EIP1193ProviderThe wallet that signs once to derive your keys, and pays for deposits.
relayerstring, optionalRelayer URL. Defaults to the public relayer for the network.
prover"worker" | "main", optionalWhere 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" });
ParamTypeDescription
amountstringAmount to deposit. Asks for a token approval first if needed.
assetAsset"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",
});
ParamTypeDescription
tostringRecipient’s dimpsy1 address.
amountstringAmount the recipient receives. The relayer fee is added on top.
assetAsset"USDG" or "USDT".
memostring, optionalUp 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);
ParamTypeDescription
paymentPaymentFrom createPayment.
options.relayerstring, optionalAnother 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" });
ParamTypeDescription
to0x…Any public address, including an exchange deposit address.
amountstringAmount to withdraw. Round amounts leak less.
assetAsset"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" });
ParamTypeDescription
assetAsset, optionalLeave out to get every asset.

ReturnsBalance: { asset, available, pending }, or an array of them when no asset is given.

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" });
ParamTypeDescription
amountstringAmount the claimer receives.
assetAsset"USDG" or "USDT".
expiresDuration, optionalSuch 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",
});
ParamTypeDescription
assetAssetWhich balance to prove.
thresholdstringThe minimum to prove. The real balance stays hidden.
audiencestringThe one reader who can verify. See Audience and expiry.
expiresDurationSuch 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" });
ParamTypeDescription
proofProof | stringA proof object or its URL.
options.audiencestringYour 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"
ParamTypeDescription
receiptsReceipt[]Receipts of payments you sent, including claim links. 32 at most.
audiencestringThe one reader who can verify.
expiresDurationSuch 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

NetworkTypeVaultVerifierStatus
Robinhood ChainMainnet0x0000…TBD0x0000…TBDPlanned
Robinhood Chain TestnetTestnet0x0000…TBD0x0000…TBDPlanned

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

AssetIssuerDecimalsNetworks
USDGPaxos · Global Dollar Network6Robinhood Chain
USDCCircle6Robinhood Chain
USDTTether6Robinhood Chain

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:

  1. On a clean device, create a new account with a new wallet.
  2. From the old account, send your whole balance to the new dimpsy1 address as a private payment. Speed matters more than privacy here.
  3. 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.
  • relay accepts { relayer: "self" } to submit from your own wallet.
  • New RELAYER_UNAVAILABLE error, raised instead of a timeout.

v0.8: Payroll and total proofs

Planned

  • Payroll runs from CSV in the app, with link rows for claim links.
  • New proveTotal: prove the total of up to 32 payments.
  • createClaimLink will return a cancel() 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_fetch tool.
  • Agent keys are created separately from your main key.