Working with Stamp metadata
Please note:
In November 2024, we soft-launched Passport API v2.
We have not announced any deprecation or retirement timelines for v1 yet, but do strongly recommend using v2 moving forward.
We are actively working on updating the v1 tutorials to use v2.
You can learn more about the differences between v1 and v2 via our migration guide.
Passport XYZ Stamps show that a user has achieved certain milestones on-chain or through some web2 service. It is common for these Stamps to be used to generate a score on the Passport server to indicate a user's trustworthiness. It is also possible to retrieve the actual Stamps instead of the score. This enables developers to use their own algorithms to generate scores, or to make decisions based on ownership of specific Stamps or groups of Stamps.
In many cases app developers can serve their users better with additional information about the Stamps outside of just the verifiable credential and Stamp name. A simple example is access to a standardized icon image so that the Stamp can be displayed consistently across many applications.
For this reason, there are two ways to access Stamp metadata using the Passport API.
On this page, you will learn how to use the Stamp metadata API endpoints to retrieve Stamp metadata.
You can follow this walk-through to build a simple app that allows a user to connect their wallet and display their Stamps to the browser, using information provided in the Stamp metadata.
You can find the code from this tutorial in this GitHub repo (opens in a new tab):
Prerequisites
Before we delve into this, it's important to note that there are a few preliminary steps you need to complete. Please ensure that these prerequisites are met before proceeding with the guide.
- You have created a Passport Scorer and received a Scorer ID
- You have an API key
If you haven't completed the preliminary steps above please refer to our getting access guide first. Once you're done with that, return here and continue with this walkthrough.
API endpoints
There are two API endpoints that can be used to query Stamp metadata.
If you want to see the metadata for all available Stamps (the full set of possible Stamps, not only those that a user actually owns) you can use:
/registry/stamp_metadata
You can also retrieve the Stamps owned by a particular address and instruct the API to return the metadata for each Stamp along with the Stamps themselves. To do this, use:
/registry/stamps/{address}?_include_metadata=true
You can test this out using an HTTP tool such as curl. You will need to replace {your-api-key}
with your API key in the request header, as well as {your-address}
with your wallet address in order to access these endpoints.
curl --request GET 'https://api.scorer.gitcoin.co/registry/stamps/{your-address}?include_metadata=true' \
--header 'X-API-KEY: {your-api-key}'
Metadata structure
The metadata for every Stamp follows this basic format:
[
{
"id": "string",
"icon": "string",
"name": "string",
"description": "string",
"connectMessage": "string",
"groups": [
{
"name": "string",
"stamps": [
{
"name": "string",
"description": "string",
"hash": "string"
}
]
}
]
}
]
The /registry/stamp_metadata
endpoint simply returns an array of these metadata objects.
For the /registry/stamps/{address}?_include_metadata=true
endpoint, these metadata objects are bundled into the credential data object. For an individual Stamp, the complete data object looks as follows:
{
"version": "1.0.0",
"credential": {
"type": ["VerifiableCredential"],
"proof": {"jws": "eybhJGciOiJREZXTQSIsaaayaXQiOSLiYYJ0Il0sSmI22NI6ZmFsc2V9..z3RmZubBgrKoMGU4PVF88ddCP1TBn0v_VkPtldRmmszh13212vyq6rpEzKkpF49kKlYQ5TOfi2xwrSjP_2lAg",
"type": "Ed25519Signature2018",
"created": "2023-04-20T13:18:05.065Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:key:z5BkPGJAvLobLEdj1bgRLhS4YbSS1tn2zghvGHcRypNYU5LC#z6MkghvGHLobLEDj1bgRLhS4LPGJAvbMA1tn2zcRyqmPP5LC"},
"issuer": "did:key:z6MkghvGHLobLEdj1bgRLhS4LPGJAvbMA1tn2zcRyqmYU5LC", "@context": ["https://www.w3.org/2018/credentials/v1"],
"issuanceDate": "2023-04-20T13:18:05.065Z",
"expirationDate": "2023-07-19T13:18:05.065Z",
"credentialSubject": {
"id": "did:pkh:eip155:1:0x1D4098C948Dc41958Bf3A745deC77AE059C3aDF6",
"hash": "v0.0.0:bTARjOAhxkfaxgJ29i/sy2tbSNaev7eGROL2Dabq6mc=",
"@context": [{
"hash": "https://schema.org/Text",
"provider": "https://schema.org/Text"
}],
"provider": "TenOrMoreGithubFollowers"
}
},
"metadata": {
"group": "Followers",
"platform": {
"id": "Github",
"icon": "https://app.passport.xyzassets/githubWhiteStampIcon.svg",
"name": "Github",
"description": "Connect your existing Github account to verify.",
"connectMessage": "Connect Account"
},
"name": "TenOrMoreGithubFollowers",
"description": "Ten or more Github followers",
"hash": "0x38085c83331abefafaff9596aNb2e5f362b71b2974e251a5f290de561579037d"
}
}
When you use this Stamp object in your app, you will extract only the data you actually need.
Setting up a basic app
We'll create an app using Nextjs (opens in a new tab). We can bootstrap using create-next-app
. This automatically creates all the necessary subdirectories, configuration and boilerplate code required to get us building as quickly as possible.
Start by entering the following command into your terminal:
npx create-next-app passport-app
This will create a new directory called passport-app
and populate it with several sub-directories and files that form the skeleton of our app. create-next-app
will ask for yes/no responses to a series of configuration questions - answer as follows:
npx create-next-app my-passport-app
✔ Would you like to use TypeScript with this project? … Yes
✔ Would you like to use ESLint with this project? … Yes
✔ Would you like to use Tailwind CSS with this project? … No
✔ Would you like to use `src/` directory with this project? … No
✔ Would you like to use experimental `app/` directory with this project? …Yes
✔ What import alias would you like configured? … @/*
Next, change to the new my-passport-app
directory and install ethers (opens in a new tab):
npm install ethers
This tutorial will also use Chakra-UI (opens in a new tab) for styling, so install it using npm:
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
Now, create a new file in my-passport-app
and add the following to set the Passport API Key and Scorer ID values:
NEXT_PUBLIC_GC_API_KEY=<your-api-key>
NEXT_PUBLIC_GC_SCORER_ID=<your-scorer-id>
Save this file as .env.local
.
Now you are ready to start building your app. All the code that you will write for this app will be in the file src/app/page.tsx
. You can delete everything in that file, and replace it with the following:
'use client'
import { useState, useEffect } from 'react'
import { ethers } from 'ethers'
import { ChakraProvider, Button, Flex, Heading } from '@chakra-ui/react'
import { Image, SimpleGrid, Tooltip } from '@chakra-ui/react'
const APIKEY = process.env.NEXT_PUBLIC_GC_API_KEY
const headers = APIKEY ? ({
'Content-Type': 'application/json',
'X-API-Key': APIKEY
}) : undefined
export default function Passport() {
// here we deal with any local state we need to manage
const [address, setAddress] = useState<string>('')
useEffect(() => {
// setShowStamps(false)
checkConnection()
async function checkConnection() {
try {
const provider = new ethers.BrowserProvider(window.ethereum)
const accounts = await provider.listAccounts()
// if the user is connected, set their account
if (accounts && accounts[0]) {
setAddress(accounts[0].address)
}
} catch (err) {
console.log('not connected...')
}
}
}, [])
async function connect() {
try {
const accounts = await window.ethereum.request({ method: 'eth_requestAccounts' })
setAddress(accounts[0])
} catch (err) {
console.log('error connecting...')
}
}
const styles = {
main: {
width: '900px',
margin: '0 auto',
paddingTop: 90
}
}
return (
/* this is the UI for the app */
<div style={styles.main}>
<ChakraProvider >
<Flex minWidth='max-content' alignItems='right' gap='2' justifyContent='right'>
<Button colorScheme='teal' variant='outline' onClick={connect}>Connect Wallet</Button>
</Flex>
<br />
<br />
<Heading as='h1' size='4xl' noOfLines={2}>Gitcoin Stamp Collector</Heading>
</ChakraProvider >
</div >
)
}
This sets up the bare bones of the app. The API key is loaded from env.local
and added to a headers
object that can be used to configure requests. Then a Passport()
function is defined that returns a simple UI. Inside Passport()
are functions that enable the user to connect their Ethereum wallets using ethers
.
These functions won't be explained again in detail here. If you would like to learn more about the functions that enable a user to connect a wallet, you can read the Passport scoring deep dive tutorial.
Retrieving Stamps
First of all, you can define an interface
type that you will use to organize the useful Stamp data that you will use in your app. Paste the following type definition immediately after the definition of headers
, above where you define the Passport()
function.
interface Stamp {
id: number
name: string
icon: string
}
This Stamp
type had fields for the Stamp id
(a unique number used to make arrays of this type iterable), a name
which corresponds to the specific credential the Stamp relates to (not the provider, who can issue many credentials), and an icon
which is a URL linking to an image to be used to represent the Stamp provider.
Next, you can make a request to the registry/stamps/{address}
endpoint, remembering to include the include_metadata=true
qualifier. In Typescript, this can be achieved as follows:
const GET_PASSPORT_STAMPS_URI = `https://api.scorer.gitcoin.co/registry/stamps/${address}?include_metadata=true`
const response: Response = await fetch(GET_PASSPORT_STAMPS_URI, { headers })
const data = await response.json()
This will send an HTTP request to the endpoint, using the connected wallet's address in the URL. The response is received in JSON format, as shown earlier in the Metadata Structure
section.
However, to make use of this data you also have to parse the JSON and extract the useful pieces of information into an instance of the Stamp
type. You can do this by iterating over all the Stamps. For each Stamp, you can extract the name
and icon
data and create a unique id using a straight-forward counter that increments in each loop. These elements can be added to a Stamp
in each iteration, and each Stamp
appended to an array you can call StampDataArray
.
const stampDataArray = []
let counter = 0
for (const i of data.items) {
let st = { id: counter, stamp: i.credential.credentialSubject.provider, icon: i.metadata.platform.icon }
stampDataArray.push(st)
counter += 1
}
You also need a way to track this in your app's state. To do this, you'll create a state variable called stampArray
with an associated function setStampArray
. This will allow you to add the following to your code, after looping through data.items
.
setStampArray(stampDataArray)
You should also wrap this in try/catch
to handle any errors. Put all this together in a single function, getStamps()
, as follows:
async function getStamps() {
const stampProviderArray = []
const GET_PASSPORT_STAMPS_URI = `https://api.scorer.gitcoin.co/registry/stamps/${address}?include_metadata=true`
try {
const response: Response = await fetch(GET_PASSPORT_STAMPS_URI, { headers })
const data = await response.json()
// parse stamp data from json
let counter = 0
for (const i of data.items) {
let st = { id: counter, stamp: i.credential.credentialSubject.provider, icon: i.metadata.platform.icon }
stampProviderArray.push(st)
counter += 1
}
setStampArray(stampProviderArray)
return
} catch (err) {
console.log('error: ', err)
}
}
Paste the getStamps()
function into page.tsx
immediately below the connect()
function. You also need to define the stampArray
state variable - you can do this by pasting the following as the first item inside the Passport()
function.
const [stampArray, setStampArray] = useState<Array<Stamp>>([])
You also need a way to execute this function, as it is not actually called anywhere in the code yet. To do this, add a button that calls getStamp()
when clicked. The following can be added to page.tsx
immediately below the existing connect()
button near the bottom of the script.
<Button colorScheme='teal' variant='outline' onClick={getStamps}>Show Stamps</Button>
Using the Stamp metadata
At this point your code is able to connect a wallet and retrieve Stamp data for the connected user. However, your app is not doing anything with the data other than holding it in state. So, in this section you will write some code for displaying the Stamp data in a "Stamp collection" that the user can browse.
Start with the simplest part - adding a simple Boolean state variable to toggle displaying the Stamps on and off. Paste the following beneath your stampArray
state variable:
const [showStamps, setShowStamps] = useState<boolean>(false)
We can use this state variable to ensure that nothing is displayed until the user clicks Show Stamps
and adds Stamp data from the Passport API to the app's state. Add a call to setShowStamps(true)
to getStamps()
immediately below setStampArray
, above the return
statement.
Now, you an create a component that renders the Stamp data in an aesthetic way. The code snippet below uses a combination of Chakra-UI's SimpleGrid
and Image
components to display the icon for each Stamp. The way this is done is using map
to create an instance of the Image
component for each Stamp in stampArray
, passing the URL from the Stamp's icon
field as the image source. You can also add a fallback image that can be displayed in case a particular image fails to load.
const StampCollection = () => {
return (
<SimpleGrid minChildWidth='120px' spacing='40px' border='black'>
<>
{stampArray.map(s => <Image key={s.id} src={s.icon} alt={s.stamp} borderRadius='90px' boxSize='80px' fallbackSrc='gtc-logo.svg' backgroundColor='#C3D3D5' />)}
</>
</SimpleGrid >
)
}
There is one annoyance with this component: there is only one unique icon for each stamp provider, not every individual Stamp. This means that providers with multiple stamps will be represented by multiple instances of the same image, with no way to tell them apart.
To fix this, you can wrap the new Image
component in an outer Tooltip
component which you import from Chakra-UI. This will allow the user to hover their cursor over the Stamps to see the Stamp name in a small text box beneath the image. The text to display comes from the Stamp's name
field which was retrieved from the API.
The final component should look as follows, and you can paste it into the Passport()
app immediately above the definition of styles
.
const StampCollection = () => {
return (
<SimpleGrid minChildWidth='120px' spacing='40px' border='black'>
<>
{stampArray.map(s => <Tooltip key={s.id} label={s.stamp}><Image src={s.icon} alt={s.stamp} borderRadius='90px' boxSize='80px' fallbackSrc='gtc-logo.svg' backgroundColor='#C3D3D5' /></Tooltip>)}
</>
</SimpleGrid >
)
}
The final thing to add is a call to StampCollection
in the app's UI. You can add the following to page.tsx
immediately before the closing tag for ChakraProvider
.
{showStamps && <StampCollection />}
This renders the StampCollection
component when ShowStamps
evaluates to true
.
Run the app
You can now run the app and check that it works as expected. To do this, navigate to your project folder and run the following:
npm run dev
The app will start and you can access in your browser at http://localhost:3000
.
When you connect your app and click Show Stamps
your app will look as follows (assuming you actually have some Stamps associated with your address). The tooltip feature is demonstrated for a Github stamp.
Summary
This walk-through guide demonstrated how to retrieve and handle Stamp metadata. You learned how to build a simple app that displays Stamp data to the browser, making use of information in the metadata. Now you understand the basics, you can incorporate Stamp metadata into your own apps!