Skip to main content
A universal account’s address is the hash of its own initial state, so creating one is always the same underlying operation: submit a UniversalStateInit action carrying that state — the code, storage, and access keys the account will start with. What differs between the three flows below is who signs the transaction that carries it, and when the account starts existing. This page assumes you’re familiar with Universal Address and Access Keys.
This is a protocol-level walkthrough using raw transactions and RPC calls. Dedicated SDK helpers for building a UniversalStateInit action are expected to follow in near-api-js and near-kit — check NEAR API for the latest tooling before hand-rolling this in production.

Choosing a flow


The uninitialized account

Before any state init runs, a 0u address can already receive NEAR: a plain Transfer to an address that doesn’t exist yet silently creates it as uninitialized — it holds a balance, but has no code, access keys, or storage.
An uninitialized account accepts exactly one thing: a UniversalStateInit action whose state init hashes to that same address. Every other action — AddKey, FunctionCall, DeployContract, Stake, and so on — fails with AccountNotInitialized, and leaves the account untouched.
Querying an uninitialized account looks like this:
bootstrap_nonce is seeded as (block_height - 1) * 1,000,000 — the block the account was created at, using the exact same formula and multiplier NEAR uses for a freshly added access key’s starting nonce (see the nonce values note on the Transaction Anatomy page).
Once its state init runs, the same account looks like any other: state and bootstrap_nonce disappear from the response entirely, rather than flipping to "initialized" / null.
Why bootstrap_nonce mattersAn uninitialized account has no access key yet, so there’s no view_access_key query to ask for a nonce. bootstrap_nonce — seeded from the block height the account was created at — is the only place to learn the value a self-signed bootstrap transaction’s nonce is based on: the transaction itself must use bootstrap_nonce + 1, the same “+1” rule as any other nonce.

Relayer-assisted creation

An existing, funded account creates and pays for someone else’s universal account in a single transaction. This is the pattern for onboarding: the new owner doesn’t need to hold any NEAR up front.
1

Build the state init and derive the address

PublicKeyHandle is the compact form an access key takes in a state init: the full public key for ed25519/secp256k1, or the 32-byte SHA3-256 hash for ml-dsa-65 — PublicKeyHandle::from(public_key) picks the right variant for you.Control over the account is entirely decided by whichever keys end up in access_keys at this step — there’s no separate “owner” concept. A relayer building this state init could just as well name its own key alongside (or instead of) the future owner’s; whoever holds any key committed here controls the account from the moment it initializes.
2

Sign one transaction with UniversalStateInit and Transfer

The relayer signs a transaction from its own account, targeting the derived id, bundling the state init with a funding transfer:
The order of UniversalStateInit and Transfer doesn’t matter — whichever runs first creates the account, and the other lands on it. A FunctionCall can follow in the same batch too, to call into the contract the state init just installed.
Why is deposit zero here, and Transfer used instead? UniversalStateInitAction.deposit isn’t a general funding mechanism — it only tops up the account’s balance to cover its storage stake, and only the first time a state init actually initializes the account; a UniversalStateInit that lands on an already-initialized account always refunds the whole deposit, since there’s nothing left for it to cover. It’s fine to leave it at zero here and fund the account entirely through a separate Transfer, as shown here — that’s what determines the account’s actual spendable balance.
3

Broadcast it

A relayer cannot make itself an owner. If the batch also includes an AddKey for the relayer’s own key, the whole receipt is rejected with ActorNoPermission — creating an account by funding it does not carry the creator’s authority into it. Nothing in the batch takes effect: the account is never created either.

Self-signed bootstrap

The future owner already holds a key pair and just needs somewhere to send funds to before that key exists on-chain as an access key. No relayer, no second party — the account initializes itself.
1

Fund the address

Anyone — typically the owner themselves, from another account or an exchange withdrawal — sends an ordinary transfer to the derived 0u address. This is the exact mechanic described in The uninitialized account: the transfer alone brings the account into existence, uninitialized.
2

Read the bootstrap nonce

Query view_account (see the example above) to confirm state is "uninitialized" and read bootstrap_nonce.
3

Sign the state init with the account's own key

The transaction’s signer_id and receiver_id are both the new account itself, signed with the key that’s inside state_init.access_keys — the same key pair the owner generated in step 1 of the relayer flow above:
4

Broadcast it

Same send_tx call as the relayer flow. Once it executes, the key that signed the bootstrap is a real full-access key, and the account behaves like any other.
Only a key that’s actually committed inside state_init.access_keys is accepted as the signer of this bootstrap transaction — this is checked independently both when the transaction is admitted to the mempool and when it executes. An unrelated key cannot claim someone else’s uninitialized account, even though the account technically has no access key on file yet.

Contract-triggered creation

A deployed contract creates a universal account as part of its own execution — the pattern for factories that spin up new accounts on demand, or for airdrop/linkdrop-style contracts that pre-fund an address before its owner claims it.
Unlike app.bob.near, a universal account created this way is not a namespace child of the creating contract — its 0u address is just a hash of its own state, unrelated to the contract’s account id. “Spinning up an account” here means creating an independently-addressed account, not extending the contract’s own domain the way named sub-accounts do.
1

Derive the account id on-chain

The contract asks the host to derive the id from the exact bytes it’s about to use, so the promise it creates in the next step is guaranteed to target the right receiver. The call also validates the bytes and reports the result, instead of silently deriving an id for anything:
These are the raw host-function imports — there’s no near-sdk-rs ergonomic wrapper for them yet, so this is what you write directly, the same low-level way you’d import any other host function it doesn’t wrap.universal_state_init_to_account_id returns a status instead of always succeeding: 0 means the id was derived and written to the register, 1 means the bytes don’t decode into a UniversalStateInit at all, and 2 means they decode but aren’t the one canonical encoding (unsorted data or access_keys, a duplicate entry, or trailing bytes). On anything but 0, the register is left unwritten. Check the status before moving on. Otherwise this is a pure computation: no side effects, and it can run inside a view call.
2

Append the UniversalStateInit action to a promise

Like in the relayer flow, this amount_ptr deposit only tops up the account’s storage stake, and only on the state init that actually initializes the account, refunding the rest back to the calling contract. A state init that lands on an account that’s already initialized — a repeat, idempotent call — always refunds the whole deposit, since there’s no storage left for it to cover. It doesn’t unconditionally add to the account’s balance the way a Transfer action does; if you want the new account to end up with a specific spendable balance, batch a Transfer action onto the same promise as well.
There’s no host function to build up a UniversalStateInit’s data map incrementally the way set_state_init_data_entry does for a deterministic account. If your contract needs to populate initial storage, it has to construct the whole Borsh-encoded state init itself before calling promise_batch_action_universal_state_init — the two host functions here only derive an id from bytes and attach an action, they don’t help build the bytes.
3

Handle malformed input at the receipt, not at the call

promise_batch_action_universal_state_init itself never rejects bad input — it always succeeds from the calling contract’s point of view, even for bytes that step 1 would already have reported as invalid. What fails is the receipt the action creates: if the bytes don’t decode into a UniversalStateInit, or decode but aren’t the canonical encoding, that receipt fails with MalformedUniversalStateInit; if the promise’s receiver doesn’t match what the bytes derive to, it fails with InvalidUniversalStateInitReceiver. Checking step 1’s status first is the cheap way to catch this before spending a promise on it at all — but check the outcome of the cross-contract call either way, if your contract’s logic depends on the account having actually been created.
Both host functions are gated by the UniversalAccounts protocol feature, stable from protocol version 88. Calling them on a network that hasn’t reached that version fails to link, the same way any other protocol-gated host function does.