
Private amounts. Public rules.
Run a confidential token from start to finish, in this page. Arcantis keeps every amount encrypted, Vigilantis enforces the issuer's rules on every transfer, and an auditor can still read it all.
Ten steps, one token.
Every step is a real devnet transaction. The page makes throwaway keys and keeps them in this browser: nothing to install, no wallet extension. Devnet SOL is free, and the test tokens have no value.
-
01 · Arcantis
Private amounts
A Token-2022 dollar whose balances and transfer amounts are encrypted on-chain. Zero-knowledge proofs show each transfer is valid without revealing it.
-
1
Get devnet SOL
DoneYour throwaway wallet pays the fees and rent for everyone in this run, about 0.05 SOL.
The call
await rpc.requestAirdrop(you.address, lamports(1_000_000_000n)).send(); // refused? get devnet SOL at faucet.solana.com -
2
Create the token
DoneOne mint carries everything: confidential transfers with an auditor key, and Vigilantis as its transfer hook, freeze authority and pause authority. Its policy turns the blocklist on.
The calls
// Token-2022 extensions: ConfidentialTransferMint { auditorElgamalPubkey }, // TransferHook { Vigilantis }, PausableConfig { Vigilantis }; freeze authority: Vigilantis const auditorKeypair = await deriveAuditorElgamalKeypair(auditor); // @softseco/arcantis const auditorElgamalPubkey = getAuditorElgamalPubkey(auditorKeypair); // the rules, with @softseco/vigilantis await vigilantis.initializeExtraAccountMetaList(mint); await vigilantis.initializePolicy({ mint, allowlist: false, blocklist: true, maxTransferAmount: 0, allowConfidential: true }); -
3
Open accounts and deposit
DoneYou, Bob and Mallory each get a token account set up for confidential transfers. You mint 1,000 test dollars and move them into your confidential balance. From here on the chain holds only ciphertext for it.
The SDK calls
await configureAccount({ rpc, rpcSubscriptions, payer: you, owner: bob, mint }); await deposit({ rpc, rpcSubscriptions, payer: you, owner: you, mint, amount: 1_000_000_000n, decimals: 6 }); await applyPendingBalance({ rpc, rpcSubscriptions, payer: you, owner: you, mint }); -
4
Pay Bob privately
DoneThe amount is encrypted three times, to you, to Bob and to the auditor. Token-2022 calls Vigilantis on the way, and Vigilantis checks who sends and who receives. Bob needs no SOL: you pay the fees.
The SDK call
await transfer({ rpc, rpcSubscriptions, payer: you, owner: you, mint, destinationOwner: bob.address, amount: 250_000_000n, auditorElgamalPubkey }); // the SDK finds the hook's accounts by itself -
5
See who can read it
DoneThree keys can open the amount. Everyone else sees two addresses and a ciphertext.
The SDK calls
await decryptBalance({ rpc, owner: you, mint }); // you await decryptBalance({ rpc, owner: bob, mint }); // Bob await decryptTransferAmountAsAuditor({ rpc, signature, auditorKeypair, mint }); // the auditor -
6
Withdraw to public
DoneBob moves half of what he received to his public balance. He proves he has enough without showing how much he had.
The SDK call
await withdraw({ rpc, rpcSubscriptions, payer: you, owner: bob, mint, amount: 125_000_000n, decimals: 6 }); -
02 · Vigilantis
Public rules
You are the issuer. Each action below records a reason code and a case reference on-chain. The case number itself stays private: the chain stores a salted hash of it. None of these actions can move anyone's tokens.
-
7
Block a wallet
DoneMallory goes on the blocklist for sanctions, case DEMO-001. Then you try to pay her 50.00, confidentially. Vigilantis cannot see the amount, but it can see who gets paid.
The SDK calls
const salt = process.env.VIGILANTIS_CASE_SALT; // the issuer's secret await vigilantis.blockWallet(mint, mallory, { reasonCode: ENFORCEMENT_REASONS.sanctions, caseRef: caseRef("DEMO-001", salt) }); // or: vigilantis block add <mint> <wallet> --reason sanctions --case DEMO-001 await transfer({ ..., destinationOwner: mallory.address }); // refused: RecipientBlocked -
8
Freeze an account
DoneBob's account is frozen for fraud, case DEMO-002, and he tries to pay you 10.00 from his public balance. Then the case is closed and his account is thawed. His balance never moves.
The SDK calls
await vigilantis.freezeTokenAccount(mint, bobToken, { reasonCode: ENFORCEMENT_REASONS.fraud, caseRef: caseRef("DEMO-002", salt) }); await vigilantis.transfer({ mint, amount: 10_000_000n, decimals: 6, owner: bob, destinationOwner: you }); // refused: AccountFrozen // a confidential payment Arcantis refuses before it builds the proofs await vigilantis.thawTokenAccount(mint, bobToken, { reasonCode: ENFORCEMENT_REASONS.caseClosed, caseRef: caseRef("DEMO-002", salt) }); -
9
Pause the token
DoneAn internal review, case DEMO-003, pauses every transfer of the token at once, confidential ones included. Bob tries to pay you 10.00, then the review ends and transfers resume.
The SDK calls
await vigilantis.pause(mint, { reasonCode: ENFORCEMENT_REASONS.internalReview, caseRef: caseRef("DEMO-003", salt) }); await vigilantis.transfer({ mint, amount: 10_000_000n, decimals: 6, owner: bob, destinationOwner: you }); // refused: MintPaused await vigilantis.resume(mint, { reasonCode: ENFORCEMENT_REASONS.caseClosed, caseRef: caseRef("DEMO-003", salt) }); -
03 · Oversight
The auditor's report
The auditor key opens every amount of this token, and nothing else. A supervisor or an accountant gets the full picture without the amounts ever being public.
-
10
Build the report
DoneEvery deposit, transfer and withdraw of the token, read from the chain and decrypted with the auditor's key. Download it as CSV or JSON.
The SDK calls
const activity = await listConfidentialActivity({ rpc, mint, auditorKeypair }); confidentialActivityToCsv(activity, { decimals: 6 }); // or: arcantis audit-report --mint <mint> --format json
Every amount stayed private. Every rule held.
The chain enforced the issuer's rules on payments nobody else could read, each with its reason on record, and the auditor still saw all of it.
Build on it.
Arcantis and Vigilantis are source-available. Payments, payroll or treasury on Solana: open an issue, or write to hello@softseco.com.