Skip to content

Walkthrough

It's time to learn how it all works! We recommend opening the starter app repository and walking through the components below to understand how it integrates Embedded Accounts.

1. The config

First, let's do our configuration in a file called config.ts.

/src/config.ts
// NOTE: feel free to change the chain here!
export const chain = arbitrumSepolia;
export const config = createConfig({
  // this is for requests to the specific chain RPC
  rpcUrl: "/api/rpc/chain/" + chain.id,
  signerConnection: {
    // this is for Alchemy Signer requests
    rpcUrl: "/api/rpc/",
  },
  chain,
  ssr: true,
  storage: cookieStorage,
});

Let's take note of three things in this configuration:

  1. First you'll notice we chose a specific chain definition here. You can change the chain variable and expect this to be reflected everywhere in the application.
  2. You'll also notice the rpcUrl points to API endpoints in our app. By doing this, the application does not expose the API key client side. We specify two different endpoints for the Alchemy Signer and general chain RPC calls to keep the behavior consistent with an Alchemy API endpoint.
  3. Lastly you'll notice we set Server Side Rendering (ssr) to be true for consistent behavior on the client/server in SSR apps. You can learn more about SSR here.

We set a few additional variables in the configuration so that we can refer to them throughout the application, see them here:

Additional config
/src/config.ts
// provide a query client for use by the alchemy accounts provider
export const queryClient = new QueryClient();
// configure the account type we wish to use once
export const accountType: SupportedAccountTypes = "MultiOwnerModularAccount";
// setup the gas policy for sponsoring transactions
export const gasManagerConfig: AlchemyGasManagerConfig = {
  policyId: process.env.NEXT_PUBLIC_ALCHEMY_GAS_MANAGER_POLICY_ID!,
};
// additional options for our account client
type SmartAccountClienOptions = z.infer<typeof SmartAccountClientOptsSchema>;
export const accountClientOptions: Partial<SmartAccountClienOptions> = {
  txMaxRetries: 20,
};

2. Setting up the provider

We're almost up to fun the part! There's just a bit more we need to do to setup our global context so we can use our react hooks everywhere.

First, we'll setup our initial client state in our root layout:

/src/app/layout.tsx
export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  // hydrate the initial state on the client
  const initialState = cookieToInitialState(
    config,
    headers().get("cookie") ?? undefined,
  );
 
  return (
    <html lang="en">
      <body className={inter.className}>
        <Providers initialState={initialState}>{children}</Providers>
      </body>
    </html>
  );
}

This initialState is, again, related to SSR.

And in the Providers, we'll make use of this initial state:

/src/app/providers.tsx
export const Providers = ({
  initialState,
  children,
}: PropsWithChildren<{
  initialState?: AlchemyAccountsProviderProps["initialState"];
}>) => {
  // providers:
  // 1. theme provider makes it easy to switch between light and dark mode
  // 2. alchemy account provider gives us access to react hooks everywhere
  return (
    <ThemeProvider attribute="class">
      <AlchemyAccountProvider
        config={config}
        queryClient={queryClient}
        initialState={initialState}
      >
        {children}
      </AlchemyAccountProvider>
    </ThemeProvider>
  );
};

Once we wrap the app in a configured AlchemyAccountProvider, we're all set to use account kit react hooks throughout our application!

3. Initial render

On the initial render of the application we'll want to know whether the user is already connected. We can make use of the useSignerStatus hook for this:

/src/app/page.tsx
export default function Home() {
  // use the various signer statuses to determine if we are:
  // loading - waiting for a request to resolve
  // connected - the user signed in with an email tied to a smart account
  // unconnected - we need to provide a login UI for the user to sign in
  const { isInitializing, isAuthenticating, isConnected, status } =
    useSignerStatus();
  const isLoading =
    isInitializing || (isAuthenticating && status !== "AWAITING_EMAIL_AUTH");
 
  return (
    <main className="flex min-h-screen flex-col items-center justify-center gap-4 p-24">
      {isLoading ? (
        <LoadingSpinner />
      ) : isConnected ? (
        <ProfileCard />
      ) : (
        <LogInCard />
      )}
      <ThemeSwitch />
    </main>
  );
}

In this case, if the user is connected we render the ProfileCard, or else we move to the LoginCard. Let's take a peek at the key parts of those two components next!

4. Authenticating (LoginCard)

In the LoginCard, we can create a nice UI to capture the user's email. We can make use of the useAuthenticate hook for kicking off the email authentication, and useSignerStatus again for checking to see if we kicked off that process.

/src/components/login-card.tsx
  const { authenticate } = useAuthenticate();
  const login = (evt: FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    authenticate({ type: "email", email });
  };
 
  const { status } = useSignerStatus();
  const isAwaitingEmail = status === "AWAITING_EMAIL_AUTH";

5. Send the user operation (ProfileCard)

Once the user is connected, we can send a user operation!

For this part, we can collect whatever information we need from the user to figure out what parameters to fill in the user operation. In this application we allow the user to specify the target address, the data to send to the address, as well as the value to send.

We'll get a client by using useSmartAccountClient with values provided from our config. Then we'll pass this client to useSendUserOperation which will provide us everything we need to know about the operations status.

/src/components/profile-card.tsx
  // use config values to initialize our smart account client
  const { client } = useSmartAccountClient({
    type: accountType,
    gasManagerConfig,
    opts,
  });
 
  // provide the useSendUserOperation with a client to send a UO
  // this hook provides us with a status, error, and a result
  const {
    sendUserOperation,
    sendUserOperationResult,
    isSendingUserOperation,
    error: isSendUserOperationError,
  } = useSendUserOperation({ client, waitForTxn: true });
 
  const send = (evt: FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
 
    // collect all the form values from the user input
    const formData = new FormData(evt.currentTarget);
    const target = formData.get("to") as Hex;
    const data = formData.get("data") as Hex;
    const value = formData.get("value") as string;
 
    // send the user operation
    sendUserOperation({
      uo: { target, data, value: value ? BigInt(value) : 0n },
    });
  };

Wrap up

That's everything for this quick start! And yet, there's still so much more to discover.

In this section we went over how to use just a few of the react hooks that are available to you in your application. If you would like to learn more about all the different react hooks that are available, go to the react hooks overview.

We also touched on the Alchemy Signer, as the key service that allows you to use embedded accounts. If you would like to learn more about how to use passkeys, configuring the user session or exporting private keys, go to the alchemy signer introduction.