How a licence works
What happens between an empty form and a certificate a stranger can check, and how to verify each part of it yourself.
The walkthrough
- Create a licence, or buy one from a marketplace listing. It starts as a draft.
- The creator signs. The licence moves to pending signatures and the signing wallet is bound to that party.
- The agency signs. The licence becomes active.
- The agency settles the fee — a real USDC transfer on devnet, or a recorded demo payment.
- The licence identifier and its hashes are anchored on-chain.
- The public certificate reports all of it at /verify/<licence id>.
- Download the document and re-upload it to confirm that the copy you hold is the one that was signed.
The lifecycle
Five states, no more. A licence takes effect when both counterparties have signed it — execution is a matter of signatures, not of settlement.
DRAFT ─▸ PENDING_SIGNATURES ─▸ ACTIVE ─┬─▸ EXPIRED
└─▸ REVOKEDPayment and the blockchain proof are recorded against an active licence rather than standing in front of it. An unpaid licence is a debt and an unanchored one is merely unwitnessed; neither is a licence that was never granted. Both facts are surfaced on their own terms, so an unpaid grant reads as unpaid without the status having to carry that news — and the legal state never depends on an RPC node being reachable.
The canonical document
This is what gets hashed. Keys are sorted in code-unit order at every depth, so the serialisation does not depend on the order fields were written in anywhere in the codebase.
{
"schema": "human-id/ai-likeness-license",
"version": "1.1",
"license_id": "HID-2026-8F3A92",
"parties": { "creator": …, "agency": …, "client": … },
"likeness": { "face": true, "voice": true },
"permissions": { "ai_generated_content": true,
"ai_model_training": false,
"commercial_advertising": true,
"sublicensing": false },
"scope": { "campaign": …, "platforms": [ … sorted … ],
"territories": …,
"start_date": "2026-08-21",
"expiration_date": "2026-11-19" },
"restrictions": …,
"payment": { "amount": 300, "currency": "USDC" }
}
sha256(canonical bytes) → documentHashNote what is absent: no wallet addresses, no timestamps of when rows were written, nothing that could differ between two honest renderings of the same agreement. The document is the terms and only the terms.
The two hashes
Every licence carries two fingerprints, and they answer different questions.
- The document hash covers the whole agreement, parties included. Changing any field — including a single permission flag — produces a different hash, which is what makes document verification meaningful.
- The scope hash covers the rights alone: no names, no campaign. It lets a licensee prove which rights were granted without revealing who granted them, or to whom.
Both are anchored. A verifier who has only the scope hash can still confirm that a specific set of permissions was committed to at a specific time.
What a party actually signs
Rebuilt from the licence row every time rather than stored as a challenge — the same inputs always produce the same bytes, so there is nothing to hold onto and nothing to expire. The nonce is derived from the licence, the two hashes and the signer's role, which is what stops one party's signature being replayed as the other's.
Human ID — License Authorization Version: 1 License ID: HID-2026-8F3A92 Document Hash: sha256:<64 hex> Scope Hash: sha256:<64 hex> Signer Role: CREATOR Issued At: 2026-08-21T05:15:56Z Nonce: <derived from the four values above> By signing, I authorize this license record with the wallet below and confirm the document and scope identified by the hashes above. This message is signed off-chain. It transfers no funds and grants no spending permission over this wallet.
The last two lines are not decoration. A wallet dialog is a poor place to learn what you are authorising, so the message says what it is not.
Payment
Settlement is USDC on Solana Devnet — Circle's mint, not a token this project issues. There is no escrow: the agency pays the creator directly, and the platform's only job is to say whether that happened.
The payout wallet is resolved in one order, and never guessed: the address named on the licence, then the wallet the creator actually signed with, then the configured fallback. With none of the three, real payment is refused rather than approximated — demo payment still works.
- The server issues an intent: recipient, mint, amount in base units, and a memo.
- The browser builds and sends the transfer, and reports back one transaction signature.
- The server fetches that transaction from an RPC node, retrying while it propagates.
- It re-derives the amount from the chain's own token balance snapshots and checks mint, recipient, amount and memo against the licence.
- Only a transfer that passes every check is written as verified.
A demo payment is a record the operator asked for. Nothing moved. It is labelled Recorded rather than Verified everywhere it appears, and it is never shown as evidence that a fee was paid.
What the platform charges
A share of each settled license, paid by the licensee on top of the licensed amount as a second transfer inside the same transaction. Both transfers are checked against that one transaction, so the license and the fee settle together or not at all.
The fee is never taken out of the licensed amount. The document is signed and hashed and it states a number; a cut out of that number would leave the creator receiving less than their own signed document says they are owed.
- A percentage of the licensed amount, so a small usage right and a large campaign are not charged the same.
- Computed in base units and truncated, so any rounding is a millionth of a dollar in the payer’s favour.
- Re-derived from the license at verification time, like every other amount here — nothing a client sends decides it.
Where no fee wallet is configured, nothing is charged and payment behaves exactly as it did before the fee existed.
What reaches the chain
One memo instruction, carrying the licence identifier and the two hashes. Biometric data and the private contents of a contract are never written on-chain, and there is no version of this product in which they are.
HumanID|HID-2026-8F3A92|sha256:<64 hex> # and the settlement memo a payer's wallet is asked to attach — # identifiers only, no amount and no party: HUMANID|v1|settlement|HID-2026-8F3A92|PAY-8F3A92
That memo is the entire on-chain footprint. Everything else a certificate shows lives in the database and is verifiable against these hashes rather than published beside them.
Checking a document yourself
You do not have to trust this application's answer. The certificate reports a document hash; the download gives you the exact bytes that were hashed. Comparing them is a one-line operation with tools you already have.
- Download the document from the licence page.
- Hash it with any SHA-256 implementation.
- Compare the result with the document hash on the certificate and with the hash inside the on-chain memo. All three should be identical.
shasum -a 256 HID-2026-8F3A92.json # or sha256sum HID-2026-8F3A92.json
If they differ, the copy you are holding is not the copy that was signed — which is exactly what you wanted to find out.
Verification is public
Anyone with a licence ID can read the scope, the signatures, the payment status and the anchored record, and can re-check a downloaded document against its hash. No sign-in, no account, and no request to either party.
That is deliberate, and it has a cost worth understanding: a licence ID is a capability. Treat it the way you would treat a contract reference number rather than a secret, and remember that the certificate names the parties.
Accounts are wallets
There is no password to store, reset or leak, and no e-mail to verify. The two counterparties already prove who they are by signing with a wallet, so signing in is that same act one step earlier.
The message is built by the server and never taken from the request, so a client cannot choose what it is agreeing to. It binds the wallet, so a signature cannot be replayed by another address; the domain, so a signature harvested elsewhere is useless here; and a single-use nonce that is burned before the session is issued. Sessions are database rows, so signing out kills the token even if the cookie survives.
When a request is refused
Failures answer with a status and, where a field is at fault, a field-level list. The common ones:
- 400 — the body could not mean anything: a missing party, a term that ends before it starts, a malformed wallet, an unparseable date.
- 401 — the action needs a session and there is not one.
- 403 — there is a session, but the wallet it belongs to did not sign this licence and therefore cannot revoke it.
- 404 — no licence with that identifier.
- 409 — the action conflicts with what is already true: this party has already signed, or the licence is revoked and its lifecycle is closed.
API reference
Reads need no authentication — verification is public by design. Writes are authorised by the ed25519 signature itself, by the chain, or by the session whose wallet signed the licence.
- POST
/api/auth/challenge - GETPOSTDELETE
/api/auth/session
- GETPOST
/api/licenses - GET
/api/licenses/[id]/document - GETPOST
/api/licenses/[id]/sign - GETPOST
/api/licenses/[id]/pay - POST
/api/licenses/[id]/pay/verify - POST
/api/licenses/[id]/proof - POST
/api/licenses/[id]/revoke - POST
/api/verify/[licenseId]/document
- GETPOST
/api/listings - POST
/api/listings/[id]/license