Embedded Wallet Service (EWS) provides a JavaScript SDK that allows you to embed user wallets on any web-enabled platform or JS framework.

Tip: Use the Table of Contents to find what you need faster 👉

Quickstart

  1. Install @paperxyz/embedded-wallet-service-sdk.

  2. Configure Embedded Wallet settings on Developer Dashboard: Auth Settings.

  3. Copy the code snippet containing your Client ID. Call loginWithPaperModal() to sign a user into your app.

    import { PaperEmbeddedWalletSdk } from "@paperxyz/embedded-wallet-service-sdk";
    
    const sdk = new PaperEmbeddedWalletSdk({
      clientId: "MY_CLIENT_ID",
      chain: "Mumbai",
    });
    
    // Call when the user clicks your "Connect with Paper" button.
    <button onClick={() => sdk.auth.loginWithPaperModal()}>
      Connect with Paper
    </button>
    
  4. Get the user object:

    const user = await sdk.getUser();
    
  5. Make a gasless blockchain call (requires a Sponsored Fees balance).

    const params = {
      contractAddress: "0xb2369209b4eb1e76a43fAd914B1d29f6508c8aae",
      methodInterface: "function claimTo(address _to, uint256 _quantity, uint256 _tokenId) external",
      methodArgs: [ user.walletAddress, 1, 0 ],
    } as ContractCallInputType;
    const { transactionHash } = await user.gasless.callContract(params);
    

Prerequisites

  • Install the Embedded Wallet Service JS SDK with your preferred package manager:

    npm install @paperxyz/embedded-wallet-service-sdk
    
    yarn add  @paperxyz/embedded-wallet-service-sdk
    

Configure your application

On Developer Dashboard: Auth Settings, provide some details about your application.

  • App name: The name of your platform that will be shown to users in UI and emails.
  • Allowlisted domains: Domains that your app will be hosted on, including production and staging environments.
    • Set the first domain as your website URL
    • Localhost is supported: http://localhost:3000
    • Subdomain wildcards are supported: https://*.example.com

🛠

Advanced: Already have an authentication provider?

Select Use my own authentication method to log in with Custom JWT Authentication. You'll be prompted for a JWKS URI and AUD Value.

Initialize the client SDK

First, create the SDK on the client to create and manage user wallets.

import { PaperEmbeddedWalletSdk } from "@paperxyz/embedded-wallet-service-sdk";

const sdk = new PaperEmbeddedWalletSdk({
  clientId: "c1e1c50a-dde5-4411-83c6-7867ede4a3d5",
  chain: "Mumbai",
});

Reference: PaperEmbeddedWalletSdk

Authenticate the user

Prompt login with a prebuilt modal (Recommended)

Open a prebuild modal to prompt the user to sign in with email or social login.

await sdk.auth.loginWithPaperModal();
Prompt the user's email.

Prompt the user's email.

The user quickly verifies their email by providing a 6-digit code.

The user provides the 6-digit code sent to their inbox.

If the user is already logged in, this modal is skipped and the method returns.

This method throws an exception if the user closes the modal.

Reference: auth.loginWithPaperModal

Prompt email one-time passcode (OTP) modal (Advanced)

If the user's email is already known, email them a one-time passcode emailed and prompt them to verify it.

await sdk.auth.loginWithPaperEmailOtp({ email: "[email protected]" });

If the user is already logged in, this modal is skipped and the method returns.

Reference: auth.loginWithPaperEmailOtp

Prompt email one-time passcode (OTP) with your own UX (Advanced)

Control when and how to email and verify the user's OTP with headless methods.

const email = '...'; // Prompt the user's email

// Send the email an OTP.
const { isNewUser, isNewDevice } = await sdk.auth.sendPaperEmailLoginOtp({
  email,
});

const otp = '...'; // Prompt the user for the OTP.
// The recovery code is needed for returning users on a new device (isNewUser = false && isNewDevice = true).
const recovery '...'; 

// Verify the OTP code.
if (isNewUser || !isNewDevice) {
  await sdk.auth.verifyPaperEmailLoginOtp({
    email,
    otp,
  });
} else {
  await sdk.auth.verifyPaperEmailLoginOtp({
    email,
    otp,
    recovery,
  });
}

Reference: auth.sendPaperEmailLoginOtp, auth.verifyPaperEmailLoginOtp

Bring your own authentication with Custom JWT (Advanced)

To log in to your own custom authentication, call loginWithJwtAuth with the user's auth JWT after they have authenticated.

await sdk.auth.loginWithJwtAuth({
  token: "<token from your auth callback>",
  authProvider: AuthProvider.CUSTOM_JWT,
  recoveryCode: "Required if user is an existing user"
})

Reference: auth.loginWithJwtAuth

Log out

When a user logs out, they will be prompted to authenticate again. They will not be prompted to provide a recovery password again on this device.

await sdk.auth.logout()

Reference: auth.logout

Get a user's auth and wallet information

Check if a user is logged in and if so, get their auth details, wallet address, and wallet.

import { UserStatus } from "@paperxyz/embedded-wallet-service-sdk";

const result = await sdk.getUser();
switch (result.status) {
  case UserStatus.LOGGED_OUT: {
    // User is logged out.
    // Call `sdk.auth.loginWithPaperModal()` to log the user in.
    break;
  }
  case UserStatus.LOGGED_IN_WALLET_INITIALIZED: {
    // User is logged in.
    const { authDetails, walletAddress, wallet } = result.user;
    break;
  }
}

Reference: getUser

Interact with the blockchain

Get an ethers.js Signer

👍

Compatible with ethers.js

The SDK provides an ethers.js Signer, the same one you may already be using to support MetaMask, Coinbase Wallet, and other wallet providers.

There is no additional code to add support for EWS wallets!

const { user } = await sdk.auth.loginWithPaperModal();

const signer = await user.wallet.getEthersJsSigner({
  // Provide your RPC node URL on production.
  // Defaults to a public RPC node which is not recommended for production use.
  rpcEndpoint: "https://eth-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_API_KEY",
});

Reference: getEthersJsSigner

Switch the wallet's network

const { user } = await sdk.auth.loginWithPaperModal();
// switches future interaction to be based on the Mumbai chain
await user.wallet.setChain({ chain: "Mumbai" });

Reference: setChain

Sign a message

Sign a message to verify the user owns this wallet address.
This action does not incur gas fees.

Use cases:

  • Allow the user to update off-chain data, like their username or profile picture, associated with this wallet address.
  • Sign in with Ethereum

Here's an example of a user signing a message to verify they are the owner of that wallet address.

import { ethers } from "ethers";

// On frontend client:
// Get a signer for an authenticated user.
const { user } = await sdk.auth.loginWithPaperModal();
const signer = await user.wallet.getEthersJsSigner();

const payload = "myNewUsername";
const signature = await signer.signMessage(payload);
// Pass `payload` and `signature` to your backend.

// On your backend:
const walletAddress = ethers.utils.verifyMessage(payload, signature);
console.log(`${walletAddress} is updating their username to '${payload}'.`);
// "0x... is updating their username to 'myNewUsername'."

Ethers.js reference: signer.signMessage

Send a transaction

Send a transaction to write to the blockchain.
This action requires gas in the user's wallet.

Use cases:

  • Transfer the native coin or ERC-20 token to another wallet or contract.
  • List an NFT on a marketplace.
  • Transfer an NFT to another wallet.
  • Update metadata on an NFT.
import { ethers } from "ethers";

// Get signer for an authenticated user.
const { user } = await sdk.auth.loginWithPaperModal();
const signer = await user.wallet.getEthersJsSigner();

// simple example of sending the native chain coin to some other address
const tx = {
  to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
  value: ethers.utils.parseEther("0.1"),
};
const txResponse = await signer.sendTransaction(tx);
const txReceipt = await txResponse.wait();
console.log("Transaction sent:", txReceipt.transactionHash);

// if you want to call a smart contract 
const interface = new ethers.utils.Interface(["function executeSale(uint256 tokenId) public"]);
// encode the data to be sent to the contract
const dataToSend = interface.encodeFunctionData("executeSale", [tokenId]);
// send the transaction to write to the contract
const txResponseToContract = await signer.sendTransaction({to: '0x8ba1f109551bD432803012645Ac136ddd64DBA72', 
                                                     value: ethers.utils.parseEther("1"), 
                                                     data: dataToSend});
const txReceiptToContract = await txResponseToContract.wait()
console.log("Transaction sent:", txReceiptToContract.transactionHash);

Ethers.js reference: sendTransaction

Call a smart contract with sponsored gas fees

Send a transaction to write to a smart contract on the blockchain.

This action does not require gas in the user's wallet.
This action will require you to have a balance in the Sponsored Fees section on the Dashboard: Developers page.

Use cases:

  • Transfer the native coin or ERC-20 token to another wallet or contract.
  • List an NFT on a marketplace.
  • Transfer an NFT to another wallet.
  • Update metadata on an NFT.
// Get signer for an authenticated user.
const { initializedUser } = await sdk.auth.loginWithPaperModal();
const signer = await user.wallet.getEthersJsSigner();

const params = {
  contractAddress: "0xb2369209b4eb1e76a43fAd914B1d29f6508c8aae",
  methodInterface:  "function claimTo(address _to, uint256 _tokenId, uint256 _quantity) external",
  methodArgs: [initializedUser.walletAddress, 1, 1],
};
const { transactionHash } = await initializedUser.wallet.gasless.callContract(params);

Reference: gasless.callContract

FAQ

Why do I need a Client ID?

The Client ID scopes user wallets to your application. Example: [email protected] signs into App1 and App2 and gets different wallet addresses. App1 would not be able to retrieve or manage the user's wallet created under App2, and vice versa.

Why is the domain allowlist required?

The Client ID is provided on the frontend meaning it can be read by anyone. The domain allowlist ensures only your website can manage your app's user wallets. For instance, a bad actor might copy your client ID and build a similar-looking webpage. The domain allowlist prevents unspecified domains from hosting your login, tricking users to sign in, and transfer assets from those wallets.

Local development environments (e.g. localhost:3000) are safe because users cannot be tricked into loading a localhost link.

Can I use Embedded Wallet Service on a non-HTTPS domain?

No, Embedded Wallet Service relies on cryptography libraries that work only on secure origins. We recommend testing on http://localhost:<port> or with a tool like ngrok and hosting your production app on an HTTPS domain.