Home
Open app

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.

Network: mainnet-beta · No devnet or testnet deployment

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

  1. Open the app and connect Phantom, Solflare or Backpack. Your wallet must be on Solana mainnet.
  2. 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.
  3. Say what you want, for example send 0.5 SOL to 7xKp…sMn privately.
  4. Read the preview: amount, fees, what the recipient receives, privacy strength, and the screening result for both addresses.
  5. 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

1Intent

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.

2Screen

/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.

3Deposit

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.

4Prove

Your browser generates a Groth16 proof (circuit transaction2) showing you own an unspent note, without revealing which one.

5Relay

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.

ItemValue
Privacy Cash program9fhQBbumKEFuXtMBDw8AaQyAjCorLGJQiS3skWZdQyQD
Address lookup tableHEN49U2ySJ85Vc78qprSW9y6mFDhs1NczRxyppNHjofe
Protocol fee recipient (SDK constant)97rSMQUukMDjA7PYErccyx7ZxbHvSDaeXp2ig5BwSrTf
USDC mintEPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Relayer APIhttps://api3.privacycash.org
Merkle tree depth26
ZK circuittransaction2 (wasm + zkey, served by your Duskmoth host)
Default RPChttps://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.

ShownHow it is measured
Public SOL and USDCgetBalance and your USDC token accounts
Shielded balanceDecrypted in your browser from your Privacy Cash notes after you sign in. Never sent to the server.
Txs, 30 daysSignatures in the last 30 days, out of the latest 100
Active daysDistinct days with at least one transaction in the last 30
Last activeTime of the most recent signature, plus recent failed transactions
Recent activityThe 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

WhatCostPaid to
Deposit (shield)0%Only the normal Solana network fee
Withdrawal (send out of the pool)0.35% of the amountPrivacy Cash protocol
Relayer rent fee per withdrawal0.006 SOL for SOL withdrawalsPrivacy Cash relayer
Duskmoth feeNoneDuskmoth 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 sendFeesRecipient getsEffective cost
0.1 SOL0.00035 + 0.006 = 0.00635 SOL0.09365 SOL6.4%
1 SOL0.0035 + 0.006 = 0.0095 SOL0.9905 SOL0.95%
5 SOL0.0175 + 0.006 = 0.0235 SOL4.9765 SOL0.47%

Duskmoth limits

  • Maximum per send: 5 SOL or 500 USDC by default. Hosts can change this with MAX_SEND_SOL and MAX_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.

IntentExampleWhat happens
Private sendsend 2 SOL to <address> privatelyShield and pay out right away
Send latersend 50 USDC to <address> laterShield now, you choose when to pay out
Shieldshield 1 SOLMove funds into your private balance
Private balancewhat's my private balance?Opens your shielded balance
Privacy scorecheck my privacy scoreOpens 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.
SignalPointsSourceFix
Funded straight from an exchange−25GET /v1/wallet/{w}/funded-byFund a fresh wallet from your private balance
Sends to exchange deposit addresses−15transfers + batch-identityUse a separate wallet for exchange withdrawals
Linked .sol / public name−15GET /v1/wallet/{w}/identityKeep the name on a small display wallet
Long, busy public history−10transfersSplit activity across wallets
Large balance visible to anyone−10balancesShield idle funds
Has used a privacy pool+10transfersNone

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 needed

POST/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" string

POST/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 minutes

POST/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
VariableRequired on mainnetWithout it (production)
NODE_ENV=productionYesNo demo behavior is available in any mode.
ENABLE_LIVE_SENDtrue for wallet-approved transfersWhen false, the operator disables deposits and withdrawals. No simulation replaces them.
SCORECHAIN_API_KEYFor sanctions checksScreening cannot pass without it; a configured key does not guarantee provider coverage or commercial eligibility.
HELIUS_API_KEYStrongly recommendedPublic RPC (rate limited), privacy score disabled
ANTHROPIC_API_KEYOptionalEnglish rule parser
PUBLIC_URLYesShare 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 check ends with All required checks passed.
  • GET /api/health shows "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_SOL and MAX_SEND_USDC for your first days live.
  • Check that privacy tools are legal where you and your users are.

Security model

WhoCan seeCannot do
Your walletEvery transaction before signingNot applicable: it holds your keys
Your browserYour encryption key (derived from your sign-in signature), your notes, the ZK proofNot applicable
Duskmoth serverYour sentence, the two addresses for screening, the address you score or view activity forSign, hold funds, see your private notes. It stores none of this.
Privacy Cash relayerThe withdrawal proof, recipient and amountLink the withdrawal to your deposit
Anyone on the explorerA deposit into the pool, and a withdrawal from the poolTell which deposit paid which recipient
  • API keys (Anthropic, Scorechain, Helius) live only on the server. The browser talks to Helius through /api/rpc with 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.