From Three Transactions to One:Simplifying Token Distribution with Atomic Operations

This article builds on our previous exploration of building a token distribution dApp with Concordium Protocol Level Tokens (PLT). You can find the first part here. You can find the dApp on the Concordium Github here.

Remember that token distribution system we built? The one that verified EU nationality and automatically sent tokens to eligible users? Well, it worked great, but as we used it more, something started bothering us. Every time someone requested tokens, they had to sit through three separate blockchain transactions. First we'd add them to the allow list, then mint tokens, then transfer those tokens. Each step took about 4-5 seconds to finalize, making users wait 12-15 seconds total while adding risk of failure on every step.

That's when we decided to leverage Token.sendOperations() from the Concordium SDK which led to an entire backend architecture restructuring.

What Was Wrong With Our Original Approach?

Let's think about what our system was doing before. When someone proved their EU nationality, we'd initiate this sequence:

// The old way - three separate transactions to the blockchain

const allowListTxHash = await this.addUserToAllowList(dto.userAccount, tokenId);

const mintTxHash = await this.mintService.mintTokens({
  tokenId,
  amount: parseInt(defaultMintAmount),
});

const transferTxHash = await this.paymentService.transferTokens({
  tokenId,
  to: dto.userAccount,
  amount: parseInt(defaultMintAmount),
});

Each line meant a separate transaction, a separate wait for blockchain confirmation, and a separate opportunity for something to go wrong. We had to build this whole complex system in the frontend and backend to track which step we were on, handle cases where maybe step two worked but step three failed, and keep users informed through the entire process. Also, if step two were to fail, step 1 would still go through, so a user would’ve been added to the allow list of a token without actually receiving anything and without more tokens being minted. Furthermore, the user wouldn’t be able to use the faucet again, since once you’re on the allow list, you can’t have any more tokens issued for you.

It felt like we were making three separate trips to the store when we could have just made one shopping list and gotten everything at once.

The Game Changer: Token.sendOperations()

Then we started using Token.sendOperations() from the SDK. This method lets you bundle multiple token operations into a single atomic transaction. Think of it like this: instead of telling the blockchain "add this user to the allow list" and waiting, then "mint some tokens" and waiting, then "transfer tokens" and waiting, you can say "do all three of these transactions together, and if any of them fails, revert prior steps and don't do anything".

Here's what that looks like in practice:

// The new way - one atomic transaction that does everything

const combinedTx = await Token.sendOperations(
  token,
  sender,
  [
    { addAllowList: { target: targetHolder } },
    { mint: { amount: tokenAmount } },
    {
      transfer: {
        recipient: targetHolder,
        amount: tokenAmount,
        memo: CborMemo.fromString(`Faucet distribution to ${userAccount}`)
      }
    }
  ],
  signer
)

That's it. One function call that handles our entire token distribution workflow. All three operations are executed in order, and either they all succeed or none do. No more partial failures, no more complex state tracking, no more making users wait through multiple confirmations.

Simplifying Our Backend Architecture

This approach led us to completely restructure our backend. We went from having three separate NestJS modules (AllowListModule, MintModule, PaymentModule) to just one TokenDistributionModule. It wasn't just about having less code - it was about thinking differently about the problem.

Our new service method became much cleaner:

private async executeCombinedTokenOperation(
  userAccount: string,
  tokenIdStr: string,
  amount: number
): Promise<string> {

  // Get our blockchain client and governance wallet
  const client = this.concordiumService.getClient()
  const { sender, signer } = this.concordiumService.loadGovernanceWallet()

  // Set up all our parameters
  const tokenId = TokenId.fromString(tokenIdStr)
  const token = await Token.fromId(client, tokenId)
  const tokenAmount = TokenAmount.fromDecimal(amount, token.info.state.decimals)
  const targetHolder = TokenHolder.fromAccountAddress(userAddress)

  // Execute everything in one shot
  const combinedTx = await Token.sendOperations(
    token,
    sender,
    [
      { addAllowList: { target: targetHolder } },
      { mint: { amount: tokenAmount } },
      { transfer: { recipient: targetHolder, amount: tokenAmount } }
    ],
    signer
  )

  // Wait for just one confirmation
  await client.waitForTransactionFinalization(combinedTx)
  return combinedTx.toString()
}

Notice how much simpler this is? We prepare everything once, execute one transaction, and wait for one confirmation. All that complexity of coordinating multiple operations just vanished.

The User Experience Gets Much Better

The improvement from the user's perspective was immediate and dramatic. Instead of watching three progress bars slowly fill up over 12-15 seconds, users now see one quick operation. But more importantly, there's no longer any weird intermediate state where they might be on the allowlist but not have their tokens yet.

Our frontend tracking also became much simpler:

// Much simpler progress tracking

this.processTrackingService.initializeProcess(processId, [
  { step: 'Execute Token Distribution', status: 'pending', progress: 0 },
])

Instead of managing three separate steps with all their potential failure combinations, we just track one operation that either works or doesn't. The user interface reflects this simplicity - when it's done, everything is done.

Understanding What Operations You Can Batch

The Token.sendOperations() method is quite flexible. You can combine different types of PLT operations in a single transaction:

  • Adding or removing users from the allow list and deny list

  • Minting new tokens or burning existing ones

  • Transferring tokens between accounts

  • Even updating governance settings

For our token faucet, we specifically needed allow list management, minting, and transfers, but you could imagine other combinations. Maybe you want to update multiple allow lists at once, or mint tokens and immediately distribute them to several recipients.

The key insight is that the operations execute in the order you specify them, and the Concordium protocol validates everything as it goes. In our case, it adds the user to the allow list first, then mints tokens to the governance account, then transfers those newly minted tokens to the user.

Why This Matters

While the performance improvement is nice, the real benefit is reliability and simplicity. With atomic operations, you eliminate a whole class of problems that come from coordinating multiple blockchain transactions.

Before, if our second transaction succeeded but our third one failed, we'd have to figure out how to clean up or retry. Now, either the user gets added to the allowlist AND tokens are minted AND the user receives the tokens, or neither happens. There's no partial success to handle.

This also makes our error handling much cleaner. Instead of trying to piece together what went wrong across three different transactions, we get one comprehensive result that tells us exactly what happened.

Looking Ahead

This evolution really shows off Protocol Level Tokens - they're designed to handle complex real-world scenarios that would be painful to implement with traditional smart contracts with proper security. The atomic batching capability we used here is just one example of how Concordium can be used for all kinds of operations that financial applications actually need.

As PLT development continues, we can expect even more sophisticated coordination capabilities. The foundation we've built positions our token distribution system to take advantage of whatever comes next.

The Takeaway

Sometimes the biggest improvements come from stepping back and asking "what if we thought about this differently?" Our original approach of three sequential transactions worked, but it wasn't optimal. By leveraging PLT's native batching capabilities, we eliminated complexity, improved performance, and created a much better user experience.

If you're building token-based applications on Concordium, this pattern of atomic operations is worth exploring. The combination of reliable transactions, built-in identity verification, and sophisticated token management continues to make Concordium a compelling choice for PayFi applications.

You can find the complete updated code on the Concordium github repository, showing how all these pieces fit together in practice and you can test out the dApp by following this link (you need a Concordium DevNet wallet to be able to use it).

0
Subscribe to my newsletter

Read articles from Dragos Gabriel Danciulescu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Dragos Gabriel Danciulescu
Dragos Gabriel Danciulescu

Developer Relations Engineer at Concordium, specializing in blockchain technology, cryptography, and developer education. I'm passionate about making complex technologies accessible to developers, with a particular focus on cryptographic systems - a passion that began during my university studies with Joan Daemen, creator of the AES encryption standard. I worked extensively with both monolithic and microservices architectures, and currently drive blockchain adoption by creating dApps, SDKs, and educational content for the Concordium blockchain. Through my role at Concordium, I help developers build privacy-preserving, compliant blockchain applications using Protocol Level Tokens and zero-knowledge proofs.