Flip 360 Commission Platform
Solution Architecture · L3 Deep-Dive · For Mathew Punter

How a stranger implements this.

Where the EA Blueprint answers "what do we build", this document answers "how does a stranger implement it". Every public endpoint, every database table, the canonical signing format, the idempotency contract, the deployment topology, the observability plan, and every error code the platform can return. This is the deliverable a CTO reads before they let a team commit code.

Contents
  1. API contracts
  2. Data model (D1 DDL)
  3. Signing format
  4. Idempotency contract
  5. Sequence diagram — happy path
  6. Deployment topology
  7. Observability plan
  8. Error catalogue

1  ·  API contracts

Every external surface. Method, path, auth posture, idempotency guarantee, and sample request/response bodies. Hardware-bound signatures are required on every member-authored handshake.

1 · Handshake chain (the trust core)

POST /api/handshakes/intent member idempotent
Referring member declares intent to refer a client. Becomes handshake H1.
Request body
{
  "client_phone": "+61 4xx xxx xxx",
  "client_name": "Sarah Chen",
  "vertical_code": "MORTGAGE",
  "product_code": "HOME_LOAN_OO",
  "receiving_member_id": 1042,
  "estimated_deal_cents": 80000000,
  "device_signature": "<base64 P-256 ECDSA over canonical payload>"
}
Response body
{
  "handshake_id": "H-2026-05-00417",
  "block_height_pending": 8821,
  "hash_prev": "a7c2…",
  "hash_self": "f19e…",
  "created_at": "2026-05-21T04:31:18Z"
}
Errors
  • 400 INVALID_SIGNATURE — Signature does not verify against the member’s registered hardware key
  • 409 DUPLICATE_INTENT — Same idempotency-key replay — original handshake returned
  • 422 RATE_CARD_MISSING — No active Rate Card for this vertical+product combination
POST /api/handshakes/:id/acknowledge public idempotent
Client acknowledges the referral via SMS magic-link. Becomes H2.
Request body
{
  "magic_token": "mlt_a1b2c3d4…",
  "client_consent_text": "I consent to being referred and to my contact details being shared with the receiving party.",
  "ip": "203.0.113.42",
  "user_agent": "<UA string>"
}
Response body
{
  "handshake_id": "H-2026-05-00417",
  "acknowledged_at": "2026-05-21T05:02:44Z",
  "chain_state": "H1+H2_COMPLETE"
}
Errors
  • 410 TOKEN_EXPIRED — Magic-link is older than 7 days
  • 404 HANDSHAKE_NOT_FOUND — Bad token or already-consumed
POST /api/handshakes/:id/intake member idempotent
Receiving member intakes the client (first meeting recorded). Becomes H3.
Request body
{
  "intake_meeting_at": "2026-05-22T09:30:00+10:00",
  "intake_notes": "Met with Sarah, discussed her PPOR refinance options",
  "device_signature": "<base64 ECDSA>"
}
Response body
{ "handshake_id": "H-2026-05-00417", "chain_state": "H1+H2+H3_COMPLETE" }
POST /api/handshakes/:id/settlement member idempotent
Receiving member confirms deal settled (the gross value). Becomes H4.
Request body
{
  "settled_at": "2026-06-18T14:00:00+10:00",
  "settled_amount_cents": 81200000,
  "reference": "Loan ID LOAN-2026-3142",
  "device_signature": "<base64 ECDSA>"
}
Response body
{
  "handshake_id": "H-2026-05-00417",
  "chain_state": "H1+H2+H3+H4_COMPLETE",
  "commission_intent_id": "CI-2026-00417",
  "commission_breakdown": {
    "gross_cents": 81200000,
    "referrer_cents": 81200,
    "recipient_cents": 81200,
    "platform_cents": 8120,
    "rate_card_version": 14
  }
}
POST /api/handshakes/:id/outcome public idempotent
Client confirms the outcome (closes the loop). Becomes H5 — the 5th handshake.
Request body
{
  "magic_token": "mlo_z9y8x7w6…",
  "outcome_rating": 5,
  "outcome_text": "Settlement went smoothly, thanks for the referral"
}
Response body
{
  "handshake_id": "H-2026-05-00417",
  "chain_state": "COMPLETE",
  "payout_scheduled_for": "2026-06-25T00:00:00+10:00"
}

2 · Commission engine & payouts

POST /api/payouts/initiate admin idempotent
Admin pushes a commission intent into the Stripe Connect payout queue.
Request body
{ "commission_intent_id": "CI-2026-00417" }
Response body
{
  "payout_id": "PO-2026-00417",
  "stripe_transfer_id": "tr_1Q5z…",
  "stripe_application_fee_id": "fee_3Mz…",
  "rcti_pdf_url": "https://flip360.com.au/rcti/CI-2026-00417.pdf",
  "expected_arrival": "2026-06-26"
}
Errors
  • 409 ALREADY_PAID — Idempotency replay — original payout returned
  • 424 STRIPE_KYC_INCOMPLETE — Receiving member’s Stripe Connect account not fully onboarded
POST /api/stripe/webhook webhook idempotent
Stripe webhook receiver. Verifies signature, idempotency-keys on event.id, writes to ledger.
Request body
{ "id": "evt_3Mz…", "type": "transfer.paid", "data": { "object": { ... } } }
Response body
{ "received": true, "ledger_row_id": 88412 }
Errors
  • 400 BAD_SIGNATURE — Stripe webhook signature does not verify — request rejected
POST /api/simulate admin idempotent
Dry-run commission engine without writing. Used by the in-app Simulator.
Request body
{ "vertical_code": "MORTGAGE", "product_code": "HOME_LOAN_OO", "gross_cents": 80000000 }
Response body
{
  "rate_card_version": 14,
  "referrer_cents": 80000,
  "recipient_cents": 80000,
  "platform_cents": 8000,
  "explanation": "0.10% to referrer, 0.10% to recipient, 0.01% platform fee per Rate Card v14"
}

3 · Member & admin

GET /api/me/commissions member idempotent
Member portal: my commissions list with chain anchors.
GET /api/me/payouts member idempotent
Member portal: my payout history.
GET /api/dashboard admin idempotent
Admin: ARR, GMV, NPS, active deals, payout queue depth.
GET /api/ledger admin idempotent
Admin: append-only commission ledger, paginated, filterable.
GET /api/rules admin idempotent
Admin: list Rate Card versions.
POST /api/rules admin
Admin: create new Rate Card version (existing version frozen, new becomes active).

4 · Disputes

POST /api/disputes member idempotent
Member or client raises a dispute against a handshake or payout.
POST /api/disputes/:id/resolve admin idempotent
Admin closes a dispute with a written determination.

2  ·  Data model (D1 DDL)

The six tables that carry the trust posture. All money in INTEGER cents. Chain tables enforced INSERT-only at the application layer. Soft deletes on operational tables only.

Schema-freeze rule. After Gate 2 (Platform Lockdown), the schema is frozen. Any change requires a new migration file, a Steerco-approved exception, and a regression test that proves existing chain reads still verify. No exceptions.
handshakes
The 5-handshake chain. One row per handshake event. Append-only. SHA-256 linked.
Schema (DDL)
CREATE TABLE handshakes (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  handshake_uid   TEXT UNIQUE NOT NULL,        -- H-2026-05-00417
  type            TEXT NOT NULL,               -- 'INTENT'|'ACK'|'INTAKE'|'SETTLEMENT'|'OUTCOME'
  chain_seq       INTEGER NOT NULL,            -- 1..5
  referrer_id     INTEGER NOT NULL,
  recipient_id    INTEGER,
  client_id       INTEGER,
  vertical_id     INTEGER NOT NULL,
  product_id      INTEGER NOT NULL,
  gross_cents     INTEGER,                     -- populated at H4
  signature       TEXT NOT NULL,               -- ECDSA P-256 over canonical payload
  signature_alg   TEXT NOT NULL DEFAULT 'ES256',
  signing_key_id  TEXT NOT NULL,               -- which device key
  payload_hash    TEXT NOT NULL,               -- SHA-256 of canonical JSON
  hash_prev       TEXT,                        -- previous handshake hash (chain link)
  hash_self       TEXT UNIQUE NOT NULL,        -- SHA-256(payload_hash || hash_prev)
  block_id        INTEGER,                     -- which sealed block this row was anchored into
  idempotency_key TEXT UNIQUE,                 -- request-supplied or derived
  created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (referrer_id) REFERENCES contacts(id),
  FOREIGN KEY (recipient_id) REFERENCES contacts(id),
  FOREIGN KEY (client_id) REFERENCES contacts(id),
  FOREIGN KEY (vertical_id) REFERENCES verticals(id),
  FOREIGN KEY (product_id) REFERENCES products(id),
  FOREIGN KEY (block_id) REFERENCES chain_blocks(id)
);

CREATE INDEX idx_handshakes_uid ON handshakes(handshake_uid);
CREATE INDEX idx_handshakes_referrer ON handshakes(referrer_id);
CREATE INDEX idx_handshakes_recipient ON handshakes(recipient_id);
CREATE INDEX idx_handshakes_chain ON handshakes(handshake_uid, chain_seq);

-- App-layer enforcement: INSERT-only. No UPDATE, no DELETE.
-- Enforced via wrangler binding policy + code review checklist.
chain_blocks
Sealed blocks — Merkle root of handshakes in a time window, anchored externally hourly.
Schema (DDL)
CREATE TABLE chain_blocks (
  id                      INTEGER PRIMARY KEY AUTOINCREMENT,
  block_height            INTEGER UNIQUE NOT NULL,
  block_hash              TEXT UNIQUE NOT NULL,
  prev_block_hash         TEXT,                    -- NULL only on genesis
  merkle_root             TEXT NOT NULL,
  event_count             INTEGER NOT NULL,
  period_start            DATETIME NOT NULL,
  period_end              DATETIME NOT NULL,
  sealed_at               DATETIME DEFAULT CURRENT_TIMESTAMP,
  external_anchor_status  TEXT NOT NULL DEFAULT 'PENDING',  -- PENDING|ANCHORED|FAILED
  external_anchor_method  TEXT,                              -- 'NOTARY_HTTP'|'BTC_OPRETURN'|'ETH_L2'
  external_anchor_ref     TEXT,
  external_anchored_at    DATETIME
);
commission_intents
Derived from a chain-complete handshake. The "what we owe whom" record before payout.
Schema (DDL)
CREATE TABLE commission_intents (
  id                 INTEGER PRIMARY KEY AUTOINCREMENT,
  intent_uid         TEXT UNIQUE NOT NULL,            -- CI-2026-00417
  handshake_uid      TEXT NOT NULL,                   -- link to H4
  rate_card_version  INTEGER NOT NULL,
  gross_cents        INTEGER NOT NULL,
  referrer_cents     INTEGER NOT NULL,
  recipient_cents    INTEGER NOT NULL,
  platform_cents     INTEGER NOT NULL,
  status             TEXT NOT NULL DEFAULT 'PENDING', -- PENDING|PAID|DISPUTED|VOID
  created_at         DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (handshake_uid) REFERENCES handshakes(handshake_uid)
);
payouts
One row per Stripe Connect transfer. Idempotency-keyed on commission_intent_id.
Schema (DDL)
CREATE TABLE payouts (
  id                          INTEGER PRIMARY KEY AUTOINCREMENT,
  payout_uid                  TEXT UNIQUE NOT NULL,        -- PO-2026-00417
  commission_intent_id        INTEGER UNIQUE NOT NULL,     -- one-to-one — idempotent
  payee_member_id             INTEGER NOT NULL,
  amount_cents                INTEGER NOT NULL,
  currency                    TEXT NOT NULL DEFAULT 'AUD',
  stripe_transfer_id          TEXT,
  stripe_application_fee_id   TEXT,
  status                      TEXT NOT NULL DEFAULT 'QUEUED', -- QUEUED|SENT|PAID|FAILED|REVERSED
  initiated_at                DATETIME,
  arrived_at                  DATETIME,
  rcti_pdf_path               TEXT,
  created_at                  DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (commission_intent_id) REFERENCES commission_intents(id)
);
rate_cards
Versioned rule engine. Every change creates a new version; old versions are frozen, never updated.
Schema (DDL)
CREATE TABLE rate_cards (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  version         INTEGER UNIQUE NOT NULL,
  vertical_id     INTEGER NOT NULL,
  product_id      INTEGER,                          -- NULL = applies to whole vertical
  referrer_bps    INTEGER NOT NULL,                 -- basis points (10 = 0.10%)
  recipient_bps   INTEGER NOT NULL,
  platform_bps    INTEGER NOT NULL,
  effective_from  DATETIME NOT NULL,
  effective_to    DATETIME,                         -- NULL = currently active
  notes           TEXT,
  created_by      INTEGER,
  created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (vertical_id) REFERENCES verticals(id),
  FOREIGN KEY (product_id) REFERENCES products(id)
);
audit_log
Every admin action. Append-only. Who did what, when, against which row.
Schema (DDL)
CREATE TABLE audit_log (
  id              INTEGER PRIMARY KEY AUTOINCREMENT,
  actor_id        INTEGER NOT NULL,
  actor_role      TEXT NOT NULL,
  action          TEXT NOT NULL,
  target_table    TEXT NOT NULL,
  target_id       INTEGER,
  payload_json    TEXT,
  ip              TEXT,
  user_agent      TEXT,
  created_at      DATETIME DEFAULT CURRENT_TIMESTAMP
);

3  ·  Signing format (canonical payload & verification)

What an iPhone Secure Enclave (or Android StrongBox, or a WebAuthn passkey) actually signs. RFC-8785-style JSON canonicalisation, ECDSA over P-256, hardware-isolated key.

// Canonical handshake payload — what the device hashes & signs
//
// Rules:
//   - JSON.stringify with sorted keys (RFC 8785 JCS-style)
//   - No whitespace
//   - All timestamps in UTC ISO-8601 with millisecond precision
//   - All money in INTEGER cents
//
// Example (handshake H1 — INTENT):

{
  "alg": "ES256",
  "kid": "device-key-uid-7f3e2a1c-…",
  "handshake_uid": "H-2026-05-00417",
  "type": "INTENT",
  "chain_seq": 1,
  "referrer_id": 1042,
  "client_phone_hash": "sha256:e3b0c44…",
  "vertical_id": 1,
  "product_id": 3,
  "estimated_deal_cents": 80000000,
  "timestamp": "2026-05-21T04:31:18.412Z",
  "nonce": "8b2c4f1e9a7d3b6c"
}

// Signing scheme:
//   ECDSA over the secp256r1 curve (NIST P-256)
//   Hash function: SHA-256
//   Hardware: Apple Secure Enclave / Android StrongBox / WebAuthn passkey
//   Key never leaves the secure element — application sees signature only

// Verification (server side, in C2 commission engine):
//   1. Look up device's public key from members.signing_key_pubkey
//   2. Canonicalise payload (sorted keys, no whitespace)
//   3. SHA-256 → message digest
//   4. ECDSA-verify(pubkey, digest, signature) — constant-time
//   5. Reject if any of the above fails — return 400 INVALID_SIGNATURE

4  ·  Idempotency contract

The rule that makes "exactly once" actually work — even when networks drop, webhooks retry, or a user double-clicks the submit button.

// Every state-changing endpoint accepts an Idempotency-Key header.
// Same key + same body  →  same response (no second side-effect)
// Same key + different body  →  409 CONFLICT
// Missing key on a state-changing endpoint  →  400 IDEMPOTENCY_KEY_REQUIRED
//
// Storage (D1 table):

CREATE TABLE idempotency_keys (
  key             TEXT PRIMARY KEY,
  method          TEXT NOT NULL,
  path            TEXT NOT NULL,
  request_hash    TEXT NOT NULL,             -- SHA-256 of canonical body
  response_status INTEGER NOT NULL,
  response_body   TEXT NOT NULL,
  created_at      DATETIME DEFAULT CURRENT_TIMESTAMP,
  expires_at      DATETIME NOT NULL          -- 24h retention default
);

CREATE INDEX idx_idem_expires ON idempotency_keys(expires_at);

// Handler pseudocode:

async function withIdempotency(req, handler) {
  const key = req.headers.get('idempotency-key')
  if (!key) return error(400, 'IDEMPOTENCY_KEY_REQUIRED')

  const existing = await db.get('SELECT * FROM idempotency_keys WHERE key = ?', key)
  if (existing) {
    if (existing.request_hash !== hashBody(req.body)) return error(409, 'CONFLICT')
    return new Response(existing.response_body, { status: existing.response_status })
  }

  const result = await handler(req)
  await db.insert('idempotency_keys', { key, ..., response_body: result.body })
  return result
}

// Stripe webhooks: idempotency on event.id (Stripe-provided, globally unique).
// Internal endpoints: client-generated UUIDv4 + sliding 24-hour TTL.

5  ·  Sequence diagram — happy path, end-to-end

From referrer intent (H1) through to payout reconciliation and hourly notary anchoring. Solid arrows are synchronous request/response; teal dashed arrows are asynchronous (SMS, webhook).

Referrer (iOS) Client (SMS) Recipient (iOS) Platform (Worker) D1 (chain) Stripe Connect Notary H1 POST /api/handshakes/intent (sig by Secure Enclave) INSERT handshakes (chain_seq=1) SMS magic-link (Twilio) H2 POST /api/handshakes/:id/acknowledge INSERT handshakes (chain_seq=2) H3 POST /api/handshakes/:id/intake INSERT handshakes (chain_seq=3) H4 POST /api/handshakes/:id/settlement ($) Commission engine fires INSERT commission_intents SMS outcome link H5 POST /api/handshakes/:id/outcome INSERT handshakes (chain_seq=5) → CHAIN COMPLETE /api/payouts/initiate Transfer + ApplicationFee (idempotency-key = CI-uid) webhook: transfer.paid UPDATE payouts.status = 'PAID' Hourly: seal Merkle block POST merkle_root → notary

6  ·  Deployment topology

Every runtime, every region, every third-party. Australia-resident by default.

LayerTechnologyRegionNotes
Edge runtime Cloudflare Workers (V8 isolates) Sydney + global PoPs Stateless. Cold-start ~5ms. Per-request CPU budget 30ms (paid plan).
Database Cloudflare D1 (SQLite) Sydney (AU pin) Read replicas at edge. Durable. Daily backups + WAL.
Static assets Cloudflare Pages Global CDN Vite build → dist/. Brotli compressed. Long-cache hashed assets.
Payment rail Stripe Connect (AU) Stripe AU Application-fee-amount model. KYC handled by Stripe. AUD only at MVP.
SMS Twilio Australia Sydney Magic-link delivery for H2/H5. Inbound-route stub for replies.
Email transactional Postmark AU sending RCTI PDFs, payout confirmations, dispute notifications.
Object storage Cloudflare R2 AU bucket RCTI PDFs, dispute attachments. S3-compatible API.
Secrets Cloudflare Worker Secrets Encrypted at rest Stripe keys, Twilio keys, JWT signing key. Rotated quarterly.
Observability Workers Logpush + Logflare AU log sink Structured JSON logs. Error budget tracking. P95 latency dashboards.
Notary External multisig service Independent operator Hourly Merkle-root anchoring. Detached signature returned + stored on chain_blocks row.

7  ·  Observability plan

The eight metrics instrumented from day one. Every one has a target threshold, a dashboard, and an on-call response rule.

SeverityMetricWhat it measures
critical Chain integrity Every minute: re-verify hash-chain across last 1,000 handshakes. Alert on any mismatch.
critical Payout success rate Rolling 24h % of payouts that reach status=PAID without retry. Target ≥ 99.5%.
high Stripe webhook lag P95 time from Stripe event creation to D1 ledger write. Target < 5s.
medium Idempotency hit rate % of POSTs that match a prior Idempotency-Key. Detects retries & double-clicks.
high Signature reject rate % of /handshakes POSTs that fail signature verification. Detects abuse & key rotation issues.
high Block anchoring lag Time from block seal to external notary confirmation. Target < 1h.
medium RCTI generation latency P95 time from H5 to RCTI PDF available. Target < 30s.
medium Member onboarding funnel Daily funnel: applied → vetted → onboarded → first referral. Drives Sprint G decisions.

8  ·  Error catalogue

Every error code the platform can return — what it means, what client should do.

CodeMeaningRemediation
400 INVALID_SIGNATURE Hardware signature does not verify against the registered device key. Member re-registers their device.
400 IDEMPOTENCY_KEY_REQUIRED State-changing endpoint called without an Idempotency-Key header. Client retries with a UUIDv4.
409 CONFLICT Same Idempotency-Key reused with different request body. Client generates a new key for the new operation.
409 DUPLICATE_INTENT Two intents for the same client+vertical in the same 24h window. Original intent returned — no second handshake created.
410 TOKEN_EXPIRED Magic-link is older than 7 days. Member re-sends.
422 RATE_CARD_MISSING No active Rate Card for the supplied vertical+product. Admin creates Rate Card; intent retries.
424 STRIPE_KYC_INCOMPLETE Receiving member’s Connect account has not finished KYC. Member completes Stripe onboarding; payout queues automatically.
503 CHAIN_INTEGRITY_FAILURE Hash mismatch detected on read. All writes paused. Incident response. Read from last-known-good block; freeze writes until reconciled.
This is the implementation contract. A new engineer can read these eight sections, run npm run db:migrate:local, hit the APIs with curl, and start shipping. No tribal knowledge required.
Back to package See the EA Blueprint