Connect with Vollector
Put a Connect button on your platform. Your user presses it, signs in with X on our side, and the wallets you already know are theirs land on their Vollector profile. One endpoint, no account system on your end.
Prefer plain text, or handing this to an assistant? Read it as markdown.
What this is for
You already know which wallets belong to which user. You send us that list, signed with your secret, and we take it as given: the wallets in a claim are that user's. There is nothing for the collector to paste, export or sign.
Identity stays on our side. They sign in with X here, so you are never asked for a handle and never have to store one.
The link
One URL, built server side. No token exchange, no OAuth round trip, nothing to store on your end.
https://vollector.id/connect?p=<partner_id>&d=<payload>&s=<signature> pis the partner id we issue you.dis your claim, JSON, base64url encoded.-
sisHMAC-SHA256(secret, "v1." + partner_id + "." + d), base64url encoded.
The claim
| Field | Type | Notes | |
|---|---|---|---|
| v | number | required | Format version. Always 1 today. |
| wallets | array | required | Up to 10 entries of { address, chain }. |
| nonce | string | required | Unique per link, at least 8 characters. Redeeming burns it. |
| iat | number | required | Issued at, seconds since epoch. |
| exp | number | required | Expiry, seconds since epoch. At most 600 seconds after iat. |
| uid | string | optional | Your own id for this user. Opaque to us, never shown. |
| redirect_uri | string | optional | Where the Back button sends them. Must be an origin you registered. |
Supported chains: solana, ethereum, polygon, base, abstract, bsc, evm.
Reference implementation
Node, and nothing outside its standard library. Any language with HMAC-SHA256 and base64url does the same job.
import { createHmac, randomBytes } from "node:crypto";
const PARTNER_ID = "your-partner-id";
const SECRET = process.env.VOLLECTOR_SECRET;
export function connectUrl(wallets, opts = {}) {
const iat = Math.floor(Date.now() / 1000);
const claim = {
v: 1,
wallets, // [{ address, chain }]
nonce: randomBytes(16).toString("hex"),
iat,
exp: iat + 600,
uid: opts.uid, // your id for this user, optional
redirect_uri: opts.redirectUri // must be a registered origin, optional
};
const d = Buffer.from(JSON.stringify(claim)).toString("base64url");
const s = createHmac("sha256", SECRET)
.update(`v1.${PARTNER_ID}.${d}`)
.digest("base64url");
return `https://vollector.id/connect?p=${PARTNER_ID}&d=${d}&s=${s}`;
} Test vector
Run your implementation against this before you call us. If your signing input and your signature match these strings, your integration is correct and any remaining problem is on our side. The secret below is published here, so it is valid for nothing.
- partner_id
- example
- secret
- vollector-test-secret
- claim
-
{ "v": 1, "wallets": [ { "address": "0x8a981c2cfdd7fbc65395dd2c02ead94e9a2f65a7", "chain": "base" }, { "address": "9WzDXwBbmkg8ZTbNMqUxvQRAyrZzDsGYdLVL9zYtAWWM", "chain": "solana" } ], "nonce": "b7c1e0a2f4d94e3a", "iat": 1767225600, "exp": 1767226200, "uid": "user_12345", "redirect_uri": "https://example.com/settings?vollector=done" } - d (base64url payload)
- eyJ2IjoxLCJ3YWxsZXRzIjpbeyJhZGRyZXNzIjoiMHg4YTk4MWMyY2ZkZDdmYmM2NTM5NWRkMmMwMmVhZDk0ZTlhMmY2NWE3IiwiY2hhaW4iOiJiYXNlIn0seyJhZGRyZXNzIjoiOVd6RFh3QmJta2c4WlRiTk1xVXh2UVJBeXJaekRzR1lkTFZMOXpZdEFXV00iLCJjaGFpbiI6InNvbGFuYSJ9XSwibm9uY2UiOiJiN2MxZTBhMmY0ZDk0ZTNhIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjYyMDAsInVpZCI6InVzZXJfMTIzNDUiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2V4YW1wbGUuY29tL3NldHRpbmdzP3ZvbGxlY3Rvcj1kb25lIn0
- signing input
- v1.example.eyJ2IjoxLCJ3YWxsZXRzIjpbeyJhZGRyZXNzIjoiMHg4YTk4MWMyY2ZkZDdmYmM2NTM5NWRkMmMwMmVhZDk0ZTlhMmY2NWE3IiwiY2hhaW4iOiJiYXNlIn0seyJhZGRyZXNzIjoiOVd6RFh3QmJta2c4WlRiTk1xVXh2UVJBeXJaekRzR1lkTFZMOXpZdEFXV00iLCJjaGFpbiI6InNvbGFuYSJ9XSwibm9uY2UiOiJiN2MxZTBhMmY0ZDk0ZTNhIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjYyMDAsInVpZCI6InVzZXJfMTIzNDUiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2V4YW1wbGUuY29tL3NldHRpbmdzP3ZvbGxlY3Rvcj1kb25lIn0
- s (signature)
- fjL7l4tymKmO6lHglIY_Ga5mkQ_LbNE9fEilWrDChvA
- full URL
- https://vollector.id/connect?p=example&d=eyJ2IjoxLCJ3YWxsZXRzIjpbeyJhZGRyZXNzIjoiMHg4YTk4MWMyY2ZkZDdmYmM2NTM5NWRkMmMwMmVhZDk0ZTlhMmY2NWE3IiwiY2hhaW4iOiJiYXNlIn0seyJhZGRyZXNzIjoiOVd6RFh3QmJta2c4WlRiTk1xVXh2UVJBeXJaekRzR1lkTFZMOXpZdEFXV00iLCJjaGFpbiI6InNvbGFuYSJ9XSwibm9uY2UiOiJiN2MxZTBhMmY0ZDk0ZTNhIiwiaWF0IjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjYyMDAsInVpZCI6InVzZXJfMTIzNDUiLCJyZWRpcmVjdF91cmkiOiJodHRwczovL2V4YW1wbGUuY29tL3NldHRpbmdzP3ZvbGxlY3Rvcj1kb25lIn0&s=fjL7l4tymKmO6lHglIY_Ga5mkQ_LbNE9fEilWrDChvA
Trying it live
Add &dry=1 to any connect link. It checks the
link and reports what we saw, without signing anyone in and without writing anything.
It works before your integration is switched on, so you can build the whole thing
against it.
When something is rejected
| Reason | What it means |
|---|---|
| bad_signature | The HMAC does not match. Check the signing input, not the payload. |
| malformed | The payload is not base64url JSON, or a required field is missing. |
| bad_version | The v field is not 1. |
| expired | exp is in the past. Links last ten minutes. |
| not_yet_valid | iat is more than a minute in the future. Usually a clock problem. |
| ttl_too_long | exp is more than 600 seconds after iat. |
| no_wallets | The wallets array is empty. |
| too_many_wallets | More than 10 wallets in one link. |
| bad_wallet | A wallet has no address. |
| bad_chain | A wallet is on a chain we do not index. |
Users see plain language, not these codes. The dry run shows you the code.
Getting your credentials
Tell us your platform name and the exact origins we should allow as return URLs, and we send back a partner id and a secret. Keep the secret on your server, never in client code. It can be rotated at any time, and the old one keeps working alongside the new one until you tell us you have finished deploying.
You can build and test the whole integration with a dry run before we switch you on. See Trying it live.
hi@vollector.id, or @vollectorid on X.
Get in touch