For the complete documentation index, see llms.txt. This page is also available as Markdown.

Intermediate Embedded Mini App Guide

This guide is takes building Embedded Mini Apps one step further and we are going to demonstrate it with a practical example mini app linked below.

Compared to simple embedded mini apps, where the user signs a transaction and an action happens directly, in this example the app performs a couple of extra backend steps (which you can vary based upon your mini app's logic). An intermediate mini app lets the host wallet execute the payment, embed a signed payment intent into the transfer, match it later through the Circles RPC, then issue a signed receipt or sign on chain transfers.

You can also setup Circles Org using this mini app if you want to process CRC payments from your backend. For example, a game which rewards people in CRC for winning , can setup an Org using that miniapp, fund it, add an eoa as a signer and confgure direct payments in the backend.


Get Started

Clone the repository:

Install Dependencies & Start the dev server:

Navigate to https://circles.gnosis.io/playground and paste your localhost dev url.

Architecture

The app has four parts:

The key principle is simple:

The frontend is not trusted.

It can ask the backend to build transactions. It can ask the host to submit them. But it never verifies payments, signs receipts, or holds secrets.


Handling Wallets Through the MiniApp SDK

Inside the Circles host, the MiniApp does not connect to a wallet directly.

Instead, it uses @aboutcircles/miniapp-sdk:

The app first checks whether it is running inside the host:

Then it subscribes to wallet updates:

This keeps the MiniApp reactive without owning the wallet session.

The most important primitive is sendTransactions():

This opens the host-controlled approval flow. The user approves through the host wallet, and the host submits the transaction through ERC-4337.

One important detail: sendTransactions() does not mean the payment has landed on-chain. It usually gives you a UserOperation submission result, not a normal transaction hash you can immediately match against a transfer.

So the MiniApp treats transaction submission as only the beginning of the verification flow.


Building a Payment Intent

When the user clicks “Buy ticket,” the frontend calls the backend:

The backend creates a signed payment intent:

It serializes that payload and signs it with HMAC:

This token is the payment intent.

It answers:

Who is buying what, for which event, before what expiry, with which unique nonce?

Because the backend can verify the HMAC later, it does not need to store this intent in a database.


Embedding the Intent Into the CRC Transfer

The backend then builds the CRC transfer calldata.

The important trick is that the signed payment intent is embedded into the transfer metadata:

The backend returns these transactions to the frontend:

Notice the value.toString().

Transaction objects cross a JSON boundary, so BigInts must become strings.

The frontend then passes these transactions to the host:

At this point:

  1. the host asks the user to approve,

  2. the wallet submits the transaction,

  3. the CRC transfer eventually lands on-chain,

  4. the Circles indexer eventually sees the transfer metadata.


Matching the Transaction Later

After submission, the frontend navigates to a ticket page:

That page polls:

The backend receives the paymentData, verifies the HMAC, and then searches the Circles indexer for a matching transfer.

The match is based on:

  • recipient equals the organizer address

  • embedded metadata contains the same crc-ticket... token

  • token HMAC is valid

  • token buyer/event/ticket fields are valid

Simplified flow:

This is the core database-less trick.

The on-chain transfer carries the intent, and the backend later confirms that the intent actually landed.


Handling Eventual Consistency

The app does not assume instant confirmation.

The receipt endpoint returns one of three states:

The frontend polls with backoff.

This is important because there are multiple async layers:

A successful sendTransactions() call only proves that the transaction request was submitted. It does not prove that payment was indexed.


Signing the Ticket Receipt

Once the backend finds the matching transfer, it signs a ticket receipt using EIP-712.

The receipt looks like this:

The backend signs it with a dedicated App Operator EOA:

This signed receipt becomes the actual ticket.

The user can store it, show it as a QR code, or present it to a scanner.


Why Use a Separate Operator Key?

The App Operator key only signs receipts.

It does not:

  • hold CRC

  • receive payments

  • send transactions

  • control organizer funds

The organizer address receives the actual payment. The operator key only attests:

“This payment was verified, and this ticket is valid.”

But you can also add a transactional backend, which for example maybe processes payouts, etc.


QR Code as Portable Proof

When the ticket is ready, the frontend encodes the signed receipt:

That encoded payload becomes the QR code.

A scanner can verify the ticket offline by checking the EIP-712 signature against the operator address.

No backend call is required at the door.

That makes the ticket portable:


Ideal User Journey (Example)


For CRC Tickets, the payment is the on-chain action. The ticket is an off-chain signed receipt. The bridge between them is the signed payment intent embedded inside the transfer.

Last updated

Was this helpful?