# ZeroDev Tutorial -- Passkeys (/sdk/v5_3_x/getting-started/tutorial-passkeys)

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



<VersionWarning version="5.3.x" />

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

## 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.  You can also see a deployed demo [here](https://passkey-demo.zerodev.app/).

Run the app in development mode:

```bash
npm i
npm run dev
```

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

## 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 BUNDLER_URL = ""
const PAYMASTER_URL = ""
const PASSKEY_SERVER_URL = ""
```

## 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(BUNDLER_URL), // [!code focus]
    entryPoint, // [!code focus]
    middleware: { // [!code focus]
      sponsorUserOperation: async ({ userOperation }) => { // [!code focus]
        const zerodevPaymaster = createZeroDevPaymasterClient({ // [!code focus]
          chain: CHAIN, // [!code focus]
          transport: http(PAYMASTER_URL), // [!code focus]
          entryPoint, // [!code focus]
        }) // [!code focus]
        return zerodevPaymaster.sponsorUserOperation({ // [!code focus]
          userOperation, // [!code focus]
          entryPoint, // [!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`.

## 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]
    userOperation: { // [!code focus]
      callData: await kernelAccount.encodeCallData({ // [!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]
  }) // [!code focus]

  setUserOpHash(userOpHash)

  const bundlerClient = kernelClient.extend( // [!code focus]
    bundlerActions(entryPoint) // [!code focus]
  ) // [!code focus]
  await bundlerClient.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.

## 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](/sdk/v5_3_x/core-api/create-account) to learn more about the SDK
* Learn more about [passkeys](/sdk/v5_3_x/advanced/passkeys)
* Read some [code examples](https://github.com/zerodevapp/zerodev-examples) of using ZeroDev
