Set Transferability
Decide who can transfer, burn, trade, or force-move BitBadges tokens with collection approvals and user-level approvals.
Set SIGNER to the wallet address authorized to sign the transaction, matching its explicit creator or sender. Browser deployment requires this binding even when reading a saved transaction.
At the end your collection has post-mint approvals (transferable, burnable, admin override, or none) and you know how users set their own incoming and outgoing approvals.
Transferability is the set of approvals a transfer must match at the collection, outgoing, and incoming levels; see Transferability. The criteria you can attach to any approval are on Approval Criteria.
1. Split Approvals into Mint and Post-Mint
Keep two groups in collectionApprovals:
- Mint approvals:
fromListId: 'Mint'. They create balances. See Mint and Distribute. - Post-mint approvals:
fromListId: '!Mint'. They move existing balances.
Two rules:
- The reserved
Alllist includesMint. Never useAllasfromListIdon a post-mint approval, or it becomes a mint approval. - A
Mintapproval must setoverridesFromOutgoingApprovals: true, because the Mint address cannot manage its own outgoing approvals.
const mintApprovals = [mintApproval]; // fromListId: 'Mint', from Mint and distribute
const postMintApprovals = [transferableApproval, burnableApproval]; // fromListId: '!Mint', step 3
const collectionApprovals = [...mintApprovals, ...postMintApprovals];Reserved list IDs are All, Mint, !Mint, AllWithoutMint, one address such as bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d, !bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d (everyone except that address), and colon-separated addresses such as bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d:bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue. See Address Lists.
Ask your agent. With the MCP builder tools installed, paste one of these:
Make collection 1 transferable and burnable after mint, run the review, and give me the review link.Add an admin override approval on collection 1 for bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d that can move any token, and flag anything risky in the review.2. Start from the Empty Criteria Template
Every approval carries approvalCriteria. This template means "no additional restrictions"; the other examples spread it and override a field or two. Amounts and counts of '0' mean unlimited; amountTrackerId is only needed when a limit is set.
// Every field at its proto default. Spread it, then override what you need.
const EmptyApprovalCriteria = {
merkleChallenges: [],
predeterminedBalances: {
manualBalances: [],
incrementedBalances: {
startBalances: [],
incrementTokenIdsBy: '0',
incrementOwnershipTimesBy: '0',
durationFromTimestamp: '0',
allowOverrideTimestamp: false,
recurringOwnershipTimes: { startTime: '0', intervalLength: '0', chargePeriodLength: '0' },
allowOverrideWithAnyValidToken: false,
allowAmountScaling: false,
maxScalingMultiplier: '0',
},
orderCalculationMethod: {
useOverallNumTransfers: false,
usePerToAddressNumTransfers: false,
usePerFromAddressNumTransfers: false,
usePerInitiatedByAddressNumTransfers: false,
useMerkleChallengeLeafIndex: false,
challengeTrackerId: '',
}
},
approvalAmounts: {
overallApprovalAmount: '0',
perToAddressApprovalAmount: '0',
perFromAddressApprovalAmount: '0',
perInitiatedByAddressApprovalAmount: '0',
amountTrackerId: '',
resetTimeIntervals: { startTime: '0', intervalLength: '0' },
},
maxNumTransfers: {
overallMaxNumTransfers: '0',
perToAddressMaxNumTransfers: '0',
perFromAddressMaxNumTransfers: '0',
perInitiatedByAddressMaxNumTransfers: '0',
amountTrackerId: '',
resetTimeIntervals: { startTime: '0', intervalLength: '0' },
},
coinTransfers: [],
requireToEqualsInitiatedBy: false,
requireFromEqualsInitiatedBy: false,
requireToDoesNotEqualInitiatedBy: false,
requireFromDoesNotEqualInitiatedBy: false,
overridesFromOutgoingApprovals: false,
overridesToIncomingApprovals: false,
autoDeletionOptions: {
afterOneUse: false,
afterOverallMaxNumTransfers: false,
allowCounterpartyPurge: false,
allowPurgeIfExpired: false,
},
mustOwnTokens: [],
dynamicStoreChallenges: [],
ethSignatureChallenges: [],
senderChecks: {
mustBeEvmContract: false,
mustNotBeEvmContract: false,
mustBeLiquidityPool: false,
mustNotBeLiquidityPool: false,
},
recipientChecks: {
mustBeEvmContract: false,
mustNotBeEvmContract: false,
mustBeLiquidityPool: false,
mustNotBeLiquidityPool: false,
},
initiatorChecks: {
mustBeEvmContract: false,
mustNotBeEvmContract: false,
mustBeLiquidityPool: false,
mustNotBeLiquidityPool: false,
},
altTimeChecks: {
offlineHours: [],
offlineDays: [],
offlineMonths: [],
offlineDaysOfMonth: [],
offlineWeeksOfYear: [],
timezoneOffsetMinutes: '0',
timezoneOffsetNegative: false,
},
mustPrioritize: false,
votingChallenges: [],
allowBackedMinting: false,
allowSpecialWrapping: false,
evmQueryChallenges: [],
userApprovalSettings: {
allowedDenoms: [],
disableUserCoinTransfers: false,
userRoyalties: { percentage: '0', payoutAddress: '' },
}
};In the SDK, approvalCriteria: undefined on a CollectionApproval has the same meaning.
3. Add the Post-Mint Approvals You Want
Transferable
Anyone can send to anyone after mint.
const transferableApproval = new CollectionApproval({
fromListId: '!Mint', // Excludes the Mint address
toListId: 'All',
initiatedByListId: 'All',
transferTimes: UintRangeArray.FullRanges(),
ownershipTimes: UintRangeArray.FullRanges(),
tokenIds: UintRangeArray.FullRanges(),
approvalId: 'transferable-approval',
version: 0n,
approvalCriteria: undefined, // No additional restrictions
});{
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"approvalId": "transferable-approval",
"approvalCriteria": {
"predeterminedBalances": {
"incrementedBalances": {
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
}
},
"approvalAmounts": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"userApprovalSettings": {
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
}
}{
"fromListId": "!Mint",
"toListId": "All",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"approvalId": "transferable-approval",
"approvalCriteria": {
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": false,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"overridesFromOutgoingApprovals": false,
"overridesToIncomingApprovals": false,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"senderChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"recipientChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"initiatorChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"altTimeChecks": {
"offlineHours": [],
"offlineDays": [],
"offlineMonths": [],
"offlineDaysOfMonth": [],
"offlineWeeksOfYear": [],
"timezoneOffsetMinutes": "0",
"timezoneOffsetNegative": false
},
"mustPrioritize": false,
"votingChallenges": [],
"allowBackedMinting": false,
"allowSpecialWrapping": false,
"evmQueryChallenges": [],
"userApprovalSettings": {
"allowedDenoms": [],
"disableUserCoinTransfers": false,
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
},
"version": "0"
}Keep approvalId: "transferable-approval". The BitBadges site and the CLI detect free transferability by this ID.
Burnable
Holders destroy tokens by sending them to the burn address bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv, which is the Ethereum null address 0x0000000000000000000000000000000000000000 in BitBadges format. No one holds its key, so tokens sent there are gone.
const burnableApproval = new CollectionApproval({
fromListId: '!Mint', // Excludes the Mint address
toListId: 'bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv', // Burn address (bb-prefixed)
initiatedByListId: 'All',
transferTimes: UintRangeArray.FullRanges(),
ownershipTimes: UintRangeArray.FullRanges(),
tokenIds: UintRangeArray.FullRanges(),
approvalId: 'burnable-approval',
version: 0n,
approvalCriteria: undefined, // No additional restrictions
});{
"fromListId": "!Mint",
"toListId": "bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"approvalId": "burnable-approval",
"approvalCriteria": {
"predeterminedBalances": {
"incrementedBalances": {
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
}
},
"approvalAmounts": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"overridesToIncomingApprovals": true,
"userApprovalSettings": {
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
}
}{
"fromListId": "!Mint",
"toListId": "bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv",
"initiatedByListId": "All",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "18446744073709551615" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"approvalId": "burnable-approval",
"approvalCriteria": {
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": false,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "0",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireFromEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"requireFromDoesNotEqualInitiatedBy": false,
"overridesFromOutgoingApprovals": false,
"overridesToIncomingApprovals": true,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"senderChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"recipientChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"initiatorChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"altTimeChecks": {
"offlineHours": [],
"offlineDays": [],
"offlineMonths": [],
"offlineDaysOfMonth": [],
"offlineWeeksOfYear": [],
"timezoneOffsetMinutes": "0",
"timezoneOffsetNegative": false
},
"mustPrioritize": false,
"votingChallenges": [],
"allowBackedMinting": false,
"allowSpecialWrapping": false,
"evmQueryChallenges": [],
"userApprovalSettings": {
"allowedDenoms": [],
"disableUserCoinTransfers": false,
"userRoyalties": { "percentage": "0", "payoutAddress": "" }
}
},
"version": "0"
}fromListId: "!Mint": any holder can burn.toListId: the burn address as a single-address list.initiatedByListId: "All": anyone can initiate (in practice the holder, whose outgoing approvals still apply).overridesToIncomingApprovals: true: the burn address has no user-level incoming approvals, so the collection approval must override.approvalId: "burnable-approval": the site detects burnability by this ID.- Amounts and transfer counts of
"0"mean unlimited.
The burnable approval is additive and sits alongside mint and transferable approvals. Do not add it to credit tokens (increment-only by design), soulbound tokens, or subscription tokens (managed by the issuer).
Admin Override
One address can move any token from any address to any address, ignoring both user-level approval sets.
const approveSelfForcefully = (address: string) => {
const id = 'complete-admin-control';
return {
fromListId: 'Mint',
toListId: 'All',
initiatedByListId: address,
transferTimes: UintRangeArray.FullRanges(),
tokenIds: UintRangeArray.FullRanges(),
ownershipTimes: UintRangeArray.FullRanges(),
approvalId: id,
version: 0n,
approvalCriteria: {
...EmptyApprovalCriteria,
overridesFromOutgoingApprovals: true,
overridesToIncomingApprovals: true,
},
};
};This grants complete control over every holder's balance. Give it only to an address you trust, and consider locking canUpdateCollectionApprovals so nobody can add one later. To forbid forceful post-mint transfers for the whole collection, set the noForcefulPostMintTransfers invariant; see Invariants.
For the !Mint variant (move existing tokens only), change fromListId to '!Mint'.
Non-transferable
Add no post-mint approvals. Tokens stay where they were minted. Lock canUpdateCollectionApprovals so the manager cannot add one later; see Lock Permissions.
4. Make NFTs Tradable on the Marketplace
The orderbook needs three standards together plus the transferable approval from step 3 with approvalId: "transferable-approval". MsgSetStandards replaces the list on an existing collection; at creation, or in MsgUniversalUpdateCollection with updateStandards: true, set the same standards array.
{
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"standards": ["NFTMarketplace", "NFTs", "NFTPricingDenom:ubadge"],
"canUpdateStandards": []
}NFTPricingDenom:<denom>sets the denom the orderbook displays prices in. Replaceubadgewith your pricing denom; new collections should use canonical USDC (see Create a Collection).- The legacy names
TradableandDefaultDisplayCurrencystill work for existing collections.
Listings and bids are user-level approvals that bb build listing, bb build bid, bb nfts list, and bb nfts bid emit for you; see Trade on the DEX and Build.
5. Set User-Level Approvals
Users control their own transfers through outgoing and incoming approvals. They use the same shape and criteria as collection approvals with three differences:
- Fixed address lists. Outgoing approvals lock
fromListIdto the user's address; incoming approvals locktoListIdto the user's address. The message omits the locked field. - No overrides. User approvals cannot override other levels.
- Only the user can update their own approvals (subject to their user permissions).
Most users never write these. The three defaultBalances flags (autoApproveSelfInitiatedOutgoingTransfers, autoApproveSelfInitiatedIncomingTransfers, autoApproveAllIncomingTransfers) cover the common case. See Transferability and User Approval Settings.
const userIncomingApproval = {
fromListId: 'All', // Who may send to me
initiatedByListId: 'All',
transferTimes: [{ start: '1', end: '18446744073709551615' }],
tokenIds: [{ start: '1', end: '100' }],
ownershipTimes: [{ start: '1', end: '18446744073709551615' }],
approvalId: 'user-incoming-approval',
approvalCriteria: {
...EmptyApprovalCriteria, // or any criteria from the collection examples
},
};
const userOutgoingApproval = {
toListId: 'All', // Who I may send to
initiatedByListId: 'All',
transferTimes: [{ start: '1', end: '18446744073709551615' }],
tokenIds: [{ start: '1', end: '100' }],
ownershipTimes: [{ start: '1', end: '18446744073709551615' }],
approvalId: 'user-outgoing-approval',
approvalCriteria: {
...EmptyApprovalCriteria,
},
};
const updateUserApprovals = {
creator: 'bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d', // Your address
collectionId: '1',
updateIncomingApprovals: true,
incomingApprovals: [userIncomingApproval],
updateOutgoingApprovals: true,
outgoingApprovals: [userOutgoingApproval],
// Flags left false keep the current value
updateAutoApproveSelfInitiatedOutgoingTransfers: false,
autoApproveSelfInitiatedOutgoingTransfers: false,
updateAutoApproveSelfInitiatedIncomingTransfers: false,
autoApproveSelfInitiatedIncomingTransfers: false,
updateAutoApproveAllIncomingTransfers: false,
autoApproveAllIncomingTransfers: false,
updateUserPermissions: false,
userPermissions: {
canUpdateOutgoingApprovals: [],
canUpdateIncomingApprovals: [],
canUpdateAutoApproveSelfInitiatedOutgoingTransfers: [],
canUpdateAutoApproveSelfInitiatedIncomingTransfers: [],
canUpdateAutoApproveAllIncomingTransfers: [],
},
};Raw MsgUpdateUserApprovals that sets one outgoing approval: the signer may send tokens 1 to 20 to anyone, one token in total and at most one per initiator, and self-initiated outgoing transfers are auto-approved. Every update* flag set to false leaves that field untouched, whatever value is passed.
{
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"updateOutgoingApprovals": true,
"outgoingApprovals": [
{
"toListId": "All",
"initiatedByListId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "20" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"approvalId": "user-outgoing-approval",
"approvalCriteria": {
"predeterminedBalances": {
"incrementedBalances": {
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" }
}
},
"approvalAmounts": {
"overallApprovalAmount": "1",
"perInitiatedByAddressApprovalAmount": "1",
"amountTrackerId": "user-outgoing-approval",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"amountTrackerId": "user-outgoing-approval",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
}
}
}
],
"updateAutoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedOutgoingTransfers": true
}{
"creator": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"collectionId": "1",
"updateOutgoingApprovals": true,
"outgoingApprovals": [
{
"toListId": "All",
"initiatedByListId": "bb1p0rrel3365scadq5k9pv0x0zp9j22js6dnw70d",
"transferTimes": [{ "start": "1", "end": "18446744073709551615" }],
"tokenIds": [{ "start": "1", "end": "20" }],
"ownershipTimes": [{ "start": "1", "end": "18446744073709551615" }],
"uri": "",
"customData": "",
"approvalId": "user-outgoing-approval",
"approvalCriteria": {
"merkleChallenges": [],
"predeterminedBalances": {
"manualBalances": [],
"incrementedBalances": {
"startBalances": [],
"incrementTokenIdsBy": "0",
"incrementOwnershipTimesBy": "0",
"durationFromTimestamp": "0",
"allowOverrideTimestamp": false,
"recurringOwnershipTimes": { "startTime": "0", "intervalLength": "0", "chargePeriodLength": "0" },
"allowOverrideWithAnyValidToken": false,
"allowAmountScaling": false,
"maxScalingMultiplier": "0"
},
"orderCalculationMethod": {
"useOverallNumTransfers": false,
"usePerToAddressNumTransfers": false,
"usePerFromAddressNumTransfers": false,
"usePerInitiatedByAddressNumTransfers": false,
"useMerkleChallengeLeafIndex": false,
"challengeTrackerId": ""
}
},
"approvalAmounts": {
"overallApprovalAmount": "1",
"perToAddressApprovalAmount": "0",
"perFromAddressApprovalAmount": "0",
"perInitiatedByAddressApprovalAmount": "1",
"amountTrackerId": "user-outgoing-approval",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"maxNumTransfers": {
"overallMaxNumTransfers": "0",
"perToAddressMaxNumTransfers": "0",
"perFromAddressMaxNumTransfers": "0",
"perInitiatedByAddressMaxNumTransfers": "0",
"amountTrackerId": "user-outgoing-approval",
"resetTimeIntervals": { "startTime": "0", "intervalLength": "0" }
},
"coinTransfers": [],
"requireToEqualsInitiatedBy": false,
"requireToDoesNotEqualInitiatedBy": false,
"autoDeletionOptions": {
"afterOneUse": false,
"afterOverallMaxNumTransfers": false,
"allowCounterpartyPurge": false,
"allowPurgeIfExpired": false
},
"mustOwnTokens": [],
"dynamicStoreChallenges": [],
"ethSignatureChallenges": [],
"recipientChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"initiatorChecks": {
"mustBeEvmContract": false,
"mustNotBeEvmContract": false,
"mustBeLiquidityPool": false,
"mustNotBeLiquidityPool": false
},
"altTimeChecks": {
"offlineHours": [],
"offlineDays": [],
"offlineMonths": [],
"offlineDaysOfMonth": [],
"offlineWeeksOfYear": [],
"timezoneOffsetMinutes": "0",
"timezoneOffsetNegative": false
},
"mustPrioritize": false,
"votingChallenges": [],
"evmQueryChallenges": []
},
"version": "0"
}
],
"updateIncomingApprovals": false,
"incomingApprovals": [],
"updateAutoApproveSelfInitiatedOutgoingTransfers": true,
"autoApproveSelfInitiatedOutgoingTransfers": true,
"updateAutoApproveSelfInitiatedIncomingTransfers": false,
"autoApproveSelfInitiatedIncomingTransfers": false,
"updateAutoApproveAllIncomingTransfers": false,
"autoApproveAllIncomingTransfers": false,
"updateUserPermissions": false,
"userPermissions": {
"canUpdateOutgoingApprovals": [],
"canUpdateIncomingApprovals": [],
"canUpdateAutoApproveSelfInitiatedOutgoingTransfers": [],
"canUpdateAutoApproveSelfInitiatedIncomingTransfers": [],
"canUpdateAutoApproveAllIncomingTransfers": []
}
}Outgoing approvals have no senderChecks and no overrides* fields, since the sender is fixed and user approvals cannot override.
Broadcast with bb deploy --msg-file ./update-approvals.json --browser --expected-address "$SIGNER". The single-approval variants MsgSetOutgoingApproval, MsgSetIncomingApproval, MsgDeleteOutgoingApproval, and MsgDeleteIncomingApproval edit one approval without resending the whole set; see Messages.
6. Deploy the Change
Collection approvals go in collectionApprovals at creation, or later through MsgUniversalUpdateCollection with updateCollectionApprovals: true (allowed only where canUpdateCollectionApprovals is not frozen).
bb check ./update.json
bb deploy --msg-file ./update.json --browser --expected-address "$SIGNER"Other criteria worth attaching to post-mint approvals: Address Checks for contract and pool restrictions, EVM Query Challenges for token gating by contract state, and Token Ownership for holder-only transfers.