Building a custom, client-side scorer
Passport XYZ offers several scoring algorithms that can be executed on the Passport servers, such that a numeric score for a Passport can be requested from the Passport API. However, this means you are restricted to Passport's algorithm and Passport's opinion about the relative weighting assigned to each individual Stamp. This might not be appropriate for all use cases.
For example, you might have a string preference for certain Stamps that are particularly relevant to your community that you want to weight more strongly in the scoring, or perhaps you have a great idea for a completely new algorithm that you want to implement to gate your app.
This tutorial will walk you through developing a custom scorer for your app.
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.
Creating a Scorer
Setting up a basic app
This tutorial will build on the Stamp Collector app tutorial. You should revisit that tutorial to build the foundations upon which this tutorial will build. You will use that app as a starting point and add scoring functionality on top. Follow the instructions in that tutorial to get the app up and running.
You can start the app now by navigating your terminal to the project root directory and running npm run dev
. Then, navigate your browser to localhost:3000
. You will see the app load in the browser, with buttons that enable you to connect your wallet and check your Stamps. You can go ahead and test that the Connect Wallet
and Show Stamps
buttons are working as expected.
The rest of the tutorial will build upon this basic app by adding functions and UI code to app/page.tsx
.
Stamps
The app queries the Passport APIs registry/stamps
endpoint to retrieve all the Stamps owned by the connected user. The getStamps()
function parses the full response and extracts the icon
, id
, and stamp
data into a Stamp
object with the following structure:
interface Stamp {
id: number
stamp: string
icon: string
}
This information is stored in a state variable, stampArray
. This is all the information you need from the Passport API.
Scorers
The Passport XYZ scoring algorithm is a simple sum of weights assigned to each Stamp. The weights are provided in a file on the Passport Github (opens in a new tab). Each weight is a decimal number associated with a specific Stamp name. The scoring algorithm simply iterates over the Stamp names for the Stamps owned by an address, retrieves the associated weights, and adds them together. The result is the user's Passport score.
The Passport Scorer
You can re-implement the Passport scoring algorithm easily in your app. Start by adding a file containing the Passport XYZ Stamp weights to your app
directory. Copy the contents of this file (opens in a new tab) and paste it into a new file app/stamp-weights.ts
.
Now, import the data into your app by adding the following import statement to app.ts
:
import { GITCOIN_PASSPORT_WEIGHTS } from './stamp-weights';
You can also add two state variables: one to store the Passport score and one to toggle displaying the score in the UI:
const [score, setScore] = useState<number>()
const [showScore, setShowScore] = useState<boolean>(false)
Now, you can create a function to calculate the score. Name the function calculateGitcoinScore()
to differentiate from a custom scorer you will create later. Inside, iterate over the Stamps
in stampArray
. Extract the name (the stamp
field) from each Stamp
and use it to look up the weight in GITCOIN_PASSPORT_WEIGHTS
. Then add those together and log it to the console.
function calculateGitcoinScore() {
let i = 0
var scores: Array<number> = []
var score = 0;
while (i < stampArray.length) {
let id = stampArray[i].stamp
if (GITCOIN_PASSPORT_WEIGHTS.hasOwnProperty(id)) {
try {
let temp_score = GITCOIN_PASSPORT_WEIGHTS[id]
scores.push(parseFloat(temp_score))
} catch {
console.log("element cannot be added to cumulative score")
}
}
i++;
}
for (let i = 0; i < scores.length; i++) {
score += scores[i]
}
setShowScore(true)
setScore(score)
}
Also add a button to invoke the new function (this can be added immediately after the current button definitions in the UI code, near line 127):
<Button colorScheme='teal' variant='outline' onClick={calculateGitcoinScore}>Get Score</Button>
Now add a simple Score
component:
const Score = () => {
return (
<>
<p> Your score is {score}</p>
</>
)
}
Finally, conditionally render your Score
component if showScore
is toggled ON. Add the following immediately below {showStamps && <StampCollection />}
near line 144.
<br />
<br />
<br />
{showScore && <Score />}
Now when you click the Get Score
button in your UI, the sentence Your score is X
is displayed under your Stamp collection! You can check this against the score given to you by the Passport XYZ app!
Custom weights
You can experiment with different weights simply by updating the values in stamp-weights.ts
. As a simple demonstration, choose one of the Stamps you own and increase its value in stamp-weights.ts
by 10. Next time you click Get Score
your Passport Score will have increased by 10 too. You can experiment with weighting models that best serve your app. For example if you value web3 Stamps most strongly, you might increase their weighting relative to web2 Stamps. You can even set some Stamps to zero if you feel they are not relevant for controlling access to your content.
Custom algorithms
You can also implement your own scoring algorithm. To do this, simply create a new function similar to calculateGitcoinScore
but implement some new logic in the function body. In this tutorial you will siumply replace the summation of weights with the mean. Later, you might design some complex model to implement.
You will repeat the steps from the calculateGitcoinScore()
. Start by adding new state variables:
const [customScore, setCustomScore] = useState<number>()
const [showCustomScore, setShowCustomScore] = useState<boolean>(false)
Next create a new function, getCustomScore
. To update the score from a sum to a mean, you just need to divide the sum by the number of Stamps. You can just add a line to do this at the end of the function. Remember to update the newly created state variables!
function calculateCustomScore() {
let i = 0
var scores: Array<number> = []
var score = 0;
while (i < stampArray.length) {
let id = stampArray[i].stamp
if (GITCOIN_PASSPORT_WEIGHTS.hasOwnProperty(id)) {
try {
let temp_score = GITCOIN_PASSPORT_WEIGHTS[id]
scores.push(parseFloat(temp_score))
} catch {
console.log("element cannot be added to cumulative score")
}
}
i++;
}
for (let i = 0; i < scores.length; i++) {
score += scores[i]
}
const mean = score / stampArray.length
setShowCustomScore(true)
setCustomScore(mean)
}
Now create a CustomScore
component:
const CustomScore = () => {
return (
<>
<p> Your custom score is {customScore}</p>
</>
)
}
And a button:
<Button colorScheme='teal' variant='outline' onClick={calculateCustomScore}>Get Custom Score</Button>
And finally add the conditional rendering to the UI code:
<br />
<br />
<br />
{showCustomScore && <CustomScore />}
Now your app has buttons to show the Stamps, get a Passport score and get your custom score. All this information will be displayed in the app's UI.
Deduplication
Please note that scoring on the Passport server includes Stamp deduplication. This means the server automatically detects when the same instance of a Stamp has been submitted more than once to a specific Scorer instance and ignores any duplicates.
While it is out of scope for this tutorial, you should implement your own deduplication to accompany a custom scorer.
This requires logging the hashes of Stamps and checking that each hash is unique to each individual user.
Next Steps
Now you know how Passport calculates its scores and have seen how to update the logic, you can be creative in implementing algorithms that best serve your community's needs.