RADIX WikiRADIX Wiki

Introduction

A Radix component does not send tokens to an address. It returns a bucket, and the transaction that called it decides where the bucket goes. When the recipient signs that transaction, this is all a dApp needs. When the recipient is not there (a payout, an airdrop, an exchange withdrawal), the Radix docs recommend one of three patterns: the user claims from the dApp with a badge, a native account locker holds what the account refuses, or the user authorizes the dApp to deposit (Patterns for Application Deposit Use Cases).

This guide builds each one. The blueprint and every manifest result below come from the ledger simulator in Scrypto 1.4.0.

Why not send tokens straight to the user's account?

Because the account may refuse them, and the component cannot know in advance. A Radix account is a component with deposit rules its owner sets: accept everything, reject everything, or accept only resources on an allow list. A third party's deposit goes through try_deposit_or_abort or try_deposit_or_refund, which apply those rules; only the owner's signature lets deposit through regardless (Account Deposit Patterns).

The docs' withdraw pattern adds three more reasons for components not to push funds out: the address may belong to a component with no deposit method at all; the user may want to use the funds in the same transaction rather than receive them; and every assumption about the recipient that turns out wrong can leave tokens stuck (The Withdraw Pattern). Ethereum's push-to-address transfers are also where its reentrancy attacks start, through the recipient contract's fallback function.

When the user is present, the answer is the plainest: the method returns a Bucket, and the dApp's front end builds a manifest that deposits it to whichever account the user chose in their wallet. A DEX's swap method takes a bucket and returns a bucket for this reason, which also lets other components compose it.

Badge and claim

The docs' default for a dApp with a continuing relationship with its users: identify each user by a non-fungible badge they hold rather than by an account address, keep what they are owed inside the component, and let them claim it with a proof of the badge from whichever account they like (Badge and Claim Pattern). This blueprint registers members, records amounts owed per badge, and pays out on claim:

#[derive(ScryptoSbor, NonFungibleData)]
pub struct Member {
    pub name: String,
}
 
#[blueprint]
mod payouts {
    enable_method_auth! {
        methods {
            register => restrict_to: [OWNER];
            credit => restrict_to: [OWNER];
            claim => PUBLIC;
        }
    }
 
    struct Payouts {
        members: NonFungibleResourceManager,
        owed: KeyValueStore<NonFungibleLocalId, Decimal>,
        funds: Vault,
    }
 
    impl Payouts {
        pub fn instantiate(owner_badge: ResourceAddress) -> Global<Payouts> {
            let (reservation, me) = Runtime::allocate_component_address(Payouts::blueprint_id());
            let owner = OwnerRole::Fixed(rule!(require(owner_badge)));
 
            let members = ResourceBuilder::new_ruid_non_fungible::<Member>(owner.clone())
                .mint_roles(mint_roles! {
                    minter => rule!(require(global_caller(me)));
                    minter_updater => rule!(deny_all);
                })
                // Soulbound: once deposited, it cannot be withdrawn again.
                .withdraw_roles(withdraw_roles! {
                    withdrawer => rule!(deny_all);
                    withdrawer_updater => rule!(deny_all);
                })
                .create_with_no_initial_supply();
 
            Self { members, owed: KeyValueStore::new(), funds: Vault::new(XRD) }
                .instantiate()
                .prepare_to_globalize(owner)
                .with_address(reservation)
                .globalize()
        }
 
        pub fn register(&mut self, name: String) -> NonFungibleBucket {
            self.members.mint_ruid_non_fungible(Member { name })
        }
 
        pub fn credit(&mut self, member: NonFungibleLocalId, payment: Bucket) {
            let before = self.owed.get(&member).map(|a| *a).unwrap_or_default();
            self.owed.insert(member, before.checked_add(payment.amount()).unwrap());
            self.funds.put(payment);
        }
 
        pub fn claim(&mut self, membership: Proof) -> Bucket {
            let id = membership
                .check_with_message(self.members.address(), "Not a member badge")
                .as_non_fungible()
                .non_fungible_local_id();
            let amount = self.owed.get(&id).map(|a| *a).unwrap_or_default();
            self.owed.insert(id, Decimal::ZERO);
            self.funds.take(amount)
        }
    }
}

Three details carry the security. claim calls check_with_message with the member resource's address before reading the badge's ID: a proof's contents cannot be read until it is checked, and without the check an attacker could mint a resource of their own with the same local ID (Before You Release). The amounts live in a KeyValueStore, which loads one entry at a time, so the component's cost does not grow with its membership. The badge's withdraw rule is deny_all, so it cannot be sold or passed on once it reaches an account.

In the simulator, after the owner credited 50 $XRD to a member's badge ID, the member's claim moved 50 $XRD into their account. A claim with a proof of a different resource failed with the panic Not a member badge. Withdrawing the badge from the member's account failed with Unauthorized on the withdrawer role.

The docs' own lottery example of this pattern does not compile as written: its instantiation function ends .globalize(); with a semicolon, so it returns () where it declares Global<LotteryExample>, and it takes &mut self, which makes it a method on a component that does not yet exist. Its withdraw method can be called repeatedly because nothing marks the prize as paid. The owed entry set to zero above is that mark.

How do I deposit to an account that rejects third-party deposits?

Ask the owner to add your badge as an authorized depositor. A deposit made through a try_deposit method, with a proof of an authorized badge in the auth zone and the badge named in the call, succeeds whatever the account's other rules say. The docs reserve this for one narrow case: sending a non-transferable badge to the exact account a user proved they own, for example with ROLA (Authorized Depositor Pattern).

The owner changes their account's rules with these methods, signed by the account's key:

CALL_METHOD Address("<account>") "set_default_deposit_rule" Enum<DefaultDepositRule::Reject>();
CALL_METHOD Address("<account>") "set_resource_preference" Address("<xrd>") Enum<ResourcePreference::Allowed>();
CALL_METHOD Address("<account>") "add_authorized_depositor" Enum<ResourceOrNonFungible::Resource>(Address("<depositor_badge>"));

The dApp then presents its badge and names it in the deposit:

CALL_METHOD Address("<app_account>") "create_proof_of_amount" Address("<depositor_badge>") Decimal("1");
# ...mint or withdraw the resource being sent, then:
TAKE_ALL_FROM_WORKTOP Address("<resource>") Bucket("badge");
CALL_METHOD Address("<user_account>") "try_deposit_or_abort"
    Bucket("badge")
    Enum<1u8>(Enum<ResourceOrNonFungible::Resource>(Address("<depositor_badge>")));

In the simulator, the member account set its default rule to Reject. The owner's attempt to deposit a new member badge with no depositor badge named failed with NotAllBucketsCouldBeDeposited. After the member added the owner badge as an authorized depositor, the same manifest with the badge named committed, and the soulbound badge arrived. In a dApp, the request to add the depositor is a transaction the front end sends to the user's wallet, where the user approves it like any other.

Before depositing at all, a Gateway or node can say whether an account will accept a resource: the Gateway's /transaction/account-deposit-pre-validation and the Core API's /lts/state/account-deposit-behaviour both report the rule that decided it (Exchange Integration).

The account locker

For sends to accounts the dApp has no relationship with, such as an airdrop or an exchange withdrawal, the network has a native Account Locker blueprint, added at the Bottlenose protocol update in 2024 (Account Locker). Its store method takes an account, a bucket and a try_direct_send flag. With the flag set it tries the account first; whatever the account refuses stays in the locker under that account, and only that account's owner can claim it:

# Once: create a locker. Returns the locker and a storer badge.
CALL_FUNCTION Address("package_rdx1pkgxxxxxxxxxlckerxxxxxxxxxx000208064247xxxxxxxxxlckerx")
    "AccountLocker" "instantiate_simple" true;
 
# Each send: try the account first, keep it in the locker if refused.
CALL_METHOD Address("<app_account>") "create_proof_of_amount" Address("<storer_badge>") Decimal("1");
CALL_METHOD Address("<app_account>") "withdraw" Address("<resource>") Decimal("25");
TAKE_ALL_FROM_WORKTOP Address("<resource>") Bucket("gift");
CALL_METHOD Address("<locker>") "store" Address("<user_account>") Bucket("gift") true;
 
# The account owner, later, in a transaction they sign:
CALL_METHOD Address("<locker>") "claim" Address("<user_account>") Address("<resource>") Decimal("25");
CALL_METHOD Address("<user_account>") "deposit_batch" Expression("ENTIRE_WORKTOP");

In the simulator, a store of 25 $XRD to the member account, whose rule was Reject, left its balance unchanged; the member's claim then added 25 $XRD. A claim against the same account signed by anyone else failed with AssertAccessRuleFailed. airdrop does the same for many accounts in one call, taking a map of accounts to amounts or non-fungible IDs.

The docs say to put the locker's address in an account_locker field of the dApp's dApp definition, and list it in claimed_entities, so that wallets can find it. Their note that the Radix Wallet cannot yet show locker claims is out of date: the Android wallet's source has the message "%s has a deposit for you to claim" and builds the claim through accountLockerClaim from Sargon, the wallets' shared Rust core, and the iOS wallet has an AccountLockersClient that queries the Gateway's account-locker endpoints (Android, iOS). The docs note that the locker does not suit non-transferable tokens: a claim withdraws the token from the locker's vault, which a non-transferable token's withdraw rule forbids. A soulbound badge still needs an authorized depositor.

Which pattern should I use?

  • The user signs the transaction: return a bucket and let the manifest deposit it.
  • A continuing relationship with users: badge and claim. Users keep the freedom to change accounts, and claims can carry extra conditions, such as a deadline or a second credential.
  • A one-off send to a known address: the account locker, with try_direct_send set.
  • A non-transferable badge to a proven account: an authorized depositor, once, and badge and claim after that.

Next Steps

HydrateLast updated 7h agov1.0.01 revision
CiteRADIX Wiki. "11. Deposit and Withdraw Patterns." November 14, 2026. https://radix.wiki/developers/scrypto/11-deposit-and-withdraw-patterns