Skip to content

Vouching and sybil resistance

A passkey proves the same device came back. It does not prove there is one person behind it. Omxus gets at that with vouching: people who are already trusted sign that someone else is a real, single human.

Vouching lives in two places, and they do not always agree:

  • The provider. auth.omxus.com keeps a vouch graph and a trust score for every address. This is what /vouch/* answers from.
  • The chain. The HER contract on Ethereum mainnet (0x262a86d8cdE379BF95B6A73fDe4EbfEC95f34C77) mints a non-transferable token to a person once three verified people have vouched for them.

Every answer that could mean either carries both, kept apart. Say "verified on chain" only when the onchain part says so.

Trust on the provider

Trust is a number from 0 to 100, worked out from the whole graph every time it is read.

  • It starts at a core. A set of founding addresses is pinned at 100 (TRUST_CORE). Everyone else starts at 0.
  • A vouch is worth the voucher's own trust. A voucher at 100 adds up to 40 points; a voucher at 50 adds up to 20; a voucher at 0 adds nothing. So a ring of strangers vouching each other stays at 0: no path to the core, no trust.
  • Verified at 60. In practice that is two vouches from fully trusted people.
  • Never below 0. Nobody can vote you down. Trust falls only when vouches are withdrawn or age out, and it stops at 0.
  • Vouches age by quarter. A vouch counts in full in the quarter it was signed, half the next quarter, a quarter the one after, then not at all. The provider's current quarter is the CURRENT_QUARTER setting.
  • First citizens. While fewer than BOOTSTRAP_CITIZENS people are verified, one vouch from someone already verified is enough, oldest vouch first. Scores do not change; only verified does, and the person is marked firstCitizen. A ring with no verified voucher still gets nothing.

auth.omxus.com runs with one core address, BOOTSTRAP_CITIZENS = 100, and CURRENT_QUARTER = 2026Q3 (checked 30 Oct 2026).

Trust on the chain

The HER contract keeps its own record:

  • a voucher must already hold a HER token, cannot vouch for themselves, and cannot vouch the same person twice;
  • the third vouch mints the token. The new person's score starts at 30% plus 70% of their three vouchers' average;
  • the token cannot be transferred;
  • when a verifier lowers someone's score, a third of the drop falls on each of the three people who vouched for them.

The contract source in the protocol repo also limits each person to one vouch a week and twenty in a lifetime. The deployed contract does not expose those limits (MAX_VOUCHES() and VOUCH_COOLDOWN() revert on mainnet, 30 Oct 2026), so do not rely on them. Every contract, its address and its calls: Contracts.

The provider cannot vouch on the chain for anyone. vouch(address) checks the sender, so the voucher signs and pays with their own key.

Signing a vouch

A vouch is a signed statement: "I, this address, affirm this address is a unique human named this, near me, now." It is signed with the voucher's secp256k1 key, so POST /vouch/submit needs no session: the signature is the authority, and anyone can carry a vouch to the provider.

The signed bytes are this JSON, keys in this order, addresses lowercase:

{"v":1,"vouchee":"0x…","voucher":"0x…","name":"Ada","rssi":-58,"ts":1793330000,"nonce":"9f2c4a1be07d3355"}

Hash it with keccak-256, sign with low-S, and post the fields with r, s and v (recovery id + 27):

const a = { vouchee_address, voucher_address: me.address, claimed_name: 'Ada',
            ble_rssi: rssi, ts: Math.floor(Date.now() / 1000),
            nonce: bytesToHex(crypto.getRandomValues(new Uint8Array(8))) };
const hash = keccak256(new TextEncoder().encode(signableVouchJSON(a)));
const sig  = await secp256k1.signAsync(hash, me.keyBytes, { lowS: true });
a.r = '0x' + sig.r.toString(16); a.s = '0x' + sig.s.toString(16); a.v = sig.recovery + 27;
await fetch('https://auth.omxus.com/vouch/submit', {
  method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(a) });

Untested as written here. It is the server's own documented client, and the server's test suite signs and submits a real vouch the same way. signableVouchJSON is exported from the auth server's vouch-routes.js; copy it rather than rebuilding the JSON, so the bytes never drift.

If the voucher has also sent vouch(vouchee) to the HER contract, add its tx_hash. The provider checks it on the chain before keeping it: it must be a real transaction, from the voucher, to HER, calling vouch for this vouchee.

Nearness is recorded, not checked

ble_rssi is signed and stored, but the HTTP route does not check it. Over Bluetooth the engine relays the same attestation between phones; over HTTP, "physically present" is the voucher's word.

Vouch codes

People find each other by a short code: the first eight hex characters of their address, shown as ABCD-EFGH. GET /vouch/code gives a signed-in person theirs; GET /vouch/resolve?code=… turns one back into an address and name.

Eight hex characters can collide. When two people share a code, resolve answers 409 ambiguous_code instead of guessing: vouching the wrong person cannot be undone.

Endpoints

method path auth what
GET /vouch/code session cookie {address, code}
GET /vouch/resolve?code=… session cookie {address, name}; any case, dash optional; 20 lookups per IP per 15 minutes
POST /vouch/submit the signature {ok, address, name, trust, verified, tx_hash, onchain}
GET /vouch/graph session cookie {address, score, verified, vouchers, vouched, onchain}
GET /vouch/chain?address=0x… none the contract's answer only; without address, the signed-in person's
GET /network/pulse none counts, the last 8 vouches and the graph, addresses shortened; cached 60 seconds

vouchers and vouched in /vouch/graph share one shape, {address, name, weight, at, tx_hash}. weight is that other person's score: in vouchers it is how much their vouch counted, in vouched how far your vouch carried them. tx_hash is null when the vouch never reached the chain.

onchain is {verified, score, tokenId, pendingVouches, contract}, with the chain's score out of 1000. It is null when the chain cannot be reached; sign-in keeps working, and /vouch/chain answers 503 chain_unreachable.

Errors

status error when
400 missing_field (+ field) /vouch/submit lacks one of vouchee_address, voucher_address, r, s, v, ts, nonce
400 self_vouch voucher and vouchee are the same address
400 bad_signature the signature cannot be read
401 signature_mismatch it was signed by someone other than voucher_address
400 bad_tx (+ reason) tx_hash is not that vouch on the chain
400 bad_code the code is not eight hex characters
404 unknown_code nobody holds that code
409 ambiguous_code more than one person holds it
401 not_signed_in a session route without a session
429 too_many_requests past the resolve limit
400 address_required /vouch/chain with no address and no session

What a site gets

Without vouching, Omxus takes you from unverified accounts to accounts that are much harder to mass-create. With vouching, a verified address means people the network already trusts stood behind it. Better, not solved: a vouch is only as honest as the people giving it. Say so to your users.