Build a Token-Gated Dapp
Build a token-gated dapp with SSX
Gating access to content based on a user's NFTs or tokens is a core-building block for building rich dapp experiences. For example:
- If a DAO wanted to limit content based on users with a certain amount of token-based voting power.
- If an NFT PFP project wanted to offer exclusive content to token holders.
- If a project wanted to enable early access to an application based on holding an NFT.
This example will show developers how to build and enable token-gated access in their dapp with SSX based on holding an ENS name.
Want to jump in and see a token-gated example in action quickly? Perform the following steps:
- Ensure SSX packages are installed from the
ssx
directory by runningyarn install
- Navigate to the example directory (
cd examples/ssx-test-token-gated
) - In your terminal, run:
yarn install
yarn start
Once run, you will be presented with a dapp using RainbowKit that prompts you to connect your wallet and Sign-In with Ethereum.

After signing in, if the wallet used owns an ENS name, you will be presented with the gated content. If not, the dapp should display "No ENS name found"

The following guide will teach you how to create a token-gated dapp enabled by SSX from our
create-ssx-dapp
package. The initial setup will be done using SSX's dapp creation tool. Type the following in your terminal to get started:
yarn create @spruceid/ssx-dapp token-gated-example
For this example, we will be using the following explicit options in the setup tool:
- Typescript
- Leave the other options empty when prompted

The
create-ssx-dapp
toolAn Alchemy account and API key will be necessary since
alchemy-sdk
is used to see if a user has an ENS name.To configure the key in the project, create a
.env
file and add the key there:# token-gated-example/.env
REACT_APP_ALCHEMY_API_KEY=YOUR_KEY
The Alchemy SDK dependency can be installed with the following command in the directory of your dapp:
yarn add alchemy-sdk
yarn add @rainbow-me/rainbowkit wagmi
Additionally, you will also need to add the
ssx-react
dependency. To add it, use this command:yarn add @spruceid/ssx-react
Head to
src/index.tsx
and add the following to plug things in initially:/** src/index.tsx **/
import { RainbowKitProvider, getDefaultWallets } from '@rainbow-me/rainbowkit';
import {
goerli,
mainnet,
configureChains,
createClient,
WagmiConfig,
} from 'wagmi';
import { alchemyProvider } from 'wagmi/providers/alchemy';
import { publicProvider } from 'wagmi/providers/public';
import { SSXProvider } from '@spruceid/ssx-react';
const { chains, provider } = configureChains(
[mainnet, goerli],
[
alchemyProvider({
// This is Alchemy's default API key.
// You can get your own at https://dashboard.alchemyapi.io
apiKey: `${process.env.REACT_APP_ALCHEMY_API_KEY}`,
}),
publicProvider(),
]
);
const { connectors } = getDefaultWallets({
appName: 'SSX ENS Token Gated Example',
chains,
});
const wagmiClient = createClient({
autoConnect: true,
connectors,
provider,
});
Wrap your
<App />
component with <WagmiConfig />
, <RainbowKitProvider/>
and <SSXProvider/>
components, making it look like the following:/* src/index.tsx */
root.render(
<React.StrictMode>
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<SSXProvider>
<App />
</SSXProvider>
</RainbowKitProvider>
</WagmiConfig>
</React.StrictMode>
);
At
src/App.tsx
some changes are required to hook SSX and RainbowKit together. Add the following imports and update the
App
component as the code below:/* src/App.tsx */
/* Add useEffect to the existing useState bracket */
import { useEffect, useState } from 'react';
import '@rainbow-me/rainbowkit/styles.css';
import {
useConnectModal,
useAccountModal,
ConnectButton,
} from '@rainbow-me/rainbowkit';
import { useSSX } from '@spruceid/ssx-react';
import { SSXClientSession } from '@spruceid/ssx';
import { useSigner } from 'wagmi';
/*....*/
function App() {
/* SSX hook */
const { ssx } = useSSX();
/* RainbowKit ConnectModal hook */
const { openConnectModal } = useConnectModal();
/* RainbowKit Account modal hook */
const { openAccountModal } = useAccountModal();
/* Some control variables */
const [session, setSession] = useState<SSXClientSession>();
const [loading, setLoading] = useState<boolean>(false);
const { data: provider } = useSigner();
useEffect(() => {
if (ssx && loading) {
/* Sign-in with SSX whenever the button is pressed */
ssx
.signIn()
.then(session => {
console.log(session);
setSession(session);
setLoading(false);
})
.catch(err => {
console.error(err);
setSession(undefined);
setLoading(false);
});
}
}, [ssx, loading]);
useEffect(() => {
if (!provider) {
setSession(undefined);
setLoading(false);
} else {
setLoading(true);
}
}, [provider]);
const handleClick = () => {
/* Opens the RainbowKit modal if in the correct state */
if (openConnectModal) {
openConnectModal();
}
/* Triggers the Sign-in hook */
setLoading(true);
};
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<span>SSX</span>
{openAccountModal && provider ? <ConnectButton /> : <></>}
</div>
<div className="App-title">
<h1>SSX Example Dapp</h1>
<h2>Connect and sign in with your Ethereum account</h2>
</div>
<div className="App-content">
{!openConnectModal && provider ? (
<>
<AccountInfo address={`${session?.address}`} />
</>
) : (
<button onClick={handleClick}>SIGN-IN WITH ETHEREUM</button>
)}
</div>
</div>
);
}
Finally, replace your CSS in
src/App.css
with the following:/* src/App.css */
.App {
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
color: white;
height: 100vh;
}
.App-content button {
border: none;
width: 100%;
padding: 16px 24px;
color: white;
background: linear-gradient(107.8deg, #4c49e4 11.23%, #26c2f3 78.25%);
border-radius: 12px;
cursor: pointer;
font-weight: 500;
font-size: 16px;
transition: all 150ms ease 0s;
margin: 16px 0px;
}
.App button:disabled {
pointer-events: none;
opacity: 0.7;
}
.App button:hover {
transform: scale(1.01);
}
.App-header {
width: calc(100% - 128px);
text-align: left;
padding: 16px 64px;
display: flex;
align-items: center;
background-color: #212121;
}
.App-header span {
font-weight: 600;
font-size: 32px;
margin-right: auto;
}
.App-title {
margin-top: auto;
}
.App-title h2 {
font-weight: 400;
font-size: 16px;
color: #667080;
}
.App-logo {
height: 40px;
pointer-events: none;
margin-right: 16px;
}
.App-content {
margin-bottom: auto;
width: 450px;
max-width: 100%;
background-color: rgba(39, 39, 39, 0.7);
backdrop-filter: blur(2px);
border-radius: 12px;
padding: 30px;
}
.App-content h1 {
font-size: 32px;
line-height: 48px;
}
.App-account-info {
margin-top: 16px;
padding: 16px 8px;
border: 1px solid #555555;
border-radius: 12px;
text-align: left;
}
.App-account-info b {
color: #667080;
}
The dapp has now been configured in a very basic state and will let you simply sign in using RainbowKit.
To token gate, we need to add some additional code and configuration.
First, let's configure the
alchemy-sdk
. Add the following to src/App.tsx
:/* src/App.tsx */
import { Network, Alchemy } from 'alchemy-sdk';
/**....**/
const alchemyConfig = {
/* This is the same you used previously for RainbowKit */
apiKey: process.env.REACT_APP_ALCHEMY_API_KEY,
/* Change this to the appropriate network for your usecase */
network: Network.ETH_MAINNET,
};
const alchemy = new Alchemy(alchemyConfig);
/**....**/
With the SDK configured, we now need to add the logic to verify if the signed-in address owns a token. In this case, we'll be gating based on an ENS name.
To accomplish this, tokens owned by the address must be fetched and filtered for ENS names. ENS names are under the contract
0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85,
so a variable with that value needs to be added. Add the following after the
import
statements:/* src/App.tsx */
const ENS_CONTRACT = '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85';
Including the modifications above to
src/App.tsx
, we can now add the additional logic needed to fetch tokens:/*....*/
+ import { Network, Alchemy } from "alchemy-sdk";
+ const alchemyConfig = {
+ /* This is the same you used previously for RainbowKit */
+ apiKey: process.env.REACT_APP_ALCHEMY_API_KEY,
+ /* Change this to the appropriate network for your usecase */
+ network: Network.ETH_MAINNET,
+ };
+ const alchemy = new Alchemy(alchemyConfig);
+ const ENS_CONTRACT = '0x57f1887a8bf19b14fc0df6fd9b2acc9af147ea85';
/*....*/
function App() {
/* SSX hook */
const { ssx } = useSSX();
/* RainbowKit ConnectModal hook */
const { openConnectModal } = useConnectModal();
/* RainbowKit Account modal hook */
const { openAccountModal } = useAccountModal();
/* Some control variables */
const [session, setSession] = useState<SSXClientSession>();
const [loading, setLoading] = useState<boolean>(false);
const { data: provider } = useSigner();
+ const [ownEnsName, setOwnEnsName] = useState(false);
useEffect(() => {
if (ssx && loading) {
/* Sign-in with SSX whenever the button is pressed */
ssx.signIn()
.then((session) => {
console.log(session);
+ alchemy.nft.getNftsForOwner(`${ssx.address()}`)
+ .then((nfts) => {
+ const ownENS = nfts.ownedNfts
+ .filter(({ contract }) => contract.address === ENS_CONTRACT)?.length > 0;
+ setOwnEnsName(ownENS);
setSession(session);
setLoading(false);
+ });
})
.catch((err) => {
console.error(err);
+ setOwnEnsName(false);
setSession(session);
setLoading(false);
});
}
}, [ssx, loading]);
useEffect(() => {
if (!provider) {
setSession(undefined);
setLoading(false);
} else {
setLoading(true);
}
}, [provider]);
const handleClick = () => {
/* Opens the RainbowKit modal if in the correct state */
if (openConnectModal) {
openConnectModal();
}
/* Triggers the Sign-in hook */
setLoading(true);
}
return (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<span>SSX</span>
+ {openAccountModal && ownEnsName && provider ? <ConnectButton /> : <></>}
</div>
<div className="App-title">
<h1>SSX Example Dapp</h1>
<h2>Connect and sign-in with your Ethereum account</h2>
</div>
<div className="App-content">
+ {!openConnectModal && ownEnsName && provider ? (
<>
<AccountInfo address={`${session?.address}`} />
+ <br></br>
+ <>You own an ENS name.</>
</>
) : (
+ <>
+ <button onClick={handleClick}>SIGN-IN WITH ETHEREUM</button>
+ <br></br>
+ {!openConnectModal && !ownEnsName && provider && !loading ? (
+ <>
+ <AccountInfo address={`${session?.address}`} />
+ <br></br>
+ No ENS name found.
+ </>
+ ) : (
+ <></>
+ )}
</>
)}
</div>
</div>
);
}
export default App;
Now you can gate any content with ENS names just by checking this
ownEnsName
variable. Gating access to content isn't only limited to tokens. You can even gate access based on different forms of on-chain activity like:
- The number of trades a user has made on Uniswap.
- How much lending activity a user has on Aave.
- The number of times a user voted on-chain in a DAO's governance process.
It doesn't stop there - this can even be extended to gating based on off-chain credentials, and more! We'll continue highlighting these examples and show how much is possible by using SSX.
Happy building!
Last modified 3mo ago