# Vollector partner integration

Put a "Connect with Vollector" 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. It takes one endpoint and no account system on your end.

Docs home: https://vollector.id/connect/docs

## 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>
```

- `p` is the partner id we issue you.
- `d` is your claim, JSON, base64url encoded.
- `s` is `HMAC-SHA256(secret, "v1." + partner_id + "." + d)`, base64url encoded.

## The claim

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| v | number | yes | Format version. Always 1 today. |
| wallets | array | yes | Up to 10 entries of { address, chain }. |
| nonce | string | yes | Unique per link, at least 8 characters. Redeeming burns it. |
| iat | number | yes | Issued at, seconds since epoch. |
| exp | number | yes | Expiry, seconds since epoch. At most 600 seconds after iat. |
| uid | string | no | Your own id for this user. Opaque to us, never shown. |
| redirect_uri | string | no | 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.

```js
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:

```json
{
  "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.

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

The mark is at https://vollector.id/vollector-mark.svg, and you can also copy it from
https://vollector.id/connect/docs. Implement it however you want, wherever you want.

## 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" above.

Contact: @vollectorid on X (https://x.com/vollectorid), or hi@vollector.id.
