Skip to main content
Version: 0.17 (unstable)

Network Accounts

A network account is a public account that the network can transact against on the owner's behalf — no client needs to be online. When a note is addressed to a network account, the node's network transaction (NTX) builder executes the consuming transaction and commits the resulting state change. This is how you build always-available onchain contracts: counters, faucets, order books, and other components that must react to incoming notes without a user driving them.

Two sides have to line up for network execution to happen:

  • The account opts in by carrying the standardized note-allowlist storage slot, added through the AuthNetworkAccount auth component.
  • The note targets the account by carrying a NetworkAccountTarget attachment.

If a note's script root is allowlisted and its fee can be estimated by the account's active fee policy, the network can consume it automatically.

What makes an account a network account​

AuthNetworkAccount writes a standardized StorageMap slot named miden::standards::auth::network_account::allowed_note_scripts. Off-chain services and the node's NTX builder treat the presence of that slot as the signal that an account is a network account. The slot holds a note allowlist: the set of note script roots the account is willing to consume. A note whose script root is not in the allowlist is rejected during authentication.

The component also holds a second allowlist of permitted transaction script roots (miden::standards::auth::network_account::allowed_tx_scripts). AuthNetworkAccount::new includes the canonical expiration script required by the network transaction builder. Any additional custom transaction script — for example a scripted deploy or interaction — must be allowlisted explicitly.

Both allowlists can be updated after deployment through the network-account configuration note. Those mutations must be protected by an owner- or RBAC-controlled Authority; auth-controlled authority is unsafe because network authentication is intentionally permissionless for allowlisted inputs. The account example below installs owner-controlled access for this purpose.

Prerequisites​

  • The account must be AccountType::Public. A private account cannot be a network account.
  • You need the script root of every application note type the account should accept, computed from the compiled note script. Rust's AuthNetworkAccount::new accepts an empty application set because it adds the standard configuration, fee-sponsorship, and P2ID scripts. It also installs BasicWallet. The Web SDK helper requires at least one application NoteScriptFee.
  • You need a FeePolicyManager, the chain's native fee-faucet ID, and an active policy that can price every allowed note script. A zero fee is valid but must still be scheduled explicitly by BasicConstantFeePolicy. The node will not execute notes for a network account configured with a different fee asset.

Building a network account​

Compile each application note script and read its MAST root before building the account. The standardized allowlist storage installed by AuthNetworkAccount is what marks the account as a network account. Compile with the client's code builder:

use std::collections::BTreeSet;

use miden_client::account::{
AccountBuilder, AccountType,
component::{
AccessControl, AuthNetworkAccount, BasicConstantFeePolicy, FeePolicyManager,
},
};
use miden_client::asset::AssetAmount;
use miden_client::note::{FeeSponsorshipNote, NetworkAccountConfigNote, P2idNote};

let note_script = client.code_builder().compile_note_script(note_code)?;
let note_script_root = note_script.root();

If the note script calls into the account's own procedures (as the counter example does), link the contract module first so the script compiles — for example client.code_builder().with_linked_module("external_contract::counter_contract", counter_code)?.compile_note_script(note_code)?.

Build an active fee policy, pass it to AuthNetworkAccount, then install every component the auth bundle yields:

let fee_policy = BasicConstantFeePolicy::new()
.with_fee(note_script_root, AssetAmount::ZERO)
.with_fee(P2idNote::script_root(), AssetAmount::ZERO)
.with_fee(
NetworkAccountConfigNote::script_root(),
AssetAmount::ZERO,
)
.with_fee(FeeSponsorshipNote::script_root(), AssetAmount::ZERO);
let header = client.get_latest_block_header().await?;
let protocol_config = client
.get_protocol_config(header.protocol_config_commitment())
.await?;
let fee_policy_manager = FeePolicyManager::builder()
.fee_faucet_id(protocol_config.fee_asset_id().faucet_id())
.active_fee_policy(fee_policy.into())
.build();
let auth = AuthNetworkAccount::new(
BTreeSet::from([note_script_root]),
fee_policy_manager,
)?;

let account = AccountBuilder::new(init_seed)
.account_type(AccountType::Public)
.with_component(counter_component)
.with_components(auth)
.with_components(AccessControl::Ownable2Step { owner: owner_id })
.build()?;

If the initial deployment uses a custom transaction script, you must also allowlist that script's root, or the network auth procedure rejects the transaction. After deployment, users interact by sending notes; the public RPC rejects user-submitted transactions that directly update an existing network account.

In that case, replace the earlier let auth = ... construction with this one:

let tx_script = client.code_builder().compile_tx_script(deploy_script_code)?;

let auth = AuthNetworkAccount::new(
BTreeSet::from([note_script_root]),
fee_policy_manager,
)?
.with_allowed_tx_scripts(BTreeSet::from([tx_script.root()]));

Deploying a network account​

Building the account and adding it to the client store is not enough to register it onchain — an account only exists to the network once a committed transaction has advanced its state (nonce 0 → 1). Submit a transaction against it to deploy it.

An empty, scriptless transaction cannot register the account, even on a zero-fee chain. Network authentication requires an input note, an output note, or an account state change before fee payment. A custom deployment script must be allowlisted as above and cause one of those effects.

One way to bootstrap the account is to consume a P2ID note carrying enough of the native fee asset for the first transaction. AuthNetworkAccount::new supplies its allowlist entry and receiving component; the policy above prices P2ID at zero. Consuming that note deploys and funds the account. The example assumes funding_note is a committed Note targeted at this account, available with its inclusion proof after synchronization.

use miden_client::transaction::TransactionRequestBuilder;

client.add_account(&account, false).await?;

let tx_id = client
.submit_new_transaction(
account.id(),
TransactionRequestBuilder::new().build_consume_notes(vec![funding_note])?,
)
.await?;

client.sync_state().await?; // repeat until `tx_id` is committed

Once the deploy transaction is committed, the network watches the account and will consume any allowlisted note addressed to it.

The network transactions tutorial uses a P2ID funding note. Its initial funding consumption publishes the network account with count zero. Subsequent increments come from notes consumed by the network transaction builder.

Inspecting a network account​

NetworkAccount is a validation wrapper that confirms an Account is public, carries a valid non-empty note allowlist, and allows the canonical expiration transaction script. Use it to check an account you fetched or built:

use miden_client::account::component::NetworkAccount;

let network_account = NetworkAccount::try_from(account)?;
let allowed = network_account.allowed_notes();

Sending a note to a network account​

A note is executed by the network when it carries a NetworkAccountTarget attachment and its script root is in the target account's allowlist. Both Rust and TypeScript can create these notes — this is the part of the flow available to web integrators.

TypeScript​

The Web SDK builds and submits a network note in one call. It creates a public, custom-script note carrying the required NetworkAccountTarget attachment, so the target network account auto-consumes it:

const { txId, note } = await client.transactions.createNetworkNote({
account: senderAccountId, // account that creates, funds, and submits the note
target: networkAccountId, // the network account the note targets
script: counterNoteScript, // custom consumption script (or pass a `recipient`)
inputs: [/* note inputs the script reads */],
assets: [/* optional assets locked into the note */],
});

// `note.isNetworkNote()` is true; the network account will consume it.

Use buildNetworkNote(...) if you want the built note without submitting it.

The Web SDK can also build and deploy the account. Pair every application script with its fee, pass the chain's fee-faucet ID after syncing, and install all returned components. Deploy by consuming a committed P2ID funding note as described above; fundingNoteId below identifies that note.

import {
AccountBuilder,
AccountComponent,
AccountStorageMode,
NoteScriptFee,
} from "@miden-sdk/miden-sdk";

const networkAuth = AccountComponent.createNetworkAuthComponents(
[new NoteScriptFee(counterNoteScript.root(), 0n)],
await client.feeFaucetId(),
);

const builder = new AccountBuilder(seed)
.storageMode(AccountStorageMode.public())
.withComponent(counterComponent);

for (const component of networkAuth) {
builder.withComponent(component);
}

const { account } = builder.build();
await client.accounts.insert({ account });
await client.transactions.consume({
account: account.id(),
notes: [fundingNoteId],
});

createNetworkAuthComponents returns the auth component, BasicWallet, and the components backing its fee policy. Omitting any of them creates an incomplete account. It includes configuration, fee-sponsorship, and P2ID scripts in the allowlist, pricing P2ID at zero unless you supply its fee. To use configuration notes to update the account, additionally install owner- or RBAC-controlled access components, as in the Rust example.

Upgrade a network account​

An existing network account can consume the standard UpgradeNote to replace its code. Set up the account with:

  • UpgradeManager and owner- or role-controlled access, such as AccessControl::Ownable2Step or AccessControl::Rbac.
  • UpgradeNote::script_root() in its note-script allowlist. The default allowlist does not include it.
  • A fee schedule entry for that root, even if the application fee is zero.

The sender must be the account's owner or hold the role authorized to call upgrade. Do not use Authority::AuthControlled for network-account upgrades: network authentication accepts allowlisted notes from any sender, so it cannot establish who is allowed to replace the code.

Once the account is deployed, build the note in the authorized sender's client:

use miden_client::note::{Note, UpgradeNote};
use miden_client::transaction::TransactionRequestBuilder;

// new_code preserves the target's existing storage layout and access controls.
let note: Note = UpgradeNote::builder()
.sender(owner_id)
.target(network_account_id)
.code(new_code)
.generate_serial_number(client.rng())
.build()?
.into();

let request = TransactionRequestBuilder::new().own_output_notes([note]).build()?;
client.submit_new_transaction(owner_id, request).await?;

The note is public, targets the network account through a NetworkAccountTarget attachment, and carries the new code in AccountCodeUpgradeAttachment chunks. Its builder enforces the attachment limits: at most four attachments and 512 words in total. The target uses one word, leaving at most 511 words for encoded code before any extra attachments. Code chunks use at most 256 words each.

An upgrade preserves storage and takes effect after the old code authenticates the consuming transaction. Keep the upgrade procedure and its authorization components in the replacement code if the account should remain upgradeable. For the common constraints, see Account code upgrades.

Surface support​

FlowRustTypeScript
Create + deploy a network account✅ AuthNetworkAccount + fee policy✅ createNetworkAuthComponents + deployment transaction
Send a network note to one✅✅ createNetworkNote / buildNetworkNote
Inspect (NetworkAccount)✅✅ isNetworkAccount() / networkNoteAllowlist()