Skip to main content

Create a social invite link

delegation toolkitembedded walletssocialinvitereferrallinkMetaMask Developer Relations | Sep 8, 2025

This tutorial walks you through creating an invite link to enable users to refer their friends to your dapp with minimal friction.

For example, Alice (the inviter) wants Bob (the invitee) to try out your dapp. She sends him a link that allows him to claim 0.001 ETH from her wallet within a time limit. Bob can start using your dapp right away, without installing a wallet or paying gas fees.

You'll enable this by adding an embedded wallet for instant onboarding, creating a MetaMask smart account to create and redeem an invitation, and creating an open delegation to represent an invitation.

Prerequisites

  • Install Node.js v18 or later.
  • Install Yarn, npm, or another package manager.
  • Get a Client ID from the Embedded Wallets (Web3Auth) dashboard.
  • Create a Pimlico API key.
    note

    This tutorial uses Pimlico's paymaster, but you can use any paymaster of your choice.

Steps

1. Install the Delegation Toolkit

Install the MetaMask Delegation Toolkit and Embedded Wallets (Web3Auth) in your project:

npm install @metamask/delegation-toolkit @web3auth/modal

2. Set up Embedded Wallets (Web3Auth)

Configure Embedded Wallets (Web3Auth) to enable users to instantly connect to your dapp using familiar login methods, like social accounts or email.

  1. Add a WEB3AUTH_CLIENT_ID environment variable, replacing <YOUR-CLIENT-ID> with your Web3Auth Client ID:

    .env.local
    WEB3AUTH_CLIENT_ID=<YOUR-CLIENT-ID>
  2. Configure Web3Auth options:

    providers/AppProvider.tsx
    import { WEB3AUTH_NETWORK, Web3AuthOptions } from '@web3auth/modal';

    const web3AuthOptions: Web3AuthOptions = {
    clientId: process.env.WEB3AUTH_CLIENT_ID as string,
    web3AuthNetwork: WEB3AUTH_NETWORK.SAPPHIRE_MAINNET,
    };

    const web3authConfig = {
    web3AuthOptions,
    };
  3. Create a connect button:

    components/ConnectButton.tsx
    import { useWeb3AuthConnect } from '@web3auth/modal/react';
    import Button from '@/components/Button'; // You can add your own Button component

    export default function ConnectButton() {
    const { connect } = useWeb3AuthConnect();

    return (
    <div className='flex gap-2'>
    <Button onClick={() => connect()}>Connect with Web3Auth</Button>
    </div>
    );
    }

3. Create Viem clients

  1. Create a Viem Public Client using Viem's createPublicClient function. You will configure a smart account and Bundler Client with the Public Client, which you can use to query the signer's account state and interact with the blockchain network.

    import { createPublicClient, http } from 'viem';
    import { sepolia as chain } from 'viem/chains';

    const publicClient = createPublicClient({
    chain,
    transport: http(),
    });
  2. Create a Viem Paymaster Client using Viem's createPaymasterClient function. This client interacts with the paymaster service. Replace <YOUR-API-KEY> with your Pimlico API key:

    import { createPaymasterClient } from 'viem/account-abstraction';

    const paymasterClient = createPaymasterClient({
    transport: http('https://api.pimlico.io/v2/11155111/rpc?apikey=<YOUR-API-KEY>'),
    });
  3. Create a Viem Bundler Client using Viem's createBundlerClient function. Pass the paymasterClient to the paymaster property. You can use the bundler service to estimate gas for user operations and submit transactions to the network.

    import { createBundlerClient } from 'viem/account-abstraction';

    const bundlerClient = createBundlerClient({
    client: publicClient,
    transport: http('https://your-bundler-rpc.com'),
    paymaster: paymasterClient,
    });

4. Create a smart account

  1. Create an account in order to create and redeem an invitation. This account will create a delegation, and must be a MetaMask smart account. This example uses a Hybrid smart account, which is a flexible smart account implementation that supports both an externally owned account (EOA) owner and any number of passkey (WebAuthn) signers.

    import { Implementation, toMetaMaskSmartAccount } from '@metamask/delegation-toolkit';
    import { privateKeyToAccount } from 'viem/accounts';

    const account = privateKeyToAccount('0x...');

    const smartAccount = await toMetaMaskSmartAccount({
    client: publicClient,
    implementation: Implementation.Hybrid,
    deployParams: [account.address, [], [], []],
    deploySalt: '0x',
    signatory: { account },
    });
  2. Deploy the smart account by sending a user operation:

    import { zeroAddress } from 'viem';

    // Appropriate fee per gas must be determined for the specific bundler being used.
    const maxFeePerGas = 1n;
    const maxPriorityFeePerGas = 1n;

    const userOperationHash = await bundlerClient.sendUserOperation({
    account: smartAccount,
    calls: [{ to: zeroAddress }],
    maxFeePerGas,
    maxPriorityFeePerGas,
    });
  3. Fund the deployed smart account with some Sepolia ETH to enable the invitee to spend funds when they redeem the invitation.

    note

    You can use the MetaMask faucet to get Sepolia ETH.

5. Create an invitation

  1. Create an open root delegation to represent an invitation. A root delegation is the first delegation in a chain of delegations, and an open root delegation grants permission to any account. In this example, the inviter creates an invitation that can be redeemed by any invitee, allowing the invitee to spend up to 0.001 ETH.

    import { createOpenDelegation, getDelegatorEnvironment } from '@metamask/delegation-toolkit';
    import { sepolia as chain } from 'viem/chains';

    const delegation = createOpenDelegation({
    from: smartAccount.address,
    environment: getDelegatorEnvironment(chain.id);
    scope: {
    type: 'nativeTokenTransferAmount',
    // 0.001 ETH in wei format.
    maxAmount: 1000000000000000n,
    },
    });
  2. Sign the delegation to enable the invitee to redeem it in the future:

    const signature = await smartAccount.signDelegation({
    delegation,
    })

    const signedDelegation = {
    ...delegation,
    signature,
    }
  3. Encode the delegation into a shareable URL:

    export function encodeDelegation(delegation: Delegation): string {
    const delegationJson = JSON.stringify(delegation);
    return Buffer.from(delegationJson, 'utf-8').toString('base64');
    }

    const encoded = encodeDelegation(signedDelegation);

    const url = new URL(window.location.href);
    url.searchParams.set("delegation", encoded);
    const shareableUrl = url.toString();

The inviter can now share the URL with anyone.

6. Redeem the invitation

  1. When the invitee opens the shared URL, decode the delegation:

    const urlParams = new URLSearchParams(window.location.search);
    const encodedDelegation = urlParams.get('delegation');

    export function decodeDelegation(encodedDelegation: string): Delegation {
    const decodedDelegationJson = Buffer.from(encodedDelegation, 'base64').toString('utf-8');
    return JSON.parse(decodedDelegationJson) as Delegation;
    }

    const decodedDelegation = decodeDelegation(encodedDelegation);
  2. Redeem the delegation by submitting a user operation from the smart account created in Step 4 to the DelegationManager contract. The delegation manager validates the delegation and executes delegated actions. In this case, the invitee can spend up to 0.001 ETH when using your dapp.

    import { createExecution, getDeleGatorEnvironment, ExecutionMode } from '@metamask/delegation-toolkit';
    import { DelegationManager } from '@metamask/delegation-toolkit/contracts';
    import { zeroAddress } from 'viem';

    const delegations = [decodedDelegation];

    const executions = createExecution({ target: zeroAddress });

    const redeemDelegationCalldata = DelegationManager.encode.redeemDelegations({
    delegations: [delegations],
    modes: [ExecutionMode.SingleDefault],
    executions: [executions]
    });

    const userOperationHash = await bundlerClient.sendUserOperation({
    account: smartAccount,
    calls: [
    {
    to: smartAccount.address,
    data: redeemDelegationCalldata,
    },
    ],
    maxFeePerGas: 1n,
    maxPriorityFeePerGas: 1n,
    });

Next steps