Documentation · Solana mainnet-beta
Duskmoth docs
Everything about how Duskmoth moves SOL and USDC privately on Solana mainnet: the contracts it talks to, what it costs, what it checks, and how to run it yourself.
Overview
Duskmoth is a chat interface for private transfers on Solana. You type what you want in plain words, Duskmoth shows a readable preview, and your own wallet signs. Under the hood it uses the Privacy Cash shielded pool on mainnet, so the recipient gets paid without an on-chain link back to your wallet.
Send
Pay any Solana address in SOL or USDC without exposing your main wallet.
Shield
Park funds in your private balance now, send from it later.
Score
See how much your public wallet reveals, with plain fixes.
Duskmoth is non-custodial. It never holds keys or funds, and the AI can only propose a transaction. Every move needs your wallet's signature.
Quickstart
- Open the app and connect Phantom, Solflare or Backpack. Your wallet must be on Solana mainnet.
- Sign once to unlock your private balance. Your wallet asks you to sign the message
Privacy Money account sign in. This is free, moves nothing, and lets your browser derive the key that encrypts your private notes. - Say what you want, for example
send 0.5 SOL to 7xKp…sMn privately. - Read the preview: amount, fees, what the recipient receives, privacy strength, and the screening result for both addresses.
- Confirm and sign. Your browser builds a zero-knowledge proof, the deposit goes on-chain, and a relayer pays the recipient from the pool.
Start with a small amount, such as 0.05 SOL, the first time you use it. Withdrawals have a flat fee, so very small sends are expensive (see Fees).
How a private send works
Your sentence goes to /api/intent. Claude (or the built-in rule parser) proposes an action. The server re-checks the token, the amount, your limits, and that the recipient address appears exactly as you typed it.
/api/screen checks valid supplied addresses using Scorechain sanctions lists. A match or an incomplete check blocks approval. This is not full AML risk screening. Transfers require your explicit confirmation and wallet approval.
Your wallet signs a deposit into the Privacy Cash pool. A commitment is added to the pool's Merkle tree (depth 26), and an encrypted note that only you can open is stored with it.
Your browser generates a Groth16 proof (circuit transaction2) showing you own an unspent note, without revealing which one.
The proof goes to the Privacy Cash relayer, through this server's /api/relayer proxy. The relayer submits the withdrawal and pays the recipient. The relayer pays the network fee, so your wallet never touches the withdrawal transaction.
For Shield now, send later, steps 4 and 5 happen whenever you choose. The longer the gap and the rounder the amount, the more deposits yours blends into.
Network and addresses
Duskmoth runs only on Solana mainnet-beta. All values below are the ones in the Privacy Cash SDK version Duskmoth ships (privacycash@1.3.1). Verify them on any explorer before you trust them.
| Item | Value |
|---|---|
| Privacy Cash program | 9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD |
| Address lookup table | HEN49U2ySJ85Vc78qprSW9y6mFDhs1NczRxyppNHjofe |
| Protocol fee recipient (SDK constant) | 97rSMQUukMDjA7PYErccyx7ZxbHvSDaeXp2ig5BwSrTf |
| USDC mint | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Relayer API | https://api3.privacycash.org |
| Merkle tree depth | 26 |
| ZK circuit | transaction2 (wasm + zkey, served by your Duskmoth host) |
| Default RPC | https://mainnet.helius-rpc.com with a Helius key, otherwise https://api.mainnet-beta.solana.com |
Explorer: view the program on Solscan.
Wallet activity
When you connect a wallet, the Wallet panel reads its public history straight from Solana mainnet, so you can see exactly what anyone with an explorer can see.
| Shown | How it is measured |
|---|---|
| Public SOL and USDC | getBalance and your USDC token accounts |
| Shielded balance | Decrypted in your browser from your Privacy Cash notes after you sign in. Never sent to the server. |
| Txs, 30 days | Signatures in the last 30 days, out of the latest 100 |
| Active days | Distinct days with at least one transaction in the last 30 |
| Last active | Time of the most recent signature, plus recent failed transactions |
| Recent activity | The 10 latest transactions with SOL and USDC changes, each linked to Solscan |
Transactions that touch the Privacy Cash program are labelled Shielded or Private payout based on the sampled transaction data. Unavailable details are marked unavailable. There is no demo wallet or sample activity fallback.
Fees and limits
| What | Cost | Paid to |
|---|---|---|
| Deposit (shield) | 0% | Only the normal Solana network fee |
| Withdrawal (send out of the pool) | 0.35% of the amount | Privacy Cash protocol |
| Relayer rent fee per withdrawal | 0.006 SOL for SOL withdrawals | Privacy Cash relayer |
| Duskmoth fee | None | Duskmoth adds nothing on top |
The relayer publishes its current rates at https://api3.privacycash.org/config (withdraw_fee_rate, rent_fees, minimum_withdrawal), and the SDK reads them at send time. The preview always shows the live numbers. Rent fees and minimums for SPL tokens such as USDC are set per token in that same config.
Worked example
| You send | Fees | Recipient gets | Effective cost |
|---|---|---|---|
| 0.1 SOL | 0.00035 + 0.006 = 0.00635 SOL | 0.09365 SOL | 6.4% |
| 1 SOL | 0.0035 + 0.006 = 0.0095 SOL | 0.9905 SOL | 0.95% |
| 5 SOL | 0.0175 + 0.006 = 0.0235 SOL | 4.9765 SOL | 0.47% |
Duskmoth limits
- Maximum per send: 5 SOL or 500 USDC by default. Hosts can change this with
MAX_SEND_SOLandMAX_SEND_USDC. - Supported tokens in the app: SOL and USDC. The pool supports more SPL tokens, which may be added later.
- The relayer enforces a minimum withdrawal per token (for SOL, currently 0.01 SOL).
What you can say
Write in plain English. With a Claude key on the server, phrasing is flexible. Without one, a rule-based parser handles the patterns below.
| Intent | Example | What happens |
|---|---|---|
| Private send | send 2 SOL to <address> privately | Shield and pay out right away |
| Send later | send 50 USDC to <address> later | Shield now, you choose when to pay out |
| Shield | shield 1 SOL | Move funds into your private balance |
| Private balance | what's my private balance? | Opens your shielded balance |
| Privacy score | check my privacy score | Opens the score for your connected wallet |
The recipient must be a full Solana address that you typed or pasted yourself. The server rejects any address the AI produced that is not in your message, word for word.
Sanctions screening
Duskmoth checks the recipient and any supplied sender using Scorechain's sanctions-only API. Missing senders are not claimed as checked. These checks do not enable live transfers.
GET https://sanctions.api.scorechain.com/v1/addresses/<ADDRESS>
x-api-key: <SCORECHAIN_API_KEY>
- Scorechain returns a boolean sanctions-list match, not a risk score.
- A match blocks approval. A no-match result is not proof that funds are safe.
- Fails closed: invalid addresses, missing credentials, invalid provider responses, request limits, and network failures cannot pass.
- The free service is limited to 100 lookups/hour and non-commercial use. Solana list completeness and public commercial eligibility still need confirmation.
Automated match tests use clearly labelled provider fixtures. They are not evidence of a verified live sanctioned Solana address.
Privacy score
The score reads what the public chain already says about a wallet, using the Helius Wallet API. To stop people from profiling other wallets, you must prove ownership first by signing this exact message (valid for 5 minutes, ed25519):
Duskmoth: prove I own <ADDRESS> to see my privacy score. Timestamp <UNIX_MS>. This costs nothing and moves no funds.
| Signal | Points | Source | Fix |
|---|---|---|---|
| Funded straight from an exchange | −25 | GET /v1/wallet/{w}/funded-by | Fund a fresh wallet from your private balance |
| Sends to exchange deposit addresses | −15 | transfers + batch-identity | Use a separate wallet for exchange withdrawals |
| Linked .sol / public name | −15 | GET /v1/wallet/{w}/identity | Keep the name on a small display wallet |
| Long, busy public history | −10 | transfers | Split activity across wallets |
| Large balance visible to anyone | −10 | balances | Shield idle funds |
| Has used a privacy pool | +10 | transfers | None |
The score starts at 100 and is graded A to F. The share card shows only the score and grade, never the address or balance.
Helius serves identity, batch-identity and funded-by only on paid plans (free plans get 403). On a free key, those signals are skipped.
API reference
The Duskmoth server is a small Express app. Base path /api, JSON in and out, rate limited per IP (about 40 requests per burst). Request bodies and addresses are never logged.
GET/api/health
Liveness and which services are live.
{ "ok": true, "mode": "production", "network": "mainnet-beta", "ai": "claude",
"screening": "scorechain", "screeningScope": "sanctions-only", "data": "helius", "liveSend": true, "demoWallet": false, "relayerProxy": true }GET/api/config
Which services are live on this host.
{
"ai": "claude", // or "rules"
"screening": "scorechain", // or "off"
"data": "helius", // or "public-rpc"
"liveSend": true, // user confirmation and wallet approval required
"limits": { "SOL": 5, "USDC": 500 }
}POST/api/intent
Turn a sentence into a proposed action. The AI never signs.
// request
{ "message": "send 0.5 SOL to 7xKpQm3vHsT9wRb2eYcNf4LdJ8gUaZ5qXr6tPkW1sMn privately" }
// response
{
"reply": "Ready to send 0.5 SOL to 7xKp…1sMn without linking it to your wallet…",
"action": { "type": "send", "amount": 0.5, "token": "SOL",
"recipient": "7xKpQm3vHsT9wRb2eYcNf4LdJ8gUaZ5qXr6tPkW1sMn", "delay": "now" }
}
// action.type: "send" | "shield" | "balance" | "score" | "help", or action is null when more info is neededPOST/api/screen
Screen both parties. Fails closed.
// request
{ "sender": "<ADDRESS>", "recipient": "<ADDRESS>" }
// response
{ "source": "scorechain", "scope": "sanctions-only", "allowed": true,
"sender": { "isSanctioned": false }, "recipient": { "isSanctioned": false } }
// blocked: "allowed": false plus a "reason" stringPOST/api/score
Privacy score for a wallet you own. Needs the signed ownership message.
// request
{ "address": "<ADDRESS>", "ts": 1793414400000, "signature": "<base58 ed25519>" }
// response
{ "wallet": "7xKp…1sMn", "score": 62, "grade": "C", "source": "helius", "txCount": 418,
"signals": [ { "id": "cexFunded", "label": "Funded from an exchange", "impact": -25, "detected": true, "fix": "…" } ],
"summary": "Your wallet scores 62/100 (C). The biggest leaks: …",
"fixes": [ "…" ] }
// 401 if the signature is missing, wrong or older than 5 minutesPOST/api/activity
Public mainnet activity for a wallet: SOL and USDC balances, 30-day stats from the last 100 signatures, and the 10 most recent transactions classified as received, sent, shielded, private payout or app interaction. Reads only standard RPC methods (getBalance, getTokenAccountsByOwner, getSignaturesForAddress, getTransaction).
// request
{ "address": "<ADDRESS>" }
// response
{ "address": "…", "network": "mainnet-beta", "source": "helius-rpc",
"sol": 4.218, "usdc": 120.5,
"stats": { "sampled": 100, "last30d": 23, "activeDays30d": 9, "failed": 1,
"lastActive": 1793410000, "oldestSampled": 1781000000 },
"history": [ { "signature": "…", "time": 1793410000, "ok": true, "type": "receive",
"sol": 0, "usdc": 25.5, "fee": 0 } ] }GET/api/card.svg?score=62&grade=C
Shareable score card (1200×630 SVG). Contains no address.
POST/api/rpc
Mainnet JSON-RPC proxy, so the Helius key never reaches the browser. Up to 10 calls per batch. Only these methods are allowed:
getLatestBlockhash getAccountInfo getMultipleAccounts getBalance getTokenAccountBalance getTokenAccountsByOwner getSignatureStatuses getSignaturesForAddress getTransaction sendTransaction simulateTransaction getMinimumBalanceForRentExemption getSlot getBlockHeight getFeeForMessage getRecentPrioritizationFees
POST/api/relayer/*
Pass-through to the official Privacy Cash relayer for the paths the SDK uses (/config, /deposit, /withdraw, /utxos/*, /merkle/*). Anything else returns 404. The user's IP is not forwarded.
POST/api/waitlist
Unavailable (503) until durable storage is configured. No email is stored or reported as saved.
Self-host on mainnet
Requirements: Node.js 20+ or Docker, and a domain with HTTPS (browser wallets will not sign on plain HTTP outside localhost). The release package already contains the ZK circuit and the pre-built proving module. Full steps are in INSTALL.md.
cp .env.example .env # fill in your keys and domain
docker compose up -d --build
docker compose exec duskmoth npm run check
# or without Docker
npm ci && npm run build
npm run check # connectivity checks: mainnet RPC, relayer, Scorechain, Helius
pm2 start ecosystem.config.cjs
Production environment
NODE_ENV=production
PORT=8787
PUBLIC_URL=https://duskmoth.xyz
ENABLE_LIVE_SEND=true # enable wallet-approved transfers; false is an operator kill switch
MAX_SEND_SOL=1 # start low
MAX_SEND_USDC=100
RELAYER_PROXY=true # browser → /api/relayer → official relayer
RELAYER_URL=https://api3.privacycash.org
SCORECHAIN_API_KEY=… # sanctions-only checks
HELIUS_API_KEY=… # mainnet RPC + Wallet API
ANTHROPIC_API_KEY=… # optional
ANTHROPIC_MODEL=claude-sonnet-5-5
ALLOW_DEMO_WALLET=false
ALLOW_UNSIGNED_SCORE=false
| Variable | Required on mainnet | Without it (production) |
|---|---|---|
NODE_ENV=production | Yes | No demo behavior is available in any mode. |
ENABLE_LIVE_SEND | true for wallet-approved transfers | When false, the operator disables deposits and withdrawals. No simulation replaces them. |
SCORECHAIN_API_KEY | For sanctions checks | Screening cannot pass without it; a configured key does not guarantee provider coverage or commercial eligibility. |
HELIUS_API_KEY | Strongly recommended | Public RPC (rate limited), privacy score disabled |
ANTHROPIC_API_KEY | Optional | English rule parser |
PUBLIC_URL | Yes | Share previews on X may not show the image |
In production Duskmoth never shows invented data: when a service is missing or down, the feature says so and stops. GET /api/health reports which services are live.
Go-live checklist
npm run checkends with All required checks passed.GET /api/healthshows"mode": "production","screening": "scorechain","data": "helius","liveSend": true,"demoWallet": false. Enabled does not mean a live-funds test has passed.- Send 0.05 SOL to a second wallet you own. Confirm the deposit and the relayer withdrawal on Solscan, and confirm no direct link between the two wallets.
- Obtain a provider-supported sanctioned Solana test case and verify it is blocked; do not assume an old Range example is sanctioned by Scorechain.
- Lower
MAX_SEND_SOLandMAX_SEND_USDCfor your first days live. - Check that privacy tools are legal where you and your users are.
Security model
| Who | Can see | Cannot do |
|---|---|---|
| Your wallet | Every transaction before signing | Not applicable: it holds your keys |
| Your browser | Your encryption key (derived from your sign-in signature), your notes, the ZK proof | Not applicable |
| Duskmoth server | Your sentence, the two addresses for screening, the address you score or view activity for | Sign, hold funds, see your private notes. It stores none of this. |
| Privacy Cash relayer | The withdrawal proof, recipient and amount | Link the withdrawal to your deposit |
| Anyone on the explorer | A deposit into the pool, and a withdrawal from the pool | Tell which deposit paid which recipient |
- API keys (Anthropic, Scorechain, Helius) live only on the server. The browser talks to Helius through
/api/rpcwith a method allowlist. Transaction submission is blocked while transfers are unavailable. - No request bodies or addresses are logged. The only thing written to disk is the waitlist email file.
- Losing access to your wallet means losing access to your private notes too, because the encryption key is derived from your wallet's signature. Withdraw what you do not need to keep shielded.
Honest limits
- The anonymity set is what other people deposit. If you deposit 3.217 SOL and someone receives 3.206 SOL a minute later, the amounts and timing can link you. Use round amounts and wait.
- Privacy is not invisibility. Your deposit is still visible as a deposit into the pool.
- Third-party dependence. Wallet features depend on Helius/RPC and sanctions checks depend on Scorechain. No failed service is replaced with invented data. Wallet-approved transfers depend on successful screening, current relayer fees, sufficient funds, proof generation and network confirmation. Provider coverage, commercial-use eligibility and live-funds verification are separate requirements.
- The live send path is new. Test it with small amounts on mainnet before relying on it.
- Not legal or financial advice. Privacy tools are restricted in some countries.
FAQ
Is Duskmoth a mixer?
No. It is a chat interface over a non-custodial zero-knowledge pool, with sanctions screening on both sides of every transfer. Duskmoth never takes custody of funds.
Does it work on devnet?
No. Privacy Cash and Duskmoth run on Solana mainnet-beta only. Test with small mainnet amounts instead.
Why did my wallet ask me to sign "Privacy Money account sign in"?
That signature derives the key that encrypts your private notes in the pool. It costs nothing and authorizes no transfer.
Can I get my funds out if Duskmoth disappears?
Yes. Your notes live on-chain in the Privacy Cash pool and are encrypted to your wallet. Any Privacy Cash client, including the official one, can withdraw them with the same wallet.
Why is a 0.05 SOL send so expensive?
The relayer charges a flat 0.006 SOL per withdrawal on top of 0.35%. On small amounts the flat part dominates.
What is $DMOTH?
The Duskmoth community token ticker. Duskmoth works without holding it, and no fees are paid in it.