How To Build a Web3 Conference Attendee List App

Using Airstack, AI, POAP, Vite, and React

Justin Hunter
12 min readAug 9, 2023

In the old days of conference attendance, the list of people who attended the event was often something you could only get if you paid for sponsorship or bought the list after. However, with the advent of Proof of Attendance, it’s possible to not only commemorate the events you’ve attended in an easy-to-visualize way, but you can also get attendee lists after the fact.

Today, we’re going to build an app that lets you enter an event that you’ve attended and see a list of attendees in near-real-time. But before we do that, let’s talk about the proof of attendance protocol. The protocol allows people to mint NFTs representing their attendance at specific events. The POAP website says:

The Proof of Attendance Protocol turns precious moments into collectibles. Using blockchain technology, POAP tokenizes your memories, so they can last forever and be truly yours.

The protocol makes it easy to scalably and provably verify attendance at various events. This has both personal benefits in, as POAP says, turning moments into collectibles, and business benefits in that you can build prospect lists, network, and more by knowing who attended the same events as you.

Let’s dive into the tutorial!

Getting Started

We’re going to use Airstack’s free APIs to help us build our web3 conference attendee app. Airstack is a developer platform designed to make it easy to query web3 data and build applications around that data. Their tools are robust and impressive. Let’s see how we can use these APIs to build a list of attendees at a specific web3 event that issued POAPs.

We’re going to be using React to build our app, so be sure you have the following:

When you’re ready and have signed up for your Airstack account, go ahead and grab the API key from your account. You can do this by clicking on your username and clicking “View API Keys”. Copy the key and save it somewhere safe. We’ll need it soon.

Now, let’s build the foundation for our app.

Setting Up The React App

From your terminal/command line (I’m going to be calling it a terminal from here on out), make sure you change into the directory where you manage all your awesome dev projects. Then, run the following:

npm create vite@latest web3-conference-attendees -- --template react

If you’re on NPM 6, you’ll want to upgrade if you can or you can run this command:

npm create vite@latest web3-conference-attendees --template react

When that’s done, you’ll see steps printed in your terminal that you need to take.

Change into your new project directory:

cd web3-conference-attendees

Then, install dependencies:

npm install

And finally, you can run the basic starter app with:

npm run dev

But we don’t want a starter app, do we? Open another terminal window and let’s install another dependency. We’re going to install the Airstack React SDK. Run the following command:

npm install @airstack/airstack-react

When that’s done, we have the scaffolding to start building our app. I’m going to keep things simple and force you to write some raw CSS in this tutorial, but feel free to reach for whatever CSS framework you like if you’d prefer. Let’s get into building the UI.

Building The UI

Before we write any code, let’s think about what we’re trying to accomplish.

  • The person using the app wants to be able to enter a conference event that issues POAPs and see a list of users (wallets + metadata) that also attended the event
  • We should make it easy to enter conference information (we’ll use Airstack AI to help us here!)
  • The attendee list should be shown in a table format with information such as wallet address, web3 social accounts, and more

In a production app, we’d probably want to let the user enter a wallet address or connect their own so that we can build automated relationships and connections, but we’ll skip that in this tutorial. Instead, we’ll build an interface that doesn’t require authentication. We’ll connect the UI to the Airstack API soon.

Open your React project in your code editor. Immediately delete the App.css file. You’ll need to remove its reference from the App.js file as well. We’ll be using the index.css file for all our CSS.

Now, create a folder called components inside the src directory. Inside that folder, let’s create a few files that we know we’ll need. Take a look at our project requirements and the rough sketches. We will need at least:

  • NavBar
  • EventSelector
  • TableView

Create a file for each of those components inside your components folder. The NavBar component would be called NavBar.jsx and the EventSelector would be called EventSelector.jsx and so on and so forth.

We’ll build each component one at a time, starting with the NavBar, so open that file in your code editor. Add the following to that file:

import React from 'react'

const NavBar = () => {
return (
<div className="navbar">
<h3>POAPify</h3>
</div>
)
}
export default NavBar

This is a very simple file that builds our top navigation bar. It’s extendable so that if you do want to add a wallet connection, you could display the profile and sign out details here in this NavBar. For us, it’s just going to show the app name.

There’s not much here yet, but let’s remember to add this to our App.jsx file so we can render the navbar. Open that file and replace everything with this:

import NavBar from './components/NavBar'

function App() {
return (
<>
<NavBar />
</>
)
}
export default App

Open your app in your web browser and you won’t see much. Just the word “POAPify”. We’re going to need some styling. Let’s work on that now. Open your index.css file and replace everything in it with this:

body {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
min-height: 100vh;
background: #213547;
color: #fff;
}

.navbar {
display: flex;
flex-direction: row;
justify-content: space-between;
}

Now if you take a look at the app in your browser window, you’ll see it still looks bad…but less bad!

Let’s start building out the rest of the app. We’ll come back and connect functions and make API calls in a bit. In the App.jsx file, let’s add a view that contains our empty state and table view. Below the NavBar add the following:

<div className="main-view">
<div className="event-input">
<EventSelector />
</div>
<div>
<TableView />
</div>
</div>

We’ve now added the components that we previously created. Eventually, we’re going to pass props to these components, but for now, let’s just start building them out so we can see what they look like.

We’ll start with the EventSelector.jsx component. Open that file, and add the following:

import React from "react";
const EventSelector = (props) => {
return (
<div className="event-search">
<h3>Search for an event</h3>
<input
type="text"
value={props.eventId}
onChange={(e) => props.setEventId({ eventId: e.target.value })}
placeholder="Enter event ID to search"
/>
</div>
);
};

export default CommunitySelector;

It takes in two prop values and displays the event ID you want to search for. Remember, we’ll get the event ID thanks to the magic of AI and Airstack. We’ll also use AI to build our Airstack query, but that’s coming later.

We have one more component to build before we can start wiring everything up to the Airstack APIs. Let’s build our table component. Open up your TableView.jsx file and add the following:

import React from 'react'
import graph from "../assets/graph.png";

const TableView = (props) => {
const renderPopulated = () => {
return (
<div>
</div>
)
}
const renderEmpty = () => {
return (
<div className="empty-state">
<img src={graph} alt="Graph of knowledge" />
<h1>Enter an Event ID and see attendees now</h1>
<button disabled={!props.selectionsMade} onClick={props.searchAttendees}>
View now
</button>
</div>
);
};
return (
<div className="table">
{
props.results.length ?
<>{renderPopulated()}</> :
<>{renderEmpty()}</>
}
</div>
)
}
export default TableView

As you can see, we haven’t built out the populated view. We’ll start with the empty state, and connect to the API, then when we have data, we can build our populated view.

This component is rendering a nice empty state that has an image I generated with Midjourney. You’re welcome to use my image or any image you want. Here’s the image:

You’ll want to make sure your image is named the same as mine as added to the assets folder in the src directory of the project.

We are passing two props into this component. We’re sending the results of our search and the function to execute the search. Simple, right?

We have two things we need to do before we can see what this all looks like. Let’s add some CSS. Update your CSS file to include the following:

body {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
min-height: 100vh;
background: #213547;
color: #fff;
}

.navbar {
display: flex;
flex-direction: row;
justify-content: space-between;
}

.event-input {
display: flex;
flex-direction: row;
justify-content: space-between;
margin: auto;
}

.table {
width: 100%;
margin: auto;
min-height: 400px;
margin-top: 20px;
background: #dadada;
border-radius: 10px;
}
.empty-state {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-bottom: 50px;
color: #213547;
}
.empty-state img {
max-width: 25%;
border-radius: 50px;
margin-top: 50px;
}

And head over to your App.jsx file, and update the entire file to look like this:

import { useState } from "react";
import NavBar from './components/NavBar'
import EventSelector from "./components/EventSelector";
import TableView from "./components/TableView";

function App() {
const [variables, setVariables] = useState({
eventId: ""
})

return (
<>
<NavBar walletAddress={walletAddress} disconnect={disconnect} />
<div className="main-view">
<div className="event-input">
<EventSelector eventId={variables.eventId} setEventId={setVariables} />
</div>
<div>
<TableView eventId={variables.eventId} results={null} searchAttendees={() => console.log("Real function soon!")} />
</div>
</div>
</>
)
}
export default App

We’ve added placeholders for all the prop values and functions we are passing into our components. Our state variable is called variables and is formatted as an object because we will be passing it into our GraphQL query when we work with the Airstack SDK.

With this, you should be able to see what we’ve built so far!

This looks like an app now! We just need to connect to the Airstack API and render our table with the list of attendees for any event we search for.

Connecting to Airstack

Before we write any code, we should do something similar to what we did with the UI. We should think through our objectives and plan our course of action.

We know we want to take inputs and request a list of people who match the criteria we input. The input in this case is an event ID.

Fortunately, Airstack had a robust GraphQL API to support our query and many more. Their documentation is fantastic. Even better, they have an AI helper to help us build the query that we can then add to our app.

Build Smarter

We could read the docs and learn how to write our queries (and you should!), but we can also use AI to write the query for us. If you sign in to your Airstack account, you’ll notice an AI Assistant option at the top. Let’s see if we can get it to build a query for us that matches something we might use.

It’s actually incredible how well this works. The result of that natural language prompt was this GraphQL query:

query POAPEventAttendeesAndSocialProfiles {
Poaps(input: {filter: {eventId: {_eq: "65602"}}, blockchain: ALL, limit: 200}) {
Poap {
owner {
identity
socials {
profileName
userHomeURL
profileTokenUri
}
}
}
}
}

This bootstraps our API calls and speeds up development a ton. Play around with the AI assistant. It’ll come in handy the more you build with Airstack.

We’ll use the query the AI assistant generated, but we’ll have to dynamically set the event ID based on input. We should probably link out to Airstack’s AI query builder to help people look up the event IDs. Update the EventSelector.jsx file to include a link out to the Airstack explorer like this:

import React from "react";

const EventSelector = (props) => {
return (
<div className="event-search">
<h3>Search for an event</h3>
<p><a style={{color: "#fff", textDecoration: "underline"}} href="https://app.airstack.xyz/explorer" target="_blank" rel="noopener noreferrer">Use Airstack AI to look up event IDs</a></p>
<input
type="text"
value={props.eventId}
onChange={(e) => props.setEventId({eventId: e.target.value})}
placeholder="Enter event ID to search"
/>
</div>
);
};

export default EventSelector;

Now, let’s start building our API calls. At the top of your App.jsx file, import the Airstack SDK like this:

import { init, useLazyQueryWithPagination } from "@airstack/airstack-react";
init("YOUR AIRSTACK API KEY");

The init function allows you to make the request with your API key to increase the requests per minute rate limit.

We’re going to be using lazy querying with pagination because we need to manually trigger the query and we don’t know how many results we might get.

Now, in your main function component, add the following:

const [fetch, { data, loading, pagination, error }] = useLazyQueryWithPagination(query, variables, {});
const { hasNextPage, hasPrevPage, getNextPage, getPrevPage } = pagination;

I added this below my useState hooks.

Now, we need to build our query so we can trigger the request to the API when someone clicks the search button. Make sure this is place above the new useLazyQueryWithPagination hook:

  const query = `
query POAPEventAttendeesAndSocialProfiles($eventId: String!) {
Poaps(input: {filter: {eventId: {_eq: $eventId}}, blockchain: ALL, limit: 200}) {
Poap {
owner {
identity
socials {
profileName
userHomeURL
profileTokenUri
}
}
}
}
}

We are leveraging the variables state value that is getting passed into our fetch function and this query will automatically be updated to pass in the eventId when the network request is made.

Now, we just need to trigger the fetch call and return the results. The results are housed in the data variable from our Airstack hook.

Let’s update the TableView component in our App.jsx file to pass the fetch variable through to the component.

TableView eventId={variables.eventId} results={data?.Poaps?.Poap || []} searchAttendees={() => fetch()} />

Notice that with the results prop we are passing in the array of result which is nested in the API response. You can see the format in the Airstack Docs.

And that’s it! We actually have everything we need to fetch our data. Let’s try it. Open up the network tab in your browser’s developer tools, then enter an event ID. Hit the “Build Now” button and check the result of the network call.

If you’d prefer, you can add a console.log in the App.jsx file anywhere like this:

console.log(data)

Either way, you’ll see that we’re now fetching data. Let’s render it on the screen!

Open up the TableView.jsx file again, and let’s build a table. In the renderPopulated function, add the following:

const renderPopulated = () => {
return (
<div>
<button style={{marginBottom: 12, marginLeft: 10}} onClick={props.searchAttendees} className="btn">Reload table</button>
<table>
<thead>
<tr>
<th scope="col">User address</th>
<th scope="col" rowspan="2">
Social Usernames
</th>
</tr>
</thead>
<tbody>
{props.results.map((r) => {
return (
<tr key={r.owner.indentity}>
<td>
{r.owner.identity}
</td>
<td>{r.owner.socials && r.owner.socials.map(s => {
return (
<><span title={s.profileName}>{s.profileName.split(0, 20)}</span><br/></>)
})}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
};

Here we are mapping through the results of our attendee list and displaying the wallet address and any social profiles they might have.

Let’s see what this looks like when it’s all put together. But first, let’s add some CSS. Open up, your index.css file and add the following:

table {
border-collapse:collapse;
border:.5px solid #E8F1F2;
text-align:left;
margin: 0 auto;
}

table > caption {
text-transform:uppercase;
font-weight:bolder;
}
th {
background-color: #172A3A;
color:#fff;
padding:8px;
text-align:center;
}
tr:nth-child(even){
background-color:#E5EFF0;
}
td[data-style="bold"]{
font-weight:bold;
color:#11202C;
border-right:1px solid #C5C7D3;
}
td {
padding: 12px;
text-align:center;
color:#11202C;
border-bottom: 1px solid #C5C7D3;
}
tfoot {
background-color:#172A3A;
text-align:center;
color:#A4C8CB;
}
tbody > tr:hover {
background-color:#8DE2E2;
}

That should do it. I think we have enough for the big reveal!

The App In Action

Reload your app and enter an event ID, and you will see something like this:

This is pretty powerful when you think about the implications. You could use these results to go and follow these attendees on the web3 social platforms that they are a part of. You can airdrop things to them. You can bootstrap your own events. You can do so much with such a simple app.

With Airstack’s API, you can go a lot further. Profile images, POAP images, links to other NFT tokens these people hold, and more.

Conclusion

POAPs have been a great tool in commemorating events that people have attended. But there’s so much more opportunity for networking and prospecting. Using Airstack’s AI assistant and their API, we can build a tool that helps you see and connect with attendees of any event simply by checking to see who holds the POAPs for the event.

If you’re ready to build more powerful event prospecting tools and leverage POAPs in a whole new way, I hope this tutorial has been helpful. If you build anything after reading this, be sure to share it.

How can you take this app further? There are a bunch of things you can do including, but not limited to:

  • Adding pagination in (remember, we already have support for it with the useLazyQueryWithPagination hook we used.
  • Add a wallet connection option and automatically find connections between the authenticated address and POAPs in the connected wallet.
  • Show profile and POAP images
  • Add a CSV export option
  • And so much more!

If you’re interested in the full code for this project, it can be found here.

--

--