Skip to main content

Creating a verifier

A verifier is a class that extends one of the two base classes in services/verifier-bot/src/verifier.ts and is exported as part of a Platform. Add a file under services/verifier-bot/src/platforms/ and register it in platforms.ts.

Claim data is carried as ClaimFields ({ key: number; value: string }); most platforms use a single field with key: 0 holding the username/handle.

Text verifier

Extend TextVerifier when ownership can be proven by the user placing their token in a public bio/description. Implement getText (fetch the bio for a claim field) and getClaimFieldsByUrl (derive claim fields from a profile URL). Optional testData* arrays drive the health-check endpoint.

import type { ClaimField, Platform } from '../models.js';
import { Result } from '../result.js';
import { createCookieEnabledAxios } from '../utility.js';
import {
TextVerifier,
type TextVerifierGetClaimFieldsTestData,
} from '../verifier.js';

class GithubTextVerifier extends TextVerifier {
protected testDataGetClaimFields: TextVerifierGetClaimFieldsTestData[] = [
{ url: 'https://github.com/futo-org', expectedClaimFields: [{ key: 0, value: 'futo-org' }] },
];

constructor() {
super('GitHub');
}

protected async getText(claimField: ClaimField): Promise<Result<string>> {
const client = createCookieEnabledAxios();
const profile = await client({ url: `https://api.github.com/users/${claimField.value}` });
if (profile.status !== 200) {
return Result.err({ message: 'Unable to find your account' });
}
return Result.ok(profile.data.bio.trim());
}

public async getClaimFieldsByUrl(url: string): Promise<Result<ClaimField[]>> {
const match = /https:\/\/(?:www\.)?github\.com\/([^/]+)\/?/.exec(url);
if (!match) return Result.err({ message: 'Failed to match regex' });
return Result.ok([{ key: 0, value: match[1] }]);
}
}

export const Github: Platform = {
name: 'GitHub',
verifiers: [new GithubTextVerifier()],
version: 1,
};

For platforms with anti-bot protection where a plain HTTP request won't return the bio, fetch the page through the shared Puppeteer browser instead (see kick.ts). Prefer a plain request whenever it works — the headless browser is slower and less reliable.

OAuth verifier

Extend OAuthVerifier<T> when the user proves ownership by signing in. Implement getOAuthURL (where to send the user), getToken (exchange the OAuth code, returning the authenticated username), and isTokenValid (confirm the token's account matches the claim). Read credentials from process.env.POLYCENTRIC_VERIFIER_BOT_*.

import type { ClaimField, Platform, TokenResponse } from '../models.js';
import { Result } from '../result.js';
import { getCallbackForPlatform } from '../utility.js';
import { OAuthVerifier } from '../verifier.js';

type DiscordTokenRequest = { code: string };

class DiscordOAuthVerifier extends OAuthVerifier<DiscordTokenRequest> {
constructor() {
super('Discord');
}

public async getOAuthURL(): Promise<Result<string>> {
const clientId = process.env.POLYCENTRIC_VERIFIER_BOT_DISCORD_CLIENT_ID;
if (!clientId) return Result.errMsg('Verifier not configured');
const redirectUri = getCallbackForPlatform(this.platform, true);
return Result.ok(
`https://discord.com/api/oauth2/authorize?client_id=${clientId}` +
`&redirect_uri=${redirectUri}&response_type=code&scope=identify`,
);
}

public async getToken(data: DiscordTokenRequest): Promise<Result<TokenResponse>> {
// Exchange data.code for an access token, look up the account, and return
// { username, token }.
}

public async isTokenValid(challenge: string, claimFields: ClaimField[]): Promise<Result<void>> {
// Decode the token from `challenge`, fetch the account, and compare its
// username to claimFields[0].value.
}

public healthCheck(): Promise<Result<void>> {
throw new Error('Method not implemented.');
}
}

export const Discord: Platform = {
name: 'Discord',
verifiers: [new DiscordOAuthVerifier()],
version: 1,
};

See discord.ts for a complete getToken / isTokenValid implementation.

Register the platform

Add the exported Platform to the platforms array in services/verifier-bot/src/platforms/platforms.ts. Routes, /platforms listings, and health checks are generated automatically from that array and each verifier's platform name and verifierType.