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_QUARTERsetting. - First citizens. While fewer than
BOOTSTRAP_CITIZENSpeople are verified, one vouch from someone already verified is enough, oldest vouch first. Scores do not change; onlyverifieddoes, and the person is markedfirstCitizen. 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.