# ZeroDev Tutorial -- Passkeys (/onboarding/passkeys/tutorial)

> For the complete documentation index, see [llms.txt](/llms.txt)



In this tutorial, we will be building a Next.js app where your users can create smart accounts and send UserOps with [passkeys](/onboarding/passkeys/overview).

<div className="fd-steps">
  <div className="fd-step">
    ## Clone the template [#clone-the-template]

    We have prepared a [Next.js template](https://github.com/zerodevapp/passkey-tutorial) for you:

    ```bash
    git clone git@github.com:zerodevapp/passkey-tutorial.git
    ```

    If you ever want to check out the completed code, you can checkout [the `completed` branch](https://github.com/zerodevapp/passkey-tutorial/tree/completed).  You can also see a deployed demo [here](https://passkey-demo.zerodev.app/).

    Run the app in development mode:

    <Tabs items="[&#x22;npm&#x22;,&#x22;yarn&#x22;,&#x22;pnpm&#x22;,&#x22;bun&#x22;]">
      <Tab value="npm">
        ```bash
        npm i
        npm run dev
        ```
      </Tab>

      <Tab value="yarn">
        ```bash
        yarn
        yarn dev
        ```
      </Tab>

      <Tab value="pnpm">
        ```bash
        pnpm install
        pnpm dev
        ```
      </Tab>

      <Tab value="bun">
        ```bash
        bun install
        bun run dev
        ```
      </Tab>
    </Tabs>

    Open `app/page.tsx`.  We will be working on this file for the rest of the tutorial.
  </div>

  <div className="fd-step">
    ## Create a ZeroDev project [#create-a-zerodev-project]

    For this tutorial, [create a ZeroDev project](https://dashboard.zerodev.app) for Sepolia.

    On `app/page.tsx`, fill out these variables with the URLs found on your dashboard.

    ```ts
    const ZERODEV_RPC = ""
    const ZERODEV_RPC = ""
    const PASSKEY_SERVER_URL = ""
    ```
  </div>

  <div className="fd-step">
    ## Create a smart account using passkeys [#create-a-smart-account-using-passkeys]

    Let's hook up the `Register` and `Login` buttons so they actually do something.

    In `handleRegister`, add the following code:

    ```ts
    // Function to be called when "Register" is clicked
    const handleRegister = async () => {
      setIsRegistering(true)

      const webAuthnKey = await toWebAuthnKey({ // [!code focus]
                passkeyName: username, // [!code focus]
                passkeyServerUrl: PASSKEY_SERVER_URL, // [!code focus]
                mode: WebAuthnMode.Register, // [!code focus]
                passkeyServerHeaders: {} // [!code focus]
      }) // [!code focus]

      const passkeyValidator = await toPasskeyValidator(publicClient, {  // [!code focus]
        webAuthnKey,   // [!code focus]
        entryPoint,   // [!code focus]
        kernelVersion: KERNEL_V3_3,   // [!code focus]
        validatorContractVersion: PasskeyValidatorContractVersion.V0_0_3_PATCHED // [!code focus]
      })   // [!code focus]

      await createAccountAndClient(passkeyValidator)  // [!code focus]

      setIsRegistering(false)
      window.alert('Register done.  Try sending UserOps.')
    }
    ```

    And in `handleLogin`, add the following code:

    ```ts
    const handleLogin = async () => {
      setIsLoggingIn(true)

      const webAuthnKey = await toWebAuthnKey({ // [!code focus]
                passkeyName: username, // [!code focus]
                passkeyServerUrl: PASSKEY_SERVER_URL, // [!code focus]
                mode: WebAuthnMode.Login, // [!code focus]
                passkeyServerHeaders: {} // [!code focus]
      }) // [!code focus]

      const passkeyValidator = await toPasskeyValidator(publicClient, { // [!code focus]
        webAuthnKey,  // [!code focus]
        entryPoint, // [!code focus]
        kernelVersion: KERNEL_V3_3,   // [!code focus]
        validatorContractVersion: PasskeyValidatorContractVersion.V0_0_3_PATCHED // [!code focus]
      })  // [!code focus]

      await createAccountAndClient(passkeyValidator)  // [!code focus]

      setIsLoggingIn(false)
      window.alert('Login done.  Try sending UserOps.')
    }
    ```

    In this tutorial, we are using a public passkey server URL.  In practice, you'd create your own passkey server URL from [the dashboard](https://dashboard.zerodev.app/).

    Now modify `createAccountAndClient` to actually create the account using the `passkeyValidator`:

    ```ts
    const createAccountAndClient = async (passkeyValidator: any) => {
      kernelAccount = await createKernelAccount(publicClient, { // [!code focus]
        plugins: { // [!code focus]
          sudo: passkeyValidator, // [!code focus]
        }, // [!code focus]
        entryPoint, // [!code focus]
        kernelVersion: KERNEL_V3_1   // [!code focus]
      }) // [!code focus]

      kernelClient = createKernelAccountClient({ // [!code focus]
        account: kernelAccount, // [!code focus]
        chain: CHAIN, // [!code focus]
        bundlerTransport: http(ZERODEV_RPC), // [!code focus]
        client: publicClient, // [!code focus]
        paymaster: { // [!code focus]
          getPaymasterData: (userOperation) => { // [!code focus]
            const zerodevPaymaster = createZeroDevPaymasterClient({ // [!code focus]
              chain: CHAIN, // [!code focus]
              transport: http(ZERODEV_RPC), // [!code focus]
            }) // [!code focus]
            return zerodevPaymaster.sponsorUserOperation({ // [!code focus]
              userOperation, // [!code focus]
            }) // [!code focus]
          } // [!code focus]
        }, // [!code focus]
      }) // [!code focus]

      setIsKernelClientReady(true) // [!code focus]
      setAccountAddress(kernelAccount.address) // [!code focus]
    }
    ```

    At this point, you should be able to create passkey accounts with either `Register` or `Login`.
  </div>

  <div className="fd-step">
    ## Sending UserOps [#sending-userops]

    Sending UserOps from a passkey account is the same as sending them from any account.  Modify `handleSendUserOp` as such:

    ```ts
    const handleSendUserOp = async () => {
      setIsSendingUserOp(true)
      setUserOpStatus('Sending UserOp...')

      const userOpHash = await kernelClient.sendUserOperation({ // [!code focus]
          callData: await kernelAccount.encodeCalls([{ // [!code focus]
            to: contractAddress, // [!code focus]
            value: BigInt(0), // [!code focus]
            data: encodeFunctionData({ // [!code focus]
              abi: contractABI, // [!code focus]
              functionName: "mint", // [!code focus]
              args: [kernelAccount.address], // [!code focus]
            }), // [!code focus]
          }]), // [!code focus]
      }) // [!code focus]

      setUserOpHash(userOpHash)

      await kernelClient.waitForUserOperationReceipt({ // [!code focus]
        hash: userOpHash, // [!code focus]
      }) // [!code focus]

      // Update the message based on the count of UserOps
      const userOpMessage = `UserOp completed. <a href="https://jiffyscan.xyz/userOpHash/${userOpHash}?network=sepolia" target="_blank" rel="noopener noreferrer" class="text-blue-500 hover:text-blue-700">Click here to view.</a>`

      setUserOpStatus(userOpMessage)
      setIsSendingUserOp(false)
    }
    ```

    Now try sending some UserOps!

    Also, the UserOps are sponsored thanks to paymasters -- that's why you are able to send UserOps from an account with no ETH.
  </div>
</div>

## Next Steps [#next-steps]

In this tutorial, you were able to create smart accounts and send UserOps with passkeys.

For next steps:

* Check out [the core API](/onboarding/create-a-smart-account) to learn more about the SDK
* Learn more about [passkeys](/onboarding/passkeys/overview)
* Read some [code examples](https://github.com/zerodevapp/zerodev-examples) of using ZeroDev
