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 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.

The button

Implement it however you want, wherever you want.

Vollector

The mark, to use in your integration.

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.