OMLA Technical Details
An engineering-level walkthrough of every moving part: what a registration is, how a payment arrives, why a wallet gets paid, and what happens when something goes wrong. Intended for engineers integrating with OMLA, auditors verifying behaviour, and anyone who wants to understand the system end-to-end. Where a design decision is still open, it's flagged and linked to the roadmap.
1. System Overview
OMLA is a licensing + routing layer for AI models. Model creators register a cryptographic identity and a payout address; commercial users who deploy those models submit quarterly reports of attributable revenue and run cost; OMLA computes the royalty at 30% of the greater of the two, resolves the derivation graph, and pays every contributing creator in one cascading pass. The system is deliberately boring: a Postgres database, a static website, and a handful of scheduled server-side jobs.
What it is not
- Not a hosting provider. OMLA does not store model weights. The weight-hash field anchors identity but the actual bytes live on Hugging Face, Civitai, S3, or wherever the creator chose.
- Not a money transmitter. OMLA is a routing + accounting system. Payment rails (Stripe, PayPal, Lightning, ACH) are the moving parts. OMLA never holds funds long-term; quarterly payouts are pass-through.
- Not a DRM or enforcement mechanism. The license obligation is financial and contractual. Compliance is reputation-based (compliance state + blacklist) and legal, not technical.
- Not a telemetry system. Usage reports are creator- and commercial-user-submitted. There is no beacon inside the model files.
2. Architecture
Three services, that's it.
| Component | Where it runs | Role |
|---|---|---|
| Static website (this site) | DreamHost | Landing, registry, registration UI, dashboards, commercial reporting UI. Talks directly to Supabase via the anon key, gated by Row-Level Security. |
| Supabase (PostgreSQL + Auth + PostgREST + Realtime + Storage) | Supabase cloud (Tokyo region) | System of record. 11 tables, plus stored functions for split resolution, cycle prevention, compliance transitions, audit triggers. |
| Edge Functions | Supabase Edge (Deno) | Scheduled jobs: submit-report, compliance-tick (weekly), quarterly-payout (quarterly), publish-blacklist (quarterly). Server-side use of service-role key, payment-rail credentials. |
The browser writes through PostgREST; RLS enforces authorisation by row. The service-role key never leaves Edge Functions or your password manager. Payment rails are called only from Edge Functions, with idempotency keys to prevent double-payment under retries.
3. Data Model
Thirteen tables; every field has a purpose. See omla-deploy/database/diagram.md for the ER diagram.
| Table | Purpose | Key fields |
|---|---|---|
models | Immutable identity for a registered model. | id, name, ed25519_pubkey (unique), model_hash (unique where non-null), creator_email, license_version, is_active. |
model_metadata | Mutable key/value overlay for fields we don't want in the main schema (domain, source URL, paper link, parameter count). | (model_id, key) PK, value JSONB. |
lineage_edges | Directed-acyclic parent→child relationships inside OMLA. | parent_id, child_id, relationship, contribution_weight (0–1). |
external_lineage | Declared ancestors that are not OMLA-registered (e.g. Llama, Stable Diffusion). Informational; no royalty flows. | child_id, external_name, external_url, license, note. |
wallets | omla1… Bech32m addresses and the real payment rail each resolves to. | address (unique), rail, rail_identifier, is_primary, is_active. |
contribution_splits | Percentage breakdown of a model's royalty pool to recipient wallets. Sum ≤ 100% within an effective window. | model_id, recipient_wallet_id, percentage, effective_from, effective_until. |
commercial_users | Companies deploying OMLA models commercially. | company_name, contact_email (unique), api_key_hash. |
usage_reports | Quarterly declarations: per-model revenue + run cost + computed royalty. | model_id, reporter_id, quarter, revenue_usd, run_cost_usd, royalty_amount_usd, status. |
payments | Actual payout records. One row per (wallet × report × quarter). | wallet_id, report_id, quarter, amount_usd, fee_usd, rail, external_txn_id, status, paid_at. |
dust_credits | Sub-minimum balances carried to the next quarter (below payout_min, typically $0.10). | wallet_id, balance_usd, updated_at. |
compliance_state | State machine per model: REGISTERED → REPORTING → COMPLIANT / DELINQUENT / UNDER_REVIEW / BLACKLISTED. | model_id PK, state, reason, last_report_at, blacklisted_at. |
complaints | Public intake: anyone can file. | complaint_type, target, target_model_id, details, complainant_email, status, notified_at. |
audit_log | Append-only record of every INSERT/UPDATE/DELETE on critical tables, emitted by triggers. | entity, entity_id, action, actor, before_state, after_state, created_at. |
4. Cryptographic Identity (Ed25519)
Every model has an Ed25519 keypair owned by the creator:
- Public key (32 bytes) — stored on-chain (in
models.ed25519_pubkey) and displayed on the public registry. - Secret key (64 bytes) — held by the creator. Never transmitted. Shown once during registration.
The keypair anchors three things:
- Registration integrity. At registration, the creator signs the tuple
{name, model_hash, pubkey, wallet_address, license_version}with the secret key; the signature goes intomodels.registration_signature. Anyone can verify later. Server-side verification of the stored signature is planned (see G2); today the signature is stored but not re-checked at write time. - Mutation authorisation Planned. Updates to wallet, splits, or lineage will be signed with the same key. A server-side trigger will verify the signature against the stored pubkey before accepting the change. Today updates are authorised by RLS email match only (G2).
- Consent for derivatives. When a child model declares a parent with
contribution_weight > 0, we allow the child's creator to self-declare (no parent-side signature required). Disputes are resolved via the complaint system, not gatekeeping.
payload = {
"name": "ExampleLM-7B",
"hash": "e3b0c44298fc1c149afb...", // SHA-256 of weights
"pubkey": "BASE64_32B_PUBKEY",
"wallet": "omla1qq0emf5v8xgs5gwtk4gec7fy5k7spvgc8q3m2",
"license":"v8-10-2025"
}
signature = nacl.sign.detached(JSON.stringify(payload), secretKey)
// → 64 bytes, base64 ~88 chars. Stored as bytea in models.registration_signature.
Key loss
If a creator loses their Ed25519 secret key, they can request recovery by emailing verify@omla-ai.org from the registered creator_email address. The board reviews the request (verifying ownership via email + the SHA-256 weight hash, which the creator can recompute). A replacement keypair is issued and the old pubkey is marked inactive. See roadmap for the automated flow.
5. Wallet Addresses (Bech32m)
OMLA wallet addresses use the Bech32m encoding (BIP-350 — same scheme as Bitcoin Taproot). Format:
- HRP (human-readable prefix):
omla - Separator:
1 - Data part: base32-encoded routing ID (typically 16 bytes), 38–58 characters
- Checksum: 6 characters, Bech32m polymod
Example: omla1qq0emf5v8xgs5gwtk4gec7fy5k7spvgc8q3m2. Regex: ^omla1[a-z0-9]{38,58}$.
Why Bech32m?
- Human-readable prefix — "omla1…" is unambiguous; users can't confuse it with a Bitcoin address.
- Checksum — typos fail closed. A single-character mistake flips the checksum.
- Case-insensitive — the data part is lowercase by convention; uppercase also valid. No mixed-case ambiguity.
- No Base58 look-alikes —
0/OandI/lambiguities are eliminated (onlyabcdefghijklmnopqrstuvwxyz023456789allowed in the data part).
Generation
- Generate 16 bytes of random data (UUIDv4 bits).
- Convert bytes to 5-bit words.
- Bech32m-encode with HRP
omlaand max-length 90.
Implementation in the browser lives at assets/js/wallet.js:
OMLA.wallet.randomWithUuid() // → { uuid, address }
OMLA.wallet.fromUuid(uuid) // deterministic from a model UUID
OMLA.wallet.decode(address) // → Uint8Array of routing ID
OMLA.wallet.isValid(address) // boolean
OMLA.wallet.truncate(addr, 10) // "omla1qq0em…8q3m2" for display
6. Model Registration Flow
Registration is entirely client-side up to the point of writing to the database. The browser never sends the secret key anywhere.
- Step 1 — Identity. Name, description, SHA-256 hash of the weight file(s), optional source URL. The hash is computed in-browser via WebCrypto
SubtleCrypto.digest()or externally viasha256sum. - Step 2 — Keypair.
nacl.sign.keyPair()generates a fresh Ed25519 pair. The secret key is displayed once, with a clear "save this now" warning. The system never stores it. - Step 3 — Lineage. The user declares upstream OMLA models (by UUID or wallet address), each with a
contribution_weightin (0, 1]. The sum of upstream weights must be ≤ 1.0. External (non-OMLA) ancestors are captured separately inexternal_lineagefor reference only — no royalty flows there. - Step 4 — Wallet. A new
omla1…address is generated from a random 16-byte routing ID (the same bytes become the model UUID). The user chooses a payment rail and provides the rail's identifier (Stripeacct_…, PayPal email, Lightning address, etc.). - Step 5 — Splits. The user declares who gets what percentage of this model's royalty pool. Splits must sum to exactly 100. A single creator is common; collaborators or funding recipients are added here.
- Step 6 — Sign & submit. The payload is canonicalised, signed with the secret key, and submitted via PostgREST. The backend inserts rows into
models,model_metadata,wallets,contribution_splits,lineage_edges, and (via theseed_compliance_on_model_inserttrigger)compliance_state.
Server-side validation
- Unique pubkey —
UNIQUEconstraint onmodels.ed25519_pubkey. - Unique weight hash (where present) — the same weight file cannot be registered twice by different parties. Disputes go through the complaint system.
- Valid wallet address — CHECK constraint on the Bech32m regex.
- Cycle prevention — the
reject_lineage_cycletrigger callswould_create_cycle(parent, child)and aborts the insert if the edge would form a loop. - Lineage weight sum — a deferred constraint trigger verifies that the sum of
contribution_weightacross all lineage edges withchild_id = this modelis ≤ 1.0. - Split total — similar deferred trigger verifies that
contribution_splits.percentagesums to exactly 100 per model within the effective window. - RLS —
creator_email = auth.jwt()->>'email'must hold for the INSERT. The caller can't register a model under someone else's email.
7. Lineage (DAG) and External Ancestors
Lineage is a directed acyclic graph: parents flow downstream to children. Two sources of ancestry are tracked separately:
OMLA-to-OMLA lineage
When both parent and child are OMLA-registered, the relationship lives in lineage_edges:
parent_id: the ancestor's model_id child_id: this model's model_id relationship: 'fine-tune' | 'merge' | 'distill' | 'quantize' | 'derivative' contribution_weight: 0 < w ≤ 1
The weight represents how much of the child's royalty pool should flow upstream to the parent. A pure fine-tune might declare 0.70 (70% to base model, 30% retained); a distillation might be 0.40; a quantisation typically 0.95.
External ancestry
If a model derives from a non-OMLA base (Llama, Stable Diffusion, etc.), declare it in external_lineage. This is informational — OMLA never attempts to pay royalties to non-participating licenses — but it makes the derivation honest and helps downstream auditors. The table exists in the schema; the registration form does not yet collect these rows, so today they're written manually or via direct INSERT by the board. UI work tracked in the roadmap.
child_id: this model's model_id external_name: 'Meta Llama 3.1 70B' external_url: 'https://huggingface.co/meta-llama/Meta-Llama-3.1-70B' license: 'Llama 3.1 Community License' note: 'Used as base; OMLA royalty obligations do not extend to Meta.'
Cycle prevention
A cycle in the lineage graph would cause the recursive CTE in resolve_splits() to loop. The would_create_cycle() function walks upstream from the proposed parent and fails the insert if the proposed child is reachable. The CTE itself caps recursion at depth 20 as a second line of defence.
Weight-sum discipline
The sum of contribution_weight across all inbound edges to a single child must be ≤ 1.0. The remainder is the model's "original contribution" and is distributed via contribution_splits.
8. Contribution Splits
A single model can have multiple human / organisational contributors. The contribution_splits table breaks the model's retained royalty pool across them.
| Column | Meaning |
|---|---|
model_id | The model whose retained pool is being split. |
recipient_wallet_id | The OMLA wallet (often the creator's, but can be any registered wallet) that receives this share. |
percentage | A positive number > 0 and ≤ 100. The sum across all splits for a model within the effective window must equal 100. |
effective_from | Start of the window. Defaults to insert time. |
effective_until | NULL means "open". Setting a finite timestamp closes the window; a new row opens the next window. |
Why temporal windows?
Splits change over time. A new co-creator joins; a funding agreement ends; a percentage shifts. Rather than mutating rows, we insert a new row that supersedes the previous one. The quarterly payout uses the split that was in effect at the end of the reporting quarter, not the current one. Historical auditability is built in.
Updates require consent (design)
Changing an existing split affects people other than the signer. The mutation rules are:
- Add a new recipient with > 0%. Requires: (1) creator signature, (2) a proportional reduction of existing percentages (or explicit consent from each affected party).
- Reduce a recipient's percentage. Requires: creator signature + affected recipient's Ed25519 signature on the new row.
- Remove a recipient. Same as a reduction to 0%.
Today the site lets the creator re-write splits unilaterally via RLS; the consent flow is on the roadmap.
9. Split Resolution Algorithm
Given a payment to a model's wallet, who actually receives the money? This is the job of resolve_splits(target_model_id), a PostgreSQL stored function.
Pseudocode
resolve_splits(M, weight=1.0, depth=0):
if depth > 20 or weight < 1e-5: return {} # cycle guard, dust guard
direct = { w.address: w.percentage / 100.0
for cs in contribution_splits[M]
for w in wallets[cs.recipient_wallet_id]
if cs.effective_window covers now() }
terminal = { addr: share * weight for addr, share in direct.items() }
for edge in lineage_edges[child_id = M]:
upstream = resolve_splits(edge.parent_id,
weight * edge.contribution_weight,
depth + 1)
for addr, share in upstream.items():
terminal[addr] = (terminal.get(addr) or 0) + share
return terminal
The SQL implementation is a recursive CTE. Key properties:
- Pure. Same inputs, same output. Safe to call from any Edge Function.
- Stable across quarters. Because it filters on
effective_window, historical quarters resolve to historical splits. - Depth-bounded. Hard cap at 20 levels; real lineage is rarely more than 3.
- Dust-pruned. Paths whose accumulated weight falls below 10-5 are dropped.
- Returns percentages summing to 100. (Within rounding; the function rounds each to 2 decimal places for display.)
Model C is derived from A (weight 0.40) and B (weight 0.30). A's splits: Alice 100%. B's splits: Bob 70%, Carol 30%. C's splits: Dave 100%. A $100 payment to C's wallet resolves to:
- Alice: 0.40 × 1.00 × $100 = $40.00
- Bob: 0.30 × 0.70 × $100 = $21.00
- Carol: 0.30 × 0.30 × $100 = $9.00
- Dave: 0.30 × 1.00 × $100 = $30.00 (C's retained 30%)
10. Compliance State Machine
Every model has a compliance state. The state machine is advanced by the tick_compliance() function, invoked weekly by the compliance-tick Edge Function.
REGISTERED
Initial state upon model insert. No commercial reporting has happened yet.
REPORTING
At least one usage_reports row has been submitted for this model.
COMPLIANT
A payments row with status='completed' exists in the last 120 days.
DELINQUENT
The model has a last_report_at more than 120 days old — payments stopped flowing.
UNDER_REVIEW
An open complaint targeting this model's wallet or lineage. Payouts are paused until resolved.
BLACKLISTED
DELINQUENT for > 90 additional days, or willful non-compliance confirmed. Published on /api/blacklist.json.
Transitions (weekly tick)
- REGISTERED → REPORTING when a
usage_reportsrow first arrives. - REPORTING / DELINQUENT → COMPLIANT when a completed payment lands in the last 120 days.
- REPORTING / COMPLIANT → DELINQUENT when
last_report_atis older than 120 days. - DELINQUENT → BLACKLISTED when the row has been DELINQUENT for > 90 days.
- Any state → UNDER_REVIEW on the insert of an open
complaintsrow targeting this model (trigger in migration 008). - UNDER_REVIEW → (previous state) on complaint resolution.
Commercial-user compliance
Commercial users have their own parallel state machine (in commercial_compliance_state, added in migration 008). A user who submits reports and pays → COMPLIANT. A user who submits but fails to pay → DELINQUENT, eventually BLACKLISTED. Blacklisted commercial users appear on the same /api/blacklist.json.
11. Commercial Usage Reporting
Every quarter, commercial users submit one or more usage_reports. A single submission can cover multiple models (pipelines) via multiple rows.
Input
POST /functions/v1/submit-report
Authorization: Bearer <user JWT>
Content-Type: application/json
{
"quarter": "2026-Q2",
"period_start": "2026-04-01",
"period_end": "2026-06-30",
"entries": [
{ "model_id": "uuid", "revenue_usd": 12500.00, "run_cost_usd": 1800.00, "pipeline_weight": 0.70 },
{ "model_id": "uuid", "revenue_usd": 12500.00, "run_cost_usd": 1200.00, "pipeline_weight": 0.30 }
]
}
Server-side processing
- Resolve the reporter (JWT email →
commercial_users.id, orx-api-key→ SHA-256 →api_key_hash). - For each entry, compute royalty =
greater(revenue × 30%, cost × 30%) × pipeline_weight. - Record
royalty_basisas'revenue'or'cost'. - Insert one
usage_reportsrow per entry, withstatus='submitted'. - Return a compact response.
Pipeline weight discipline
pipeline_weight values should sum to ≤ 1.0 across all models in a single pipeline. If your product uses Model A as a retriever and Model B as a generator, and both are needed for the output, you might split 0.30 / 0.70. If Model B alone produces the output, use 0.0 / 1.0. The sum-≤-1 rule prevents a commercial user from over-attributing.
Multi-basis reporting
A single commercial deployment may have:
- Clear attributable revenue — SaaS with per-call billing. Use
revenue_usd. - No attributable revenue but clear costs — internal tool saving labour. Use
run_cost_usd. - Both — a hosted inference service with additional internal use. Report both; OMLA picks the greater.
12. Royalty Calculation
For a single (model, commercial_user, quarter) report:
royalty_basis = (revenue × 0.30 ≥ cost × 0.30) ? 'revenue' : 'cost' royalty_amount = max(revenue × 0.30, cost × 0.30) × pipeline_weight
The 30% rate is a system-wide constant (OMLA_ROYALTY_RATE, configurable per deployment but fixed for the published license v8-10-2025).
Why "the greater of"?
Two failure modes the rule guards against:
- Revenue hiding — a commercial user claims $0 revenue (maybe behind a free tier). Cost is still real. Cost anchors the floor.
- Cost hiding — hardware is fully depreciated, labour is volunteer. Revenue anchors the floor.
The greater-of rule makes both games unprofitable: you have to under-report both fields to avoid the royalty, which is increasingly hard to defend in an audit.
- SaaS: $50,000 revenue, $6,000 run cost → max($15,000, $1,800) = $15,000 royalty (revenue-based).
- Free tier: $0 revenue, $12,000 run cost → max($0, $3,600) = $3,600 royalty (cost-based).
- Internal labour-saving tool: $0 attributable revenue, $2,000 hardware + labour cost → $600 royalty (cost-based).
- Pipeline 2 models: Model A (40% weight), Model B (60% weight). Pipeline revenue $100k, cost $10k → A: max($12k, $1.2k) = $12k royalty; B: max($18k, $1.8k) = $18k royalty.
- No attributable anything: depreciated hardware, volunteer labour, no revenue → basis is $0, royalty is $0. Fully legal per license §3.
13. Quarterly Payout Pipeline
The quarterly-payout Edge Function runs once per quarter, typically on the first business day of the quarter at 10:00 UTC. Default is DRY_RUN=true — records only, no money moves — until the operator flips the flag after a successful dry run.
- Fetch reports.
SELECT * FROM usage_reports WHERE quarter = '2026-Q2' AND status = 'submitted'. - Resolve splits. For each report, call
resolve_splits(model_id)— returns terminal wallets with percentages. - Aggregate per wallet. Across all reports in the quarter, build a map:
wallet_address → total_amount_usd. - Apply dust rule. Any wallet with
total < $0.10should be written todust_creditsinstead ofpayments, accumulating forever until the wallet crosses threshold. Today the pipeline flags sub-threshold wallets asbelow_minand returns them in the response, but does not yet persist todust_credits. The table exists; wiring the write is a one-line addition. - Insert payments. One
paymentsrow per remaining wallet withstatus='processing'. Unique constraint(wallet_id, report_id, quarter)makes this idempotent. - Call rails Stub. Design is: for each row, call the rail's
execute()with the OMLA idempotency keysha256(quarter | wallet | sorted_report_ids); rails return{success, external_txn_id, fee_usd, settled_at}. Today the call site is commented with "this is where you call Stripe" — thepaymentsrow is inserted withstatus='processing'but no real rail is invoked. Wiring one rail (Stripe Connect first) is a P0 item; see roadmap. - Update payment row.
status='processing'until the rail's webhook confirms settlement; thenstatus='completed'withpaid_at. - Mark reports paid.
UPDATE usage_reports SET status='paid' WHERE id IN (...).
Idempotency
The pipeline is safe to re-run. The unique index on payments(wallet_id, report_id, quarter) rejects duplicate inserts; rails use the same idempotency key and return the original result if called twice.
Failure modes
| Failure | Behaviour |
|---|---|
| Edge Function times out (60s limit) | Re-run; idempotency keys pick up where we left off. For large quarters, process in 50-report chunks. |
| Rail rejects (insufficient funds, wallet frozen) | status='failed', logged. Operator decides: retry, route through alternate rail, or contact recipient. |
| Webhook never fires | status='processing' forever → monitoring alert after 48 hours. Operator queries the rail directly. |
| Rail clawback (Stripe dispute, chargeback) | Webhook updates status='reversed'; operator decides whether to rebill. |
14. Payment Rails
Rails are interchangeable plugins behind a common interface:
interface PaymentRail {
name: 'stripe' | 'paypal' | 'lightning' | 'ach' | 'sepa' | 'wise' | 'other';
canPay(railIdentifier: string): boolean;
estimateFee(amountUsd: number): number;
execute(railIdentifier, amountUsd, idempotencyKey): Promise<PaymentResult>;
}
| Rail | Typical fee | Speed | Coverage | Key constraint |
|---|---|---|---|---|
| Stripe Connect | 0.25% + $0.25 | 1–2 days (Standard), instant (paid) | 40+ countries | Recipient must complete Stripe KYC. |
| PayPal Payouts | ~2% (cap $20) | Minutes | Near-global | Recipient must confirm email. |
| Lightning | 0.01–0.1% | Seconds | Global | USD ↔ sats FX via an oracle; recipient needs Lightning wallet. |
| ACH | $0.25 flat (Stripe), ~$1 (bank) | 2–5 days | US only | Routing + account numbers. |
| SEPA | €0.20 | 1–2 days | EU + EEA | IBAN + BIC. |
| Wise | 0.4–1% | Minutes–days | Global | Recipient must have a Wise account or supported local bank. |
Fee cap
The OMLA license caps pass-through fees at 10% of the payout (§4 Part B(3)). Rails that would exceed this for a small amount are skipped in favour of accumulating dust until the next quarter.
Rail selection
Each wallet is bound to a single rail at registration time. Changing a rail requires an update signed by the creator's Ed25519 key. Creators can add additional wallets to the same model for different rails; splits can point to different wallets within the same split table.
15. Dust Credits & Accumulation
Tiny amounts ( < $0.10 per wallet per quarter) are expensive to pay and useless to receive. Instead of dropping them, OMLA accumulates:
CREATE TABLE dust_credits ( wallet_id UUID PRIMARY KEY REFERENCES wallets(id), balance_usd NUMERIC(12,4) NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT now() );
The quarterly pipeline, after aggregation, checks each wallet's total_this_quarter + existing dust_balance:
- If
total ≥ $0.10→ pay out and zero the dust balance. - If
total < $0.10→ add to dust balance; no payment.
Dust never expires. A wallet that earns $0.05 per quarter for two years accumulates $0.40 and pays out. Dashboards display the current dust balance so creators see it.
16. Complaint & Appeal Flow
- Filing. Anyone fills out
/complaint.html. A row lands incomplaintswithstatus='open'. - Auto-transition. A trigger sets the target model's
compliance_statetoUNDER_REVIEWif the complaint type isinvalid_wallet,ownership_dispute, orfraud_impersonation. - Notification. Within 24 hours (SLA, manual for now — the board watches the
complaintstable and emails the wallet holder atmodels.creator_email; an automatednotify-complaintsEdge Function is tracked as roadmap item).complaints.notified_atis stamped once email goes out. - Response window. 24 hours for the holder to respond with documentation. No response →
wallets.is_active = false; royalty flows to this wallet become 0%. This transition is manual today; automation also on the roadmap. - Appeal. The holder may appeal within 7 days. The complaint status flips to
under_appeal. A board member reviews (/board). - Resolution. Three outcomes:
resolved(wallet reactivated, back to prior state),dismissed(complaint invalid, status rolled back), orblacklisted(model moved to BLACKLISTED at next quarter tick).
While a complaint is open, flow-through royalties to other wallets continue. The 0% applies only to the disputed wallet.
17. Blacklist Publication
Each quarter, after the payout cycle settles, the publish-blacklist Edge Function runs:
- Query all
compliance_state WHERE state = 'BLACKLISTED'(plus any incommercial_compliance_state). - Build a JSON body: version, quarter, entries (each with model_id, name, reason, blacklisted_at).
- Sign with the OMLA association's Ed25519 key (held in Supabase secret
OMLA_SIGNING_KEY). - Upload to Supabase Storage as
public/blacklist.json. - DreamHost rewrites
/api/blacklist.jsonto that storage URL.
Consumers (SDKs, payment processors, inference gateways) are expected to fetch /api/blacklist.json at least daily, verify the signature with the OMLA public key (shipped in config.js), and refuse to route payments to blacklisted wallets.
Signature verification in the client
fetch('/api/blacklist.json')
.then(r => r.text())
.then(raw => {
const body = JSON.parse(raw);
const { signature, ...payload } = body;
const ok = nacl.sign.detached.verify(
new TextEncoder().encode(JSON.stringify(payload)),
base64Decode(signature),
base64Decode(OMLA_CONFIG.omlaSigningPublicKey)
);
if (!ok) throw new Error('Invalid blacklist signature');
return payload.entries;
});
18. Security Model
Authorisation
Three tiers:
- Anonymous — public read-only access to the registry. Gated by RLS
SELECTpolicies. - Authenticated user — Supabase JWT. RLS policies check
auth.jwt()->>'email'againstcreator_emailorcontact_email. - Service-role — used exclusively by Edge Functions. Bypasses RLS. Never leaves the server.
Key inventory
| Key | Sensitivity | Location |
|---|---|---|
| Supabase anon key | Public | assets/js/config.js |
| Supabase service-role | Secret | Edge Function secret only |
| DB password | Secret | Operator's password manager |
| Stripe / PayPal / Lightning secrets | Secret | Edge Function secrets |
| OMLA signing secret key | Secret | Edge Function secret OMLA_SIGNING_KEY |
| OMLA signing public key | Public | config.js as omlaSigningPublicKey |
| Creator Ed25519 secret | User-held | Never stored |
| Commercial API key | User-held | Only SHA-256 stored |
Content Security Policy
The site's .htaccess pins scripts to self, cdn.jsdelivr.net, and *.supabase.co. Frames are forbidden (X-Frame-Options: DENY). Referrer is trimmed. HSTS is on.
Audit trail
Every mutation on models, wallets, contribution_splits, payments, compliance_state, usage_reports, complaints is logged in audit_log with before_state and after_state JSONB. No UPDATE or DELETE policies on audit_log — the log is literally append-only from the API side.
19. API Surface
Public (anon)
| Method | Endpoint | Purpose |
|---|---|---|
| GET | /rest/v1/models?is_active=eq.true | List active models |
| GET | /rest/v1/models?id=eq.{uuid}&select=*,wallets(*),lineage_edges(*),contribution_splits(*),compliance_state(*) | Model detail |
| GET | /rest/v1/models?model_hash=eq.{sha256} | Lookup by weight hash |
| POST | /rest/v1/rpc/resolve_splits body: {"target_model_id":"uuid"} | Resolved split table |
| POST | /rest/v1/rpc/verify_by_hash body: {"hash":"e3b0..."} | Is this weight hash registered? |
| GET | /api/blacklist.json | Signed quarterly blacklist |
Authenticated (creator JWT)
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /rest/v1/models | Register model |
| PATCH | /rest/v1/wallets?id=eq.{uuid} | Update wallet rail |
| GET | /rest/v1/payments?wallet_id=in.(...) | View earnings |
Authenticated (commercial user JWT or API key)
| Method | Endpoint | Purpose |
|---|---|---|
| POST | /functions/v1/submit-report | Submit a usage report |
| GET | /rest/v1/usage_reports?reporter_id=eq.{uuid} | Past reports |
Service-role (Edge Functions only)
Unrestricted. Use for compliance ticks, payout pipeline, blacklist publication. Service role bypasses RLS.
20. Weight Verification Lookup
The verify_by_hash(hash text) RPC lets any consumer (payment processor, inference gateway, model-hub) check whether a specific weight file is OMLA-registered:
POST /rest/v1/rpc/verify_by_hash
{ "hash": "e3b0c44298fc1c149afb..." }
→ {
"model_id": "uuid",
"name": "ExampleLM-7B",
"license_version": "v8-10-2025",
"wallet": "omla1qq0em...",
"compliance": "COMPLIANT",
"blacklisted": false
}
If the hash is unknown, the RPC returns null. Integrators use this to short-circuit the "is this OMLA?" check before any payment or badge rendering.
21. Audit Log & Reconciliation
Every mutation lands in audit_log via PL/pgSQL triggers. Schema:
entity TEXT -- table name ('models','wallets',...)
entity_id UUID -- primary key of the affected row
action TEXT -- 'INSERT' | 'UPDATE' | 'DELETE'
actor TEXT -- auth.jwt()->>'email' or 'postgres' (service role)
before_state JSONB -- row snapshot before the change (NULL on INSERT)
after_state JSONB -- row snapshot after the change (NULL on DELETE)
created_at TIMESTAMPTZ
Reconciliation queries
Some questions you can answer entirely from audit_log:
- "Who changed wallet X's rail and when?"
- "What was Model Y's compliance state on 2026-03-15?"
- "Did this registration exist at quarter close?"
- "Reconstruct the splits table as of 2026-Q1 end."
Retention
Indefinite by default. The audit log is the source of truth for accounting. If GDPR requires deletion of a specific user's PII, a narrow UPDATE (via service role) can redact before_state/after_state.contact_email fields while preserving row structure.
22. Extensibility: Datasets, Likenesses, Other Creative Works
The OMLA license text (§6) and board roadmap contemplate extending the framework beyond AI models to datasets, synthetic-voice identities, art styles, and other creative works. The database is already shaped for this:
modelscould be renamed / supplemented by a genericassetstable with akindcolumn:'model' | 'dataset' | 'likeness' | 'artwork'.- The wallet, lineage, splits, compliance, and payout infrastructure is kind-agnostic. No change required.
- Royalty basis may differ per kind. Datasets: revenue from anything trained on it. Likenesses: revenue from any output that depicts the subject. The
royalty_ratecould become a per-kind column. - Lineage for datasets: a model trained on Dataset A becomes a child; Dataset A's contributors receive royalties.
The existing system would absorb datasets with a single migration adding an asset_kind enum and a default of 'model'. This is explicitly planned and tracked on the roadmap.
23. Known Gaps & Open Questions
The items below are things the system does not yet do automatically, or design choices still under discussion. Each has a proposed implementation; all are tracked on the Roadmap.
contribution_splits row should require signatures from every affected recipient. Today the creator can unilaterally rewrite splits (subject to the 100% sum rule). Design: a split_change_requests table holds pending changes; once every affected recipient signs, the change is applied.pgcrypto or pgsodium to verify each update against models.ed25519_pubkey.usage_disputes table; when opened, the disputed report's status flips to disputed; board arbitration; outcome either confirms or revises the royalty.payments.settled_currency + fx_rate_used pair; an FX snapshot table fx_rates(asof TIMESTAMPTZ, from TEXT, to TEXT, rate NUMERIC).tax_forms table linked to wallets; rails won't execute until a valid form is on file for high-volume recipients.evidence_uploads table (signed PDFs, Stripe export CSVs), board review if contested./register.html; per-IP rate limit via Supabase Edge + Upstash Redis.omla-sdk-js and omla-sdk-python with a 20-line "register" and "report" path; publish to npm / PyPI. Would dramatically lower the bar for commercial users.omla.json that model-hubs can publish next to the weights, with the OMLA-ID and license invocation block./admin.html that uses the service-role key (stored only in the browser's IndexedDB after a manual entry), with flows for complaint resolution, manual state transitions, and blacklist inspection./stats.html page showing total models, lifetime payouts, active commercial users, quarterly paid / blacklisted counts.i18n/{lang}.json with string keys; a tiny runtime replaces data-i18n-attributed text at load time. License translations are legally non-binding advisory until each is approved by the board..range(offset, offset+49) + infinite scroll.models-only table names. Design: add asset_kind column defaulting to 'model'; support datasets / likenesses / artworks in the same tables.Each gap is tracked with a priority, a design sketch, and an estimated complexity on the Roadmap page.