👤 Embedded Wallet Service
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 quickly find your use case. 👉
Quickstart
-
Configure EWS settings on Developer Dashboard: Auth Settings.
-
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>
-
Get the user object:
const user = await sdk.getUser();
-
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
Integration: 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
After saving, a code sample is provided that instantiates the SDK and prompts the user to log in.
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.
This feature is enabled for Enterprise customers only.
Integration: Instantiate the client SDK
First, instantiate 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
Integration: Authenticate the user
(Recommended) Prompt login with a prebuilt modal
Prompt the user to sign in with a prebuilt modal.
await sdk.auth.loginWithPaperModal();
This method prompts the user to continue with email or social login.

Prompt the user's email.

The user quickly verifies their email by providing a 6-digit code.
If the user is already logged in, this modal is skipped and the method returns immediately.
Reference: auth.loginWithPaperModal
(Advanced) Prompt email one-time passcode (OTP) modal
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 immediately.
Reference: auth.loginWithPaperEmailOtp
(Advanced) Provide your own one-time passcode (OTP) UX
Control when and how to email and verify the user's OTP with headless methods.
// Email the user an OTP.
const { isNewUser } = await sdk.auth.sendPaperEmailLoginOtp({
email: "[email protected]",
});
// Prompt the user for their OTP and optional recovery phrase.
if (isNewUser) {
const user = await Paper.auth.verifyPaperEmailLoginOtp({
email: "[email protected]",
otp: "123456",
});
} else {
const user = await Paper.auth.verifyPaperEmailLoginOtp({
email: "[email protected]",
otp: "123456",
recovery: "AbCdEfGHIJk",
});
}
Reference: auth.sendPaperEmailLoginOtp, auth.verifyPaperEmailLoginOtp
(Advanced) Log in with Custom JWT Authentication
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 a user out of their wallet
When a user logs out, they will be prompted to authenticate and provide their recovery phrase when they log in again.
await sdk.auth.logout()
Reference: auth.logout
Integration: Manage a user wallet
Check if a user is logged in and if so, get their auth information 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
Integration: Interact with the blockchain
Get an ethers.js-compatible 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
Change the user's existing chain
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();
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)
Ethers.js reference: sendTransaction
Call a smart contract
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 { user } = 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 publicly provided on the frontend. The domain allowlist ensures only your website can manage your app's user wallets. Without it, a bad actor could use your client ID and phish your users to gain access to their assets.
Local development environments (e.g. localhost:3000
) are safe because a bad actor would not be able to phish your user from their local machine.
Can I use EWS on a non-HTTPS domain?
EWS works only on secure origins. We recommend using http://localhost:* or a tool like ngrok for development, and an HTTPS domain on production.
Updated 18 days ago