Arkade$ Assets
Issue, send, and explore custom assets on Bitcoin. Stablecoins, loyalty points, real-world assets — the same wallet, the same transactions, with asset balances attached. No sidechain. No bridge.
Arkade Assets let anyone issue custom tokens — stablecoins, loyalty points, real-world assets — directly on Bitcoin. They travel with regular Bitcoin transactions using OP_RETURN outputs encoded in TLV format with ARK magic bytes 0x41524b. Recipients need only an Arkade address.
Every asset gets a permanent ID at genesis: (txid, group_index). Metadata — name, ticker, decimals, icon — is committed via BIP-341 Merkle root and cannot be changed after issuance.
First issuance. AssetId assigned. Metadata set here is immutable.
Optional. Holder controls supply. Can reissue more. Non-transitive — one level only.
Transfers travel with BTC txs. Recipient needs only an Arkade address.
Burn the control asset to permanently freeze total supply. Irreversible.
Transfers bridge offchain VTXOs to onchain outputs via an intent system: Old Asset VTXO → [Intent TX] → [Commitment TX] → New VTXOs / Onchain outputs. A single intent can split an asset across multiple destinations in one operation — useful for batch payouts and airdrops.
import { MnemonicIdentity, Wallet } from '@arkade-os/sdk';
const wallet = await Wallet.create({
identity: MnemonicIdentity.fromMnemonic(
process.env.ARKADE_MNEMONIC
),
arkServerUrl: 'https://mutinynet.arkade.sh',
});
const result = await wallet.assetManager.issue({
amount: 1000000,
metadata: {
name: 'My Asset',
ticker: 'MYA',
decimals: 8,
},
});
// result.assetId — permanent asset identifier
// result.arkTxId — Arkade transaction ID
Name, ticker, decimals, and icon are committed at genesis using a BIP-341 tagged Merkle hash. Once issued, these fields cannot be updated. Verify everything before issuing on mainnet.
const txId = await wallet.assetManager.send({
assetId: 'ark1assetabc...',
amount: 1000,
to: 'ark1qrecipient...',
});
console.log('Tx:', txId);
Docs: Send Assets ↗
A single intent transaction can split an asset across multiple destinations — useful for batch payouts, airdrops, or splitting payments between multiple parties simultaneously.
const balances = await wallet.assetManager.balances();
// Returns: Record<assetId, bigint>
for (const [id, amount] of Object.entries(balances)) {
console.log(id, amount.toString());
}
Docs: Check Balance ↗
Your balance is the sum of all unspent Virtual Transaction Outputs (VTXOs) holding your asset. VTXOs are offchain outputs managed by the Arkade operator. They can be redeemed to base-layer Bitcoin via unilateral exit at any time.
// Reissue — requires holding the control asset
const result = await wallet.assetManager.issue({
amount: 500000,
controlAssetId: 'ark1asset_control...',
metadata: {}, // metadata unchanged at reissue
});
Control assets only work one level deep. A control asset cannot delegate control to another asset — no nested supply hierarchies. The controller is designated at genesis only and cannot be changed.
Run a full local environment with Nigiri — same API as testnet, zero cost.
Get it working
Step-by-step from zero to issuing assets on mainnet. Full doc also at arkadeinfra.md in the Arkade$ repo.
-
1
Install Nigiri:
curl https://getnigiri.vulpem.com | bash -
2
Run:
nigiri start --ark→ Arkade atlocalhost:7070 -
3
npm install @arkade-os/sdk - 4 Create wallet, issue test asset, verify AssetId returned
- 5 Send test asset to second address, check balance
-
1
Set
arkServerUrl: 'https://mutinynet.arkade.sh' - 2 Fund wallet with Mutinynet faucet test sats
- 3 Issue asset — verify on Mutinynet explorer ↗
- 4 Test reissuance and supply cap (burn control asset)
- 5 Test send/receive between two wallets
ARKADE_MNEMONIC — never in code