Deep dive

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

2. Architecture

Three services, that's it.

ComponentWhere it runsRole
Static website (this site)DreamHostLanding, 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 FunctionsSupabase 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.

TablePurposeKey fields
modelsImmutable identity for a registered model.id, name, ed25519_pubkey (unique), model_hash (unique where non-null), creator_email, license_version, is_active.
model_metadataMutable 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_edgesDirected-acyclic parent→child relationships inside OMLA.parent_id, child_id, relationship, contribution_weight (0–1).
external_lineageDeclared ancestors that are not OMLA-registered (e.g. Llama, Stable Diffusion). Informational; no royalty flows.child_id, external_name, external_url, license, note.
walletsomla1… Bech32m addresses and the real payment rail each resolves to.address (unique), rail, rail_identifier, is_primary, is_active.
contribution_splitsPercentage 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_usersCompanies deploying OMLA models commercially.company_name, contact_email (unique), api_key_hash.
usage_reportsQuarterly declarations: per-model revenue + run cost + computed royalty.model_id, reporter_id, quarter, revenue_usd, run_cost_usd, royalty_amount_usd, status.
paymentsActual 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_creditsSub-minimum balances carried to the next quarter (below payout_min, typically $0.10).wallet_id, balance_usd, updated_at.
compliance_stateState machine per model: REGISTERED → REPORTING → COMPLIANT / DELINQUENT / UNDER_REVIEW / BLACKLISTED.model_id PK, state, reason, last_report_at, blacklisted_at.
complaintsPublic intake: anyone can file.complaint_type, target, target_model_id, details, complainant_email, status, notified_at.
audit_logAppend-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:

The keypair anchors three things:

  1. Registration integrity. At registration, the creator signs the tuple {name, model_hash, pubkey, wallet_address, license_version} with the secret key; the signature goes into models.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.
  2. 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).
  3. 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.
Example — signing a registration payload:
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:

Example: omla1qq0emf5v8xgs5gwtk4gec7fy5k7spvgc8q3m2. Regex: ^omla1[a-z0-9]{38,58}$.

Why Bech32m?

Generation

  1. Generate 16 bytes of random data (UUIDv4 bits).
  2. Convert bytes to 5-bit words.
  3. Bech32m-encode with HRP omla and 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.

  1. 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 via sha256sum.
  2. 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.
  3. Step 3 — Lineage. The user declares upstream OMLA models (by UUID or wallet address), each with a contribution_weight in (0, 1]. The sum of upstream weights must be ≤ 1.0. External (non-OMLA) ancestors are captured separately in external_lineage for reference only — no royalty flows there.
  4. 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 (Stripe acct_…, PayPal email, Lightning address, etc.).
  5. 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.
  6. 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 the seed_compliance_on_model_insert trigger) compliance_state.

Server-side validation

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.

Example: Model C is a fine-tune of Model A (weight 0.40) merged with Model B (weight 0.30). The remaining 30% is C's original work. A commercial payment of $100 into C's wallet flows $40 upstream to A, $30 upstream to B, and $30 into C's local split pool (which may then be further divided between C's own contributors).

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.

ColumnMeaning
model_idThe model whose retained pool is being split.
recipient_wallet_idThe OMLA wallet (often the creator's, but can be any registered wallet) that receives this share.
percentageA positive number > 0 and ≤ 100. The sum across all splits for a model within the effective window must equal 100.
effective_fromStart of the window. Defaults to insert time.
effective_untilNULL 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:

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:

Worked example:
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%)
Sum: $100.00 ✓

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)

  1. REGISTERED → REPORTING when a usage_reports row first arrives.
  2. REPORTING / DELINQUENT → COMPLIANT when a completed payment lands in the last 120 days.
  3. REPORTING / COMPLIANT → DELINQUENT when last_report_at is older than 120 days.
  4. DELINQUENT → BLACKLISTED when the row has been DELINQUENT for > 90 days.
  5. Any state → UNDER_REVIEW on the insert of an open complaints row targeting this model (trigger in migration 008).
  6. 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

  1. Resolve the reporter (JWT email → commercial_users.id, or x-api-key → SHA-256 → api_key_hash).
  2. For each entry, compute royalty = greater(revenue × 30%, cost × 30%) × pipeline_weight.
  3. Record royalty_basis as 'revenue' or 'cost'.
  4. Insert one usage_reports row per entry, with status='submitted'.
  5. 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:

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:

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.

Worked examples:
  • 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.

  1. Fetch reports. SELECT * FROM usage_reports WHERE quarter = '2026-Q2' AND status = 'submitted'.
  2. Resolve splits. For each report, call resolve_splits(model_id) — returns terminal wallets with percentages.
  3. Aggregate per wallet. Across all reports in the quarter, build a map: wallet_address → total_amount_usd.
  4. Apply dust rule. Any wallet with total < $0.10 should be written to dust_credits instead of payments, accumulating forever until the wallet crosses threshold. Today the pipeline flags sub-threshold wallets as below_min and returns them in the response, but does not yet persist to dust_credits. The table exists; wiring the write is a one-line addition.
  5. Insert payments. One payments row per remaining wallet with status='processing'. Unique constraint (wallet_id, report_id, quarter) makes this idempotent.
  6. Call rails Stub. Design is: for each row, call the rail's execute() with the OMLA idempotency key sha256(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" — the payments row is inserted with status='processing' but no real rail is invoked. Wiring one rail (Stripe Connect first) is a P0 item; see roadmap.
  7. Update payment row. status='processing' until the rail's webhook confirms settlement; then status='completed' with paid_at.
  8. 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

FailureBehaviour
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 firesstatus='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>;
}
RailTypical feeSpeedCoverageKey constraint
Stripe Connect0.25% + $0.251–2 days (Standard), instant (paid)40+ countriesRecipient must complete Stripe KYC.
PayPal Payouts~2% (cap $20)MinutesNear-globalRecipient must confirm email.
Lightning0.01–0.1%SecondsGlobalUSD ↔ sats FX via an oracle; recipient needs Lightning wallet.
ACH$0.25 flat (Stripe), ~$1 (bank)2–5 daysUS onlyRouting + account numbers.
SEPA€0.201–2 daysEU + EEAIBAN + BIC.
Wise0.4–1%Minutes–daysGlobalRecipient 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:

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

  1. Filing. Anyone fills out /complaint.html. A row lands in complaints with status='open'.
  2. Auto-transition. A trigger sets the target model's compliance_state to UNDER_REVIEW if the complaint type is invalid_wallet, ownership_dispute, or fraud_impersonation.
  3. Notification. Within 24 hours (SLA, manual for now — the board watches the complaints table and emails the wallet holder at models.creator_email; an automated notify-complaints Edge Function is tracked as roadmap item). complaints.notified_at is stamped once email goes out.
  4. 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.
  5. Appeal. The holder may appeal within 7 days. The complaint status flips to under_appeal. A board member reviews (/board).
  6. Resolution. Three outcomes: resolved (wallet reactivated, back to prior state), dismissed (complaint invalid, status rolled back), or blacklisted (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:

  1. Query all compliance_state WHERE state = 'BLACKLISTED' (plus any in commercial_compliance_state).
  2. Build a JSON body: version, quarter, entries (each with model_id, name, reason, blacklisted_at).
  3. Sign with the OMLA association's Ed25519 key (held in Supabase secret OMLA_SIGNING_KEY).
  4. Upload to Supabase Storage as public/blacklist.json.
  5. DreamHost rewrites /api/blacklist.json to 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:

  1. Anonymous — public read-only access to the registry. Gated by RLS SELECT policies.
  2. Authenticated user — Supabase JWT. RLS policies check auth.jwt()->>'email' against creator_email or contact_email.
  3. Service-role — used exclusively by Edge Functions. Bypasses RLS. Never leaves the server.

Key inventory

KeySensitivityLocation
Supabase anon keyPublicassets/js/config.js
Supabase service-roleSecretEdge Function secret only
DB passwordSecretOperator's password manager
Stripe / PayPal / Lightning secretsSecretEdge Function secrets
OMLA signing secret keySecretEdge Function secret OMLA_SIGNING_KEY
OMLA signing public keyPublicconfig.js as omlaSigningPublicKey
Creator Ed25519 secretUser-heldNever stored
Commercial API keyUser-heldOnly 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)

MethodEndpointPurpose
GET/rest/v1/models?is_active=eq.trueList 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.jsonSigned quarterly blacklist

Authenticated (creator JWT)

MethodEndpointPurpose
POST/rest/v1/modelsRegister 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)

MethodEndpointPurpose
POST/functions/v1/submit-reportSubmit 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:

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:

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.

G1. Split update consent. Mutating an existing 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.
G2. Mutation signatures. Today wallet / split / lineage updates are authorised by RLS email match. They should also require an Ed25519 signature from the model's creator keypair, server-verified. Design: a server-side trigger calls pgcrypto or pgsodium to verify each update against models.ed25519_pubkey.
G3. Key rotation. If a creator loses their secret key, the only path is email-based manual recovery. Design: a "recovery key" registered at creation time; either the primary or the recovery key can authorise updates; a third-party timelock for replacing both.
G4. Usage-report disputes. A creator has no in-system mechanism to challenge a commercial user's report ("your $100k number looks low"). Design: a usage_disputes table; when opened, the disputed report's status flips to disputed; board arbitration; outcome either confirms or revises the royalty.
G5. Multi-currency payouts. All ledger fields are USD. Stripe Connect into an EU account disburses EUR. Design: a payments.settled_currency + fx_rate_used pair; an FX snapshot table fx_rates(asof TIMESTAMPTZ, from TEXT, to TEXT, rate NUMERIC).
G6. Withholding & tax forms. US creators receiving > $600/year need 1099-MISC; international need W-8BEN. Design: a tax_forms table linked to wallets; rails won't execute until a valid form is on file for high-volume recipients.
G7. Audit rights for creators. Commercial-user reports are self-declared. Creators may want to audit books. Design: a request-and-respond flow with an evidence_uploads table (signed PDFs, Stripe export CSVs), board review if contested.
G8. Sybil & bot registration. No CAPTCHA. Design: Cloudflare Turnstile on /register.html; per-IP rate limit via Supabase Edge + Upstash Redis.
G9. Integration SDKs. 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.
G10. Hugging Face / Civitai model-card integration. A sidecar omla.json that model-hubs can publish next to the weights, with the OMLA-ID and license invocation block.
G11. Admin console. Today board review is done via SQL Editor. Design: a password-gated /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.
G12. Public transparency dashboard. A /stats.html page showing total models, lifetime payouts, active commercial users, quarterly paid / blacklisted counts.
G13. i18n. English only. Design: a per-page JSON file 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.
G14. Pagination on registry. Current query fetches all models; fine for < 1000, bad at scale. Design: .range(offset, offset+49) + infinite scroll.
G15. Asset kinds beyond models. Framework is ready but schema commits to 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.