Build on MoneroUSD — provider API, tokens, NFTs, swaps, and privacy-first integration
MoneroUSD dApps interact with the blockchain through window.monerousd, a provider API injected by the desktop wallet or browser extension. This is analogous to window.ethereum for Ethereum dApps.
// Check if the provider is available if (window.monerousd) { console.log('MoneroUSD provider detected'); const result = await window.monerousd.connect(); console.log('Connected:', result.address); } else { console.log('Please install the MoneroUSD wallet'); }
~/.monerousd/dapp-origins.json.Download from monerousd.org. Provider is auto-injected into the built-in dApp browser.
Chrome/Firefox extension injects window.monerousd into any website via a companion auth proxy on port 27751.
JavaScript (frontend). Any language for backend via wallet-rpc JSON-RPC.
FCMP++ at consensus layer. Amounts, senders, recipients, and asset types are all hidden by default.
The window.monerousd provider exposes read and write methods. Write methods trigger a biometric/passphrase approval popup in the wallet.
| Method | Returns | Gate | Description |
|---|---|---|---|
connect() | { address } | Consent popup | Initiates connection. Shows a consent sheet listing requested permissions, then delegates to the wallet's approval popup. |
isConnected() | boolean | None | Returns true if the dApp is currently connected. |
disconnect() | void | None | Tears down the connection for this origin. |
getAddress() | string | None | Returns the per-origin stealth subaddress. |
| Method | Returns | Gate | Description |
|---|---|---|---|
getBalance({ asset_type }) | { balance, unlocked_balance } | None | Returns atomic balance for a specific asset. Default: 'USDm'. |
listAssets() | [{ id, name, symbol, decimals, balance, isNFT }] | None | Lists all assets held by the wallet. |
getAssetBalance(assetId) | { balance, unlocked_balance, asset_type } | None | Returns balance for a specific asset ID. |
| Method | Returns | Gate | Description |
|---|---|---|---|
transfer({ destinations, asset_type, memo }) | { txid, fee } | Biometric | Sends USDm or any asset. Each destination: { address, amount }. |
transferNFT({ nftId, toAddress }) | { txId } | Biometric | Transfers an NFT to another address. |
| Method | Returns | Gate | Description |
|---|---|---|---|
createToken({ name, symbol, maxSupply, decimals, supplyRule }) | { tokenId, creationTxId, bondPaid } | Biometric | Creates a new USDm-T1 fungible token. Bond is non-refundable. |
mintTokenSupply({ tokenId, amount }) | { txId, feePaid } | Biometric | Mints additional supply for mintable tokens (50 bps fee). |
mintNFT({ name, description, contentHash }) | { nftId, creationTxId, bondPaid } | Biometric | Creates a Shadow NFT (USDm-N1). Bond: 10+ USDm. |
| Method | Returns | Gate | Description |
|---|---|---|---|
proveOwnership({ assetId, challenge }) | { proof } | Spend key | Generates an FCMP++ membership proof proving you own the asset. |
proveNFTOwnership({ nftId, challenge }) | { proof } | Spend key | Generates an FCMP++ membership proof for a specific NFT. |
shareViewKey({ scope, id, recipientPubKey }) | { encryptedViewKey } | Biometric | Shares a read-only view key with a specific recipient. |
window.monerousd.on('connect', () => console.log('Connected')); window.monerousd.on('disconnect', () => console.log('Disconnected')); window.monerousd.on('addressChanged', (addr) => console.log('New address:', addr)); window.monerousd.on('chainChanged', (chain) => console.log('Chain:', chain));
USDm-T1 tokens are native protocol assets that inherit FCMP++ privacy. They are not smart contracts — they are processed at the daemon level, making amounts, senders, recipients, and even the token type hidden from observers.
ion1_)Every token is assigned a unique ion1_ address at creation — analogous to a contract address on Ethereum, but privacy-preserving. The address is derived from BLAKE2b-256 (CryptoNote-native) and is 69 characters long.
| Property | Value | Notes |
|---|---|---|
| Prefix | ion1_ | Version byte 1 for future-proofing |
| Hash | BLAKE2b-512 → 256 bits | CryptoNote-native, same as key derivation |
| Length | 69 characters | 5-char prefix + 64 hex digits |
| Verified tokens | Deterministic | BLAKE2b("MoneroUSD_protocol_asset:" + symbol) |
| Custom tokens | Stealth-derived | BLAKE2b(creatorStealth + randomSalt) |
| Property | Constraints | Immutable |
|---|---|---|
name | Max 32 characters | Yes |
symbol | 3-6 alphanumeric characters | Yes |
decimals | 0-18 | Yes |
maxSupply | Hard cap (0 = unlimited) | Yes |
supplyRule | fixed | mintable | deflationary | Yes |
const result = await window.monerousd.createToken({ name: 'My Token', symbol: 'MTK', decimals: 8, maxSupply: '1000000', supplyRule: 'fixed', description: 'A private fungible token on MoneroUSD', metadataPublic: true, }); console.log('Token address:', result.tokenId); // → "ion1_a7f3b2c1...4e8d9f0a" (69-char BLAKE2b-derived address) console.log('Bond paid:', result.bondPaid, 'USDm');
TOKEN/TOKEN direct pools will be enabled post-launch once sufficient USDm pool depth exists.Shadow NFTs are tokens with maxSupply=1, decimals=0. On MoneroUSD, an NFT output is indistinguishable from any other output — FCMP++ hides the owner, amount, and asset type. There is no ownerOf() function. Ownership is provable only by the holder via an FCMP++ membership proof.
Every secondary sale deducts 200 bps (2%) automatically at the settlement layer. Royalties are routed through the fee split: 50% reserves, 30% POL, 20% bounty. Creator receives royalty at their stealth address (hidden from observers).
// Mint an NFT const nft = await window.monerousd.mintNFT({ name: 'Private Art #1', description: 'View-key-gated digital art', contentHash: 'QmYwAPJzv5CZsnA...', // IPFS hash }); console.log('NFT ID:', nft.nftId); // Later: prove you own it (without revealing identity) const proof = await window.monerousd.proveNFTOwnership({ nftId: nft.nftId, challenge: 'random-challenge-from-verifier', }); // Send `proof` to the verifier. They can confirm // ownership without learning your identity.
Dark Contracts extend MoneroUSD's attestation layer into a programmable virtual machine. You write in DarkSolidity (DSOL) — a Solidity-dialect language with privacy-native types — compile to .dc bytecode, and deploy via a DC_DEPLOY attestation. The contract executes in the DarkVM, a pure-JavaScript BigInt stack machine hosted identically by every indexer operator. Every mutation lands in the same pool_events log the base layer already anchors to the USDm chain.
deriveDarkState pass picks up state diffs deterministically; the explorer decodes arguments against abi.json.dark contract Erc20Private is Ownable { private mapping(stealth => uint64) balances; public string symbol; public uint64 totalSupply; constructor(string sym, uint64 supply) { symbol = sym; totalSupply = supply; balances[msg.sender] = supply; } @batch entry transfer(stealth to, uint64 amount) { require(amount > 0, "AMOUNT_ZERO"); require(to != msg.sender, "SELF_TRANSFER"); require(balances[msg.sender] >= amount, "INSUFFICIENT"); balances[msg.sender] = balances[msg.sender] - amount; balances[to] = balances[to] + amount; emit encrypted Transfer(msg.sender, to, amount); } @direct entry balanceOf(stealth who) returns (uint64 when revealed) { return balances[who]; } }
require guards on transfer are mandatory. amount > 0 rejects no-op gas-burn calls, to != msg.sender rejects no-op self-transfers, and the balance check rejects overdrafts. The compiler does not insert these for you. v1.2.182's production-readiness sweep made them mandatory across every transfer entrypoint in the standard library; any contract that ships without them is a regression.Four language constructs do most of the privacy work:
| Construct | Behaviour |
|---|---|
private field | Pedersen-committed on every write. Never rendered in plaintext by any public API. |
@batch entry | Compiler emits a REQUIRE_COMMIT_REVEAL preamble. A direct DC_CALL without a prior commit is rejected with DC_MUST_COMMIT. |
emit encrypted | Event payload is AES-256-GCM sealed to HKDF(viewkeyHash, contractId, eventName). Only the caller decrypts. |
when revealed | Return type compiles to open-and-prove: auto-generated Bulletproofs++ range proof (576 bytes when BPPP_REAL_SOUNDNESS=1), value encrypted to msg.sender. |
Per call. No per-opcode gas market — deterministic resource ceilings simplify multi-operator consensus.
Opcodes per call. Overrun aborts the call, rolls back state, keeps the fee (DC-2).
Combined stack + heap. Hard cap (DC-3).
Same path as TOKEN_CREATE (DC-10). Routed through the Protocol Fee Split.
| # | Rule | What it means for your contract |
|---|---|---|
| DC-1 | Deterministic execution | Your bytecode must not read clocks, randomness, or float. All math is BigInt. |
| DC-4 | Commit-reveal for private mutations | Any entry that writes a private field must be tagged @batch. The compiler enforces this. |
| DC-7 | Syscall allowlist | Only the frozen host-syscall table is callable. Unknown codes abort with DC_SYSCALL_UNKNOWN. |
| DC-11 | No transparent call path | You cannot mutate contract state through an HTTP route. Every write flows through an attestation. |
| DC-13 | Bytecode immutable | Upgrades flow through a nullifier-gated proxy pattern, not UPDATE. Plan proxies in from day one. |
| DC-14 | Compile-time privacy analysis | Writing a private value into a public sink without when revealed is a compile error. |
| DC-15 | Wallet decodes before approval | Ship a clean abi.json with your contract. Without one, the wallet shows a loud warning and users cannot read what they are signing. |
The full DC-1…DC-15 rulebook is documented in the runtime source and the monerousd-explorer protocol spec (PROTOCOL-DC-V1.md).
Phase 3 of DSOL adds Solidity-familiar constructs:
dark contract Child is Parent1, Parent2 { … }. Parents are linearized left-to-right (C3-style). Child declarations override parents by name; constructor bodies concatenate so parent initialization runs before child initialization.modifier onlyOwner(stealth o) { require(msg.sender == o); _; }. Per-entry invocations expand inline at the callsite; the _; placeholder expands to the original entry body.syscall(EXT_CALL_TAIL_V1, { contractId, entrypoint, argv }). The callee executes with the remaining step budget and no return-to-caller — reentrancy is impossible by construction because there is no caller frame to re-enter.// 1. Compile locally (ships with desktop wallet) const { bytecode, abi } = await window.monerousd.dsol.compile('ErcPrivate.dsol'); // 2. Deploy — bond is debited from USDm balance const deploy = await window.monerousd.dcDeploy({ bytecode, abi, constructorArgs: ['MyToken', 'MTK', '1000000', callerStealth], }); // → { contractId: 'dc1_...', codeHash, bondPaid } // 3. Commit (block N). Wallet hashes (entry, args, salt) and broadcasts. const { commitId } = await window.monerousd.dcCall({ contractId: deploy.contractId, entrypoint: 'transfer', args: [recipientStealth, '25000'], }); // 4. Reveal (block N+2) is automatic once the block clears. // 5. Read public state or encrypted return values any time const res = await fetch(`https://ion.monerousd.org/v1/contracts/${deploy.contractId}`).then(r => r.json()); // res.state.name → "MyToken" (public) // res.state.totalSupply → { commitment, rangeProofHash } (private)
| Resource | Where |
|---|---|
LANGUAGE.md | Inside the desktop wallet package: dark-contracts/LANGUAGE.md. Full syntax, opcodes, and stdlib. |
PROTOCOL-DC-V1.md | Frozen wire-format spec: opcodes, attestation codec, state-root layout, Pedersen H generator bytes. |
| Stdlib | dark-contracts/stdlib/: Erc20Private.dsol, NftCollection.dsol, Ownable.dsol, Pausable.dsol, PausableToken.dsol, ReentrancyGuard.dsol. |
| Compiler CLI | dsolc compile MyContract.dsol → produces MyContract.dc + MyContract.abi.json. |
Two security rules are binding on every DSOL contract that lands in the wallet bundle:
operatorStealth set at deploy may call any function that mutates state attributed to a specific actor, acts as a privileged operator, or modifies another actor's resource. Comments-as-authorization (e.g. // operator-only without the matching require()) are forbidden — that bug class produced two critical findings in the v1.2.182 audit (IonSwapDeposit::register and LimitOrderBook::record_fill, both of which were missing their documented operator gates).Acceptable authorization patterns:
| Pattern | Use case | Example |
|---|---|---|
modifier onlyOwner | Single-owner admin (Ownable inheritance) | pause(), unpause(), NFT collection owner |
require(msg.sender == operatorStealth) | Privileged operator pinned at deploy | IonSwapDeposit::register, LimitOrderBook::record_fill |
require(msg.sender == ownerOf[id]) | Per-resource ownership | NftCollection::transfer |
require(msg.sender == orderOwner[id]) | Per-order ownership | LimitOrderBook::cancel |
require(amount > 0) + require(to != msg.sender) | Universal transfer guards | Erc20Private::transfer, PausableToken::transfer |
entry has a documented authorization model (open to anyone? owner-only? operator-only? stealth-keyed?), and the code matches the documentation.msg.sender appropriately (rule 107).require(amount > 0)).require(to != msg.sender)).require(to != 0) or equivalent).requires).tests/dark-contracts/*.test.js) before merging.Dark Contracts touch the outside world only through SYSCALL. Every syscall in the table below routes to the SAME canonical handler the hardcoded attestation paths use, so a DSOL contract emitting an event produces byte-identical pool_events rows to the legacy attestation flow. DC-8 ("syscall invariants propagate") is satisfied by construction.
| Syscall | opId | Canonical event | Authorization |
|---|---|---|---|
TOKEN_TRANSFER_EMIT_V1 | 0x0101 | token_transfer | caller-debited (srcStealth = ctx.callerStealth) |
TOKEN_MINT_SUPPLY_V1 | 0x0102 | token_supply_mint | backend enforces creator_stealth === callerStealth |
LP_MINT_V1 | 0x0201 | mint_first / mint | creator-only for non-verified asset1; pool-create on demand |
LP_BURN_V1 | 0x0202 | burn | requires (positionId, openAmount, openBlinding) match committed Pedersen opening |
BRIDGE_UNWRAP_EMIT_V1 | 0x0301 | bridge_burn (full unwrap) | caller's indexed balance ≥ amount; address validated; dynamic-fee surge applied |
NFT_MINT_V1 | 0x0401 | nft_mint | mints to caller (the to argv field is ignored) |
NFT_TRANSFER_V1 | 0x0402 | nft_transfer | caller must currently own the item (NFT_NOT_OWNER otherwise) |
READ_BALANCE_V1 | 0x0500 | (read-only) | privacy-preserving: returns commitment hash, never plaintext |
READ_BLOCK_V1 | 0x0501 | (read-only) | open |
EXT_CALL_TAIL_V1 | 0x0601 | (no event) | tail-only; reentrancy impossible by construction |
VERIFY_RANGE_PROOF_BPPP_V1 | 0x0700 | (read-only) | open; charges 100,000 instruction steps per verify |
The VERIFY_RANGE_PROOF_BPPP_V1 syscall lets DSOL contracts validate Bulletproofs++ range proofs that prove a committed value lies in [0, 264) without revealing the value. The verifier runs entirely on-chain (consuming the per-call instruction budget) so contracts can gate state transitions on cryptographic range guarantees.
// argv: { commitmentHex: "<64-char hex>", // proofHex: "<1088-char hex = 544 bytes>", // Nd?: 16, Np?: 16, // transcriptLabel?: "BPPP-V1-..." } // // returns: { valid: bool, soundnessMode: "real"|"soft", Nd, Np } let argv = abi.encode({ commitmentHex, proofHex }); let result = syscall(VERIFY_RANGE_PROOF_BPPP_V1, argv); require(result.valid, "RANGE_PROOF_INVALID");
The proof is 544 bytes for the canonical 64-bit range. Step-cost is fixed at 100,000 (10% of the 1,000,000-instruction per-call budget per DC-2) — heavy enough to deter contracts that loop on verification, cheap enough that ~10 verifications per call are economically reasonable.
Activation flag: when BPPP_REAL_SOUNDNESS=1 is set in the indexer's environment, pedersen.js::rangeProof() returns 576 bytes (32 commitment + 544 BP++ proof) instead of the legacy 96-byte SHA-512 placeholder. The wire-shape difference is intentional — silent soundness downgrade is impossible because consumers immediately fail on length mismatch. Operators flip the flag once they have validated their indexer ships with the real verifier; the live VPS has it set as of v1.2.182.
Ion Swap is the private batch AMM for MoneroUSD. Every swap goes through a commit-reveal cycle — no transparent path exists.
| Endpoint | Method | Description |
|---|---|---|
/api/pools | GET | List all pools with reserves, volume, fees |
/api/pool/create | POST | Create a new TOKEN/USDm pool |
/api/batch/commit | POST | Submit a commit hash for a swap |
/api/batch/reveal | POST | Reveal swap params after 2 blocks |
/api/orders/submit | POST | Submit encrypted dark-pool limit order |
/api/config | GET | Protocol configuration (fee rates, committee key, etc.) |
Every fee collected routes through a non-configurable split, enforced at the settlement layer:
| Source | Fee | Reserves | POL | Bounty |
|---|---|---|---|---|
| Standard swap | 30 bps | 60% | 30% | 10% |
| Stable-pair swap | 5 bps | 60% | 30% | 10% |
| Dark-pool match | 20 bps | 60% | 30% | 10% |
| Token creation bond | Flat | 60% | 30% | 10% |
| NFT royalty | 200 bps | 50% | 30% | 20% |
| Bridge wrap/unwrap | 10 bps | 70% | 20% | 10% |
Wallets and block explorers can render a canonical, deterministic activity feed for any set of stealth subaddresses via the indexer's /v1/activity endpoint. The feed is derived directly from pool_events and includes every token transfer, bridge mint, and bridge burn involving the queried addresses — not just outbound transactions the local wallet-rpc happens to know about.
| Endpoint | Method | Description |
|---|---|---|
/v1/activity?addrs=<csv> | GET | Returns up to the most recent N activity rows (default 200) involving any of the supplied stealth subaddresses. |
{
"txHash": "ca758d7c...",
"blockHeight": 142850,
"ts": 1714000000000,
"direction": "in", // "in" | "out"
"eventType": "token_transfer", // "token_transfer" | "bridge_mint" | "bridge_burn"
"tokenId": "ion1_f7f71a00...",
"symbol": "Sats",
"decimals": 8,
"logoUrl": "/api/token-logo/ion1_f7f71a00...",
"amount": "10000000000", // atomic units, as a string (BigInt-safe)
"counterpartyStealth": "Mp6mHy..." // may be null if out-of-view
}
logoUrl field as your source of truth. It is served by the indexer and tracks whatever the token creator uploaded; the client does not need a separate logo registry. The symbol and decimals fields are resolved from the backend asset registry, so the feed is usable even when the local wallet has never seen the token before.For server-side integration, use the wallet-rpc JSON-RPC interface. Any language that can make HTTP requests works.
| Service | Default Port | Purpose |
|---|---|---|
| USDm daemon RPC | 17750 | Chain queries, block templates, tx broadcast |
| Wallet RPC | 27750 | Balance, transfers, address generation |
| Ion Swap API | 7777 | Pools, swaps, tokens, orders |
| Ion Swap WebSocket | 7778 | Real-time block ticks, batch events |
| Explorer | 8081 | Block/tx/token display |
curl -s http://127.0.0.1:27750/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"get_balance",
"params":{"asset_type":"USDm"}}' \
-H "Content-Type: application/json"
curl -s http://127.0.0.1:27750/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"transfer",
"params":{"destinations":[{"address":"Mp6mH...","amount":100000000}],
"asset_type":"USDm"}}' \
-H "Content-Type: application/json"
Every dApp gets a fresh subaddress. Your main account stays hidden. Never reuse addresses across dApps.
All amounts are Pedersen-committed with Bulletproofs+ range proofs. Observers see commitments, never values.
Every swap is hash-committed before reveal. Front-running is cryptographically impossible.
Full-chain membership proofs. Every output in the chain is a decoy — no ring size limitation.
tor-detect.js pattern to detect Tor Browser and .onion routing. Offer clearnet-free operation.// Connect wallet and execute a private swap via Ion Swap const { address } = await window.monerousd.connect(); const balance = await window.monerousd.getBalance({ asset_type: 'USDm' }); // Get a quote from Ion Swap const quote = await fetch('https://ion.monerousd.org/api/quote', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ from: 'USDm', to: 'wBTC', amountIn: '1000' }), }).then(r => r.json()); // Commit the swap (hash-hiding, MEV-free) const salt = crypto.getRandomValues(new Uint8Array(32)); // ... commit and reveal flow via batch-client.js
// Create a token and auto-create a pool on Ion Swap const token = await window.monerousd.createToken({ name: 'Community Token', symbol: 'CMT', decimals: 8, maxSupply: '10000000', supplyRule: 'fixed', }); // token.tokenId → "ion1_..." (69-char unique address) // Create a CMT/USDm pool with initial liquidity await fetch('https://ion.monerousd.org/api/pool/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ asset0: 'CMT', asset1: 'USDm', amount0: '5000000', amount1: '10000', }), });
// Server-side: create an invoice via Ion Swap invoicing API const invoice = await fetch('https://ion.monerousd.org/api/invoices/create', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ amount: '50.00', currency: 'USDm', acceptedAssets: ['USDm', 'wBTC', 'wXMR'], merchantAddress: 'Mp6mH...', memo: 'Order #12345', }), }).then(r => r.json()); // Payer sends any accepted asset; Ion Swap auto-converts // to USDm and delivers to merchant's stealth subaddress.
MoneroUSD is open source. Build with privacy as the default.