Developer Guide

Build on MoneroUSD — provider API, tokens, NFTs, swaps, and privacy-first integration

Contents

  1. Getting Started
  2. Provider API Reference
  3. Token Creation (USDm-T1)
  4. Shadow NFTs (USDm-N1)
  5. Dark Contracts (DarkSolidity)
  6. Ion Swap Pool Integration
  7. Activity API (v1)
  8. Wallet-RPC Reference
  9. Privacy Best Practices
  10. Example Projects

1. Getting Started

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');
}
Key difference from Ethereum: Every dApp gets a fresh per-origin stealth subaddress. Your main account address is never exposed. The wallet generates a unique subaddress for each domain via ~/.monerousd/dapp-origins.json.

Desktop Wallet

Download from monerousd.org. Provider is auto-injected into the built-in dApp browser.

Browser Extension

Chrome/Firefox extension injects window.monerousd into any website via a companion auth proxy on port 27751.

Language

JavaScript (frontend). Any language for backend via wallet-rpc JSON-RPC.

Privacy

FCMP++ at consensus layer. Amounts, senders, recipients, and asset types are all hidden by default.

2. Provider API Reference

The window.monerousd provider exposes read and write methods. Write methods trigger a biometric/passphrase approval popup in the wallet.

Connection

MethodReturnsGateDescription
connect(){ address }Consent popupInitiates connection. Shows a consent sheet listing requested permissions, then delegates to the wallet's approval popup.
isConnected()booleanNoneReturns true if the dApp is currently connected.
disconnect()voidNoneTears down the connection for this origin.
getAddress()stringNoneReturns the per-origin stealth subaddress.

Balance & Assets

MethodReturnsGateDescription
getBalance({ asset_type }){ balance, unlocked_balance }NoneReturns atomic balance for a specific asset. Default: 'USDm'.
listAssets()[{ id, name, symbol, decimals, balance, isNFT }]NoneLists all assets held by the wallet.
getAssetBalance(assetId){ balance, unlocked_balance, asset_type }NoneReturns balance for a specific asset ID.

Transfers

MethodReturnsGateDescription
transfer({ destinations, asset_type, memo }){ txid, fee }BiometricSends USDm or any asset. Each destination: { address, amount }.
transferNFT({ nftId, toAddress }){ txId }BiometricTransfers an NFT to another address.

Token & NFT Creation

MethodReturnsGateDescription
createToken({ name, symbol, maxSupply, decimals, supplyRule }){ tokenId, creationTxId, bondPaid }BiometricCreates a new USDm-T1 fungible token. Bond is non-refundable.
mintTokenSupply({ tokenId, amount }){ txId, feePaid }BiometricMints additional supply for mintable tokens (50 bps fee).
mintNFT({ name, description, contentHash }){ nftId, creationTxId, bondPaid }BiometricCreates a Shadow NFT (USDm-N1). Bond: 10+ USDm.

Proofs & View Keys

MethodReturnsGateDescription
proveOwnership({ assetId, challenge }){ proof }Spend keyGenerates an FCMP++ membership proof proving you own the asset.
proveNFTOwnership({ nftId, challenge }){ proof }Spend keyGenerates an FCMP++ membership proof for a specific NFT.
shareViewKey({ scope, id, recipientPubKey }){ encryptedViewKey }BiometricShares a read-only view key with a specific recipient.

Events

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));

3. Token Creation (USDm-T1)

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.

Token Addresses (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.

PropertyValueNotes
Prefixion1_Version byte 1 for future-proofing
HashBLAKE2b-512 → 256 bitsCryptoNote-native, same as key derivation
Length69 characters5-char prefix + 64 hex digits
Verified tokensDeterministicBLAKE2b("MoneroUSD_protocol_asset:" + symbol)
Custom tokensStealth-derivedBLAKE2b(creatorStealth + randomSalt)
Privacy guarantee: Custom token addresses are derived from the creator’s stealth key + random salt. The address reveals neither the creator’s identity nor the token name. Multiple tokens by the same creator have unlinked addresses. Token addresses are searchable and copyable in the block explorer.

Token Properties

PropertyConstraintsImmutable
nameMax 32 charactersYes
symbol3-6 alphanumeric charactersYes
decimals0-18Yes
maxSupplyHard cap (0 = unlimited)Yes
supplyRulefixed | mintable | deflationaryYes

Creation Bond

Token Creation Bond
bond_usdm = 100 × (1 + active_tokens / 100) + metadata_bytes × 0.001
Non-refundable. The bond is burned through the Protocol Fee Split: 60% to reserves, 30% to POL, 10% to bounty. This prevents spam while directly strengthening USDm backing.

Supply Rules

Fixed
All supply minted at creation. No further minting.
Mintable
Creator can mint more (50 bps fee each time) up to maxSupply.
Deflationary
All minted at creation. Burns only after that.

Example: Create a Token

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');

USDm Pairing Rule

V1 constraint: All custom token pools must pair against USDm. This forces every trade to route through USDm, concentrating liquidity, maximizing fee capture, and making USDm the settlement layer for the entire ecosystem. TOKEN/TOKEN direct pools will be enabled post-launch once sufficient USDm pool depth exists.

4. Shadow NFTs (USDm-N1)

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.

Mint Bond

NFT Mint Bond
nft_bond = 10 + metadata_bytes × 0.001 USDm

Royalties

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

Example: Mint and Prove Ownership

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

5. Dark Contracts (DarkSolidity)

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.

When to use Dark Contracts vs. the base layer. Token creation, LP mint/burn, NFT mint, bridge wrap/unwrap, limit order, DCA, and savings are all first-class base-layer operations — reach them through the provider API for the lowest fees and fastest confirmation. Reach for Dark Contracts when you need custom logic: a private ERC-20 analogue with a non-standard transfer rule, a blind auction, a pausable wrapper, a cross-contract router, a DSOL-native DAO. The base-layer ops can be invoked from inside a Dark Contract via the syscall table.

5.1 The life of a Dark Contract call

dsolc compile
DC_DEPLOY
DC_CALL_COMMIT
DC_CALL_REVEAL
DarkVM executes
Deploy once, then commit at block N and reveal at block N+2. The indexer’s deriveDarkState pass picks up state diffs deterministically; the explorer decodes arguments against abi.json.

5.2 A production-ready private ERC-20 in DSOL

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];
  }
}
The three 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:

ConstructBehaviour
private fieldPedersen-committed on every write. Never rendered in plaintext by any public API.
@batch entryCompiler emits a REQUIRE_COMMIT_REVEAL preamble. A direct DC_CALL without a prior commit is rejected with DC_MUST_COMMIT.
emit encryptedEvent payload is AES-256-GCM sealed to HKDF(viewkeyHash, contractId, eventName). Only the caller decrypts.
when revealedReturn type compiles to open-and-prove: auto-generated Bulletproofs++ range proof (576 bytes when BPPP_REAL_SOUNDNESS=1), value encrypted to msg.sender.

5.3 Resource discipline

Flat fee

0.1 USDm

Per call. No per-opcode gas market — deterministic resource ceilings simplify multi-operator consensus.

Step limit

1,000,000

Opcodes per call. Overrun aborts the call, rolls back state, keeps the fee (DC-2).

Memory

256 KB

Combined stack + heap. Hard cap (DC-3).

Deployment bond

Non-refundable

Same path as TOKEN_CREATE (DC-10). Routed through the Protocol Fee Split.

5.4 Key invariants to design around

#RuleWhat it means for your contract
DC-1Deterministic executionYour bytecode must not read clocks, randomness, or float. All math is BigInt.
DC-4Commit-reveal for private mutationsAny entry that writes a private field must be tagged @batch. The compiler enforces this.
DC-7Syscall allowlistOnly the frozen host-syscall table is callable. Unknown codes abort with DC_SYSCALL_UNKNOWN.
DC-11No transparent call pathYou cannot mutate contract state through an HTTP route. Every write flows through an attestation.
DC-13Bytecode immutableUpgrades flow through a nullifier-gated proxy pattern, not UPDATE. Plan proxies in from day one.
DC-14Compile-time privacy analysisWriting a private value into a public sink without when revealed is a compile error.
DC-15Wallet decodes before approvalShip 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).

5.5 Inheritance, modifiers, and tail-calls (Phase 3)

Phase 3 of DSOL adds Solidity-familiar constructs:

5.6 Deploying and calling a contract

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

5.7 Language reference

ResourceWhere
LANGUAGE.mdInside the desktop wallet package: dark-contracts/LANGUAGE.md. Full syntax, opcodes, and stdlib.
PROTOCOL-DC-V1.mdFrozen wire-format spec: opcodes, attestation codec, state-root layout, Pedersen H generator bytes.
Stdlibdark-contracts/stdlib/: Erc20Private.dsol, NftCollection.dsol, Ownable.dsol, Pausable.dsol, PausableToken.dsol, ReentrancyGuard.dsol.
Compiler CLIdsolc compile MyContract.dsol → produces MyContract.dc + MyContract.abi.json.

5.8 Production-readiness security model (v1.2.182)

Two security rules are binding on every DSOL contract that lands in the wallet bundle:

Rule 107 — caller-authorization on every state-mutating entrypoint. Nobody other than the owner, the scoped stealth address, or the explicit 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:

PatternUse caseExample
modifier onlyOwnerSingle-owner admin (Ownable inheritance)pause(), unpause(), NFT collection owner
require(msg.sender == operatorStealth)Privileged operator pinned at deployIonSwapDeposit::register, LimitOrderBook::record_fill
require(msg.sender == ownerOf[id])Per-resource ownershipNftCollection::transfer
require(msg.sender == orderOwner[id])Per-order ownershipLimitOrderBook::cancel
require(amount > 0) + require(to != msg.sender)Universal transfer guardsErc20Private::transfer, PausableToken::transfer
Rule 108 — mandatory security audit before bundling. Every contract change runs through this checklist before it's compiled into the wallet binary:

5.9 Host syscall reference

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.

SyscallopIdCanonical eventAuthorization
TOKEN_TRANSFER_EMIT_V10x0101token_transfercaller-debited (srcStealth = ctx.callerStealth)
TOKEN_MINT_SUPPLY_V10x0102token_supply_mintbackend enforces creator_stealth === callerStealth
LP_MINT_V10x0201mint_first / mintcreator-only for non-verified asset1; pool-create on demand
LP_BURN_V10x0202burnrequires (positionId, openAmount, openBlinding) match committed Pedersen opening
BRIDGE_UNWRAP_EMIT_V10x0301bridge_burn (full unwrap)caller's indexed balance ≥ amount; address validated; dynamic-fee surge applied
NFT_MINT_V10x0401nft_mintmints to caller (the to argv field is ignored)
NFT_TRANSFER_V10x0402nft_transfercaller must currently own the item (NFT_NOT_OWNER otherwise)
READ_BALANCE_V10x0500(read-only)privacy-preserving: returns commitment hash, never plaintext
READ_BLOCK_V10x0501(read-only)open
EXT_CALL_TAIL_V10x0601(no event)tail-only; reentrancy impossible by construction
VERIFY_RANGE_PROOF_BPPP_V10x0700(read-only)open; charges 100,000 instruction steps per verify

5.10 Bulletproofs++ verification (v1.2.182)

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.

6. Ion Swap Pool Integration

Ion Swap is the private batch AMM for MoneroUSD. Every swap goes through a commit-reveal cycle — no transparent path exists.

Swap Lifecycle

Block N
commit = H(pool, side, amt, minOut, salt)
POST /api/batch/commit
Block N+2
POST /api/batch/reveal (params + salt)
Uniform-price batch clearing
All reveals in a batch clear at the same price. MEV is cryptographically impossible.

API Endpoints

EndpointMethodDescription
/api/poolsGETList all pools with reserves, volume, fees
/api/pool/createPOSTCreate a new TOKEN/USDm pool
/api/batch/commitPOSTSubmit a commit hash for a swap
/api/batch/revealPOSTReveal swap params after 2 blocks
/api/orders/submitPOSTSubmit encrypted dark-pool limit order
/api/configGETProtocol configuration (fee rates, committee key, etc.)

Protocol Fee Split

Every fee collected routes through a non-configurable split, enforced at the settlement layer:

SourceFeeReservesPOLBounty
Standard swap30 bps60%30%10%
Stable-pair swap5 bps60%30%10%
Dark-pool match20 bps60%30%10%
Token creation bondFlat60%30%10%
NFT royalty200 bps50%30%20%
Bridge wrap/unwrap10 bps70%20%10%

7. Activity API (v1)

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.

EndpointMethodDescription
/v1/activity?addrs=<csv>GETReturns up to the most recent N activity rows (default 200) involving any of the supplied stealth subaddresses.

Response row shape

{
  "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
}
Use the 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.

8. Wallet-RPC Reference

For server-side integration, use the wallet-rpc JSON-RPC interface. Any language that can make HTTP requests works.

Endpoints

ServiceDefault PortPurpose
USDm daemon RPC17750Chain queries, block templates, tx broadcast
Wallet RPC27750Balance, transfers, address generation
Ion Swap API7777Pools, swaps, tokens, orders
Ion Swap WebSocket7778Real-time block ticks, batch events
Explorer8081Block/tx/token display

Example: Query Balance via curl

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"

Example: Send Transfer via curl

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"

9. Privacy Best Practices

Stealth Addresses

Per-origin

Every dApp gets a fresh subaddress. Your main account stays hidden. Never reuse addresses across dApps.

Pedersen Commitments

Hidden Amounts

All amounts are Pedersen-committed with Bulletproofs+ range proofs. Observers see commitments, never values.

Commit-Reveal

MEV-Free

Every swap is hash-committed before reveal. Front-running is cryptographically impossible.

FCMP++

Full Chain

Full-chain membership proofs. Every output in the chain is a decoy — no ring size limitation.

Guidelines for dApp Developers

10. Example Projects

Minimal Swap dApp

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

Token Launcher

// 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',
  }),
});

Merchant Payment Gateway

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