Version. 0.7 Date. 2026-05-07 Provenance. Claude.ai scoping session. Operator: Marvin Percival. Status. Scoping note. Discovery-stage. Seventh draft. Three architectural insights from v0.6 discussion.
Prior versions and what changed.
credit_ledger table denominated in cents. FORAY as attestation bolted on.Additionally: invitation_code table renamed to credit_grant; email_grant_registry table added; account_status and related person columns added; suspension/deletion mechanism scoped to Accounting evaluator (Phase 48); request form acknowledged as requiring a public marketing website.
Discovery trajectory. Conventional accounting (v0.1) → FORAY-native (v0.2) → external engagement (v0.3) → full architecture (v0.4) → co-located infrastructure engagements (v0.5) → model-identified credits (v0.6) → grant-based delivery with email registry and lifecycle accountability (v0.7). The v0.7 insight: the Authority controls issuance, not just redemption. Credits are not requested in the freeform-code sense; they are delivered.
A growth infrastructure layer serving three purposes:
The credit system is governed through two Loomworks engagements — a Credit Management Engagement (the Authority) and an Accounting Engagement — whose data co-locates with the engine under a credit schema.
Credit Management Engagement (the Credits Authority). Writes FORAY flows for all credit-affecting events. Owns policy: oracle rates, grant decisions, email registry, referral policy, campaign data, model profiles. Its specialist writes issuance flows, consumption flows, suspension and deletion flows. The Companion advises the Operator on grant decisions and serves as the Authority's decision-making surface for inbound requests.
Accounting Engagement. Maintains balances from flows. Does not originate flows. Two mechanisms:
credit.foray_action_flows fires on each insert and updates the balance table atomically.Three constraints drive co-location:
The data lives under credit.* in the engine database. Logical separation (different schema) without operational separation (single connection pool, single backup).
Operational engagements. External data store, engine doesn't depend on it. Goosey storybook, FarmGuard.
Infrastructure engagements. Engine depends on the data operationally. Credit Management and Accounting. Data co-locates in engine database under separate schema.
The management surface is identical. Four rooms, approval cards, Companion, FORAY.
Same as v0.6. The pattern reuses for any metered-resource credit engagement (GPU minutes, filament, translation characters). The Accounting archetype reuses for any environment with FORAY flows.
loomworks_credit_haiku/sonnet/opus are distinct assets.Same as v0.6. Three credit asset types (haiku, sonnet, opus), six provider token types (per model × input/output), Whisper seconds, USD cents.
Same as v0.6. Per-credit-type rates. No cross-model conversion.
Same as v0.6. Classifier always Haiku. Responder model determined by the credit asset_id resolved for this turn. Multi-credit-type users use highest-tier first, automatic tier-drop when exhausted.
Same as v0.6 with two additions:
Unchanged. Credit issuance, token consumption, referral, token purchase, reconciliation correction.
Action flows:
{ asset_id: "loomworks_account_status", quantity: -1, from: "person_alice", to: "credit_authority",
metadata: { reason: "user_chose_suspension", expires_at: "2026-05-28T...", original_auth: "passkey_xyz" } }
The loomworks_account_status is a synthetic asset whose flow records lifecycle transitions for FORAY auditing. The trigger updates a synthetic balance (1 = active, 0 = suspended, -1 = deleted) but the operative state lives on the person table.
Action flows:
{ asset_id: "loomworks_account_status", quantity: -1, from: "person_alice", to: "credit_authority",
metadata: { reason: "user_chose_deletion", deletion_kind: "user_initiated" | "suspension_expired" } }
{ asset_id: "loomworks_credit_haiku", quantity: -<remaining>, from: "person_alice", to: "credit_authority",
metadata: { reason: "deletion_zero_balance" } }
The deletion flow zeroes any remaining balances and records the lifecycle transition. The person record is then anonymized (email replaced with deleted_<hash>, identifying fields nulled). The email hash persists in the registry. FORAY flows persist with the anonymized party_id.
Same as v0.6. One row per party per asset.
Same as v0.6. UPSERT on flow insert.
Below the pipeline (trigger). Balance arithmetic from flow inserts.
Through the pipeline (time-filtered evaluator). Phase 48+ responsibilities expanded to cover:
expires_at with remaining balance)account_status = 'suspended' whose expires_at is approaching (24-hour warning) or has passed (delete now).expires_at has passed for a suspended account, the deletion flow writes, the person record anonymizes, the email hash is preserved in the registry.The evaluator is the same Phase 44 trigger evaluator pattern applied to a wider set of conditions.
Credit Management Companion. Policy questions. Grant decisions. Email registry queries. Campaign performance.
Accounting Companion. State questions. Aggregate liability. Reconciliation health. Approaching expirations. Recent lifecycle transitions.
Same as v0.6. Multi-credit-type resolution returns highest-tier with positive balance.
Same as v0.6. Multi-flow per turn. Trigger fires.
def issue_grant(
recipient_email: str,
asset_id: str, # "loomworks_credit_haiku" etc.
amount: int,
grant_kind: str, # "form_initiated", "operator_curated", "referrer_initiated"
initiated_by: UUID | None, # person_id of Operator or referrer (null for form_initiated)
campaign_ref: str | None,
metadata: dict, # form answers, referrer context, etc.
) -> Grant:
"""Check email_grant_registry. If eligible, create credit_grant row, generate claim_token,
register email hash, return grant object with claim_token. Caller sends the email."""
def claim_grant(claim_token: str, person_id: UUID) -> ClaimResult:
"""Verify token, check not expired, check email matches the verifying person.
Write credit issuance flow. Mark grant as claimed. Trigger updates balance."""
def check_email_eligibility(email: str) -> EmailEligibility:
"""Hash the email (and the normalized form). Look up in email_grant_registry.
Returns: ELIGIBLE_NEW (never seen), ELIGIBLE_COOLED (last grant > N months ago),
INELIGIBLE_RECENT (last grant within N months), INELIGIBLE_DELETED (deleted account on this email).
Eligibility decision is made; the Authority can override (Operator-curated grants always issue)."""
def suspend_account(person_id: UUID, requested_by: UUID) -> None
def reactivate_account(person_id: UUID, auth_method: str) -> ReactivationResult
def delete_account(person_id: UUID, deletion_kind: str) -> None
def check_credit_balance(person_id: UUID, asset_id: str) -> int
def resolve_credit_model(person_id: UUID) -> CreditResolution | None
In-process Python module for alpha. Direct database calls. Interface abstraction for future HTTP transport.
This section replaces v0.6 §6 (Invitation codes). The conceptual shift: grants are delivered to specific emails by the Authority, not requested via freeform codes. The email registry is the abuse boundary.
Initiated → Issued → Delivered → Claimed → Consumed → Exhausted → [Choice]
│
┌───────────────────────────────────┼───────────────┐
│ │ │
Add own key Suspend Delete
│ │ │
Maker (N weeks hold) │
│ │
┌──────────┴──────────┐ │
│ │ │
Reactivate Expire │
│ │ │
Active Delete ────┘
│
Email hash retained
Phases:
Form-initiated grant. Anyone can request via the public form (§8). Companion-as-Authority decides eligibility and credit type. The primary public path.
Operator-curated grant. The Operator (with Companion advice) issues grants to specific email lists. Used for known audiences: conference attendee lists, VIP demos, partner outreach. Companion advises on credit type per audience profile.
Referrer-initiated grant. A current Loomworks user invites someone by providing the friend's email. Same flow as form-initiated but with referred_by recorded.
All three flavors share: the email registry check, the claim token mechanism, the email delivery, and the FORAY attestation.
credit.email_grant_registry
├── email_hash VARCHAR PRIMARY KEY — SHA-256 of normalized email
├── email_normalized_hash VARCHAR — SHA-256 of aggressively normalized form (gmail+plus stripped, etc.)
├── first_grant_at TIMESTAMPTZ
├── last_grant_at TIMESTAMPTZ
├── total_grants_issued INTEGER DEFAULT 1
├── last_grant_status VARCHAR — 'pending_claim', 'claimed', 'expired', 'deleted'
├── last_status_at TIMESTAMPTZ
Two hash columns:
email_hash: SHA-256 of the email exactly as the user submitted (lowercase, trim).email_normalized_hash: SHA-256 of the aggressively normalized form. Strips Gmail +plus-addressing (alice+kaspathon@gmail.com → alice@gmail.com). Strips dots in Gmail local-parts (a.l.i.c.e@gmail.com → alice@gmail.com). Other provider-specific normalization rules as discovered.Eligibility check uses both: a match on either hash means "we've seen this human before." This catches casual alias variations without requiring DUNIN7 to maintain the email itself.
The registry persists past person deletion. That's the whole point — deletion zeros the credits and anonymizes the person, but the email_hash registry remembers that this email previously received and exhausted a grant. A new signup attempt with the same email (by hash) returns INELIGIBLE_DELETED from the eligibility check; the user can still create an account, but as a Maker with their own key, not as a fresh trial.
credit schema)
credit.credit_grant
├── id UUID PRIMARY KEY
├── claim_token VARCHAR(64) UNIQUE — opaque token in the email link
├── recipient_email VARCHAR — stored only until claim; then nulled
├── recipient_email_hash VARCHAR — links to email_grant_registry
├── grant_kind VARCHAR — 'form_initiated', 'operator_curated', 'referrer_initiated'
├── asset_id VARCHAR — 'loomworks_credit_haiku' etc.
├── credit_amount INTEGER
├── initiated_by UUID | NULL — Operator or referrer person_id
├── campaign_ref VARCHAR | NULL
├── metadata JSONB — form answers, referrer note, etc.
├── status VARCHAR — 'pending_claim', 'claimed', 'expired', 'revoked'
├── expires_at TIMESTAMPTZ — claim deadline (e.g., 30 days from issuance)
├── claimed_at TIMESTAMPTZ | NULL
├── claimed_by_person_id UUID | NULL
├── created_at TIMESTAMPTZ
After claim, recipient_email is nulled (the hash remains in the registry; the cleartext email is no longer needed). After 30 days unclaimed, status becomes expired and a new grant can be issued to that email if eligibility allows.
https://app.loomworks.com/claim?token=<claim_token>.POST /claim/grant.pending_claim or expires_at passed → error.claimed, update email_grant_registry status to claimed.All within one database transaction.
There is no POST /signup-with-code endpoint. There is no freeform code field on signup. The substrate does not accept "I have this code, give me credits." It only accepts "I have this claim_token, give me the credits that token references." The token is generated by the Authority at issuance time and only sent via email. The user cannot manufacture a token; they can only consume one that was sent to them.
The require_invitation_code config flag from v0.6 D5 goes away. There is no flag because there is no path for codes-without-grants. Cold signup (no grant) goes to Maker with own key — a separate path from the trial path.
A web form accessible without authentication. Located on the public Loomworks marketing website (which is itself a separate deliverable — see §16). Three or four questions.
Candidate questions. Tunable based on early signal:
Plus the required field: email address.
The form is short by design. The questions are not gates — every requester gets something. The questions decide which credit type fits.
Form submissions arrive as messages in a Companion conversation on Instance A. The Companion's system prompt for grant decisions:
> You are the Credits Authority for Loomworks. Someone has requested trial credits via the public form. Read their answers below. Consult the model profile assertions in your Memory (which describe what each model is good at and which audience benefits from each). Decide which credit type and amount is appropriate. Issue the grant via the seam. Briefly note your reasoning in the grant metadata.
The Companion reads:
loomworks_credit_haiku/sonnet/opus profiles in §9.2 of v0.6)The Companion decides:
The Companion writes:
issue_grant call via the seam
Alpha (Phase 47). No public form. Operator-curated grants only. The Operator manually triggers issue_grant calls (via the admin endpoint) for known emails. The substrate supports the form-initiated flow (the database tables, the seam, the claim mechanism), but no public surface yet.
Phase 48. Public form on the Loomworks marketing website. SMTP for email delivery. Companion-driven grant decisions. Form answers stored as Memory in the Credit Management Engagement.
Future. Form questions iterated based on observed signal. Operator can refine via the engagement pipeline (update model profile assertions; revise grant logic in the Companion's prompt; introduce additional questions if Memory observations show gaps).
Replaces v0.6 §7 (Referral system). Conceptually simpler with the grant model.
check_email_eligibility(alice@example.com).issue_grant(recipient=alice@example.com, asset_id=<policy choice>, amount=<policy>, grant_kind="referrer_initiated", initiated_by=<referrer's person_id>).The previous v0.6 D8 (default referral max_uses = 5) shifts meaning. There is no shareable code with multiple uses; each referral is a specific grant to a specific email. The rate limit becomes: how many referral grants a referrer can initiate per period (default: 5 per 30 days, admin-adjustable per person).
The trust escalation pattern still applies — referrers whose referrals convert at high rates earn higher rate limits.
When alice (referee) adds her own API key (converts to Maker), the engine detects this state transition and calls issue_referral_credit via the seam. A flow writes credit to the referrer's balance. Idempotent: at most one conversion credit per referee.
The credit type for the conversion credit is policy: default is to mirror what alice received (Haiku referee → Haiku conversion credit to referrer). Operator can override per-campaign.
Same as v0.6.
The Companion serves as the Authority's decision-making surface. Two modes:
The Companion is autonomous on inbound by default (small grants, registered eligibility, low risk). Operator can require pre-approval if needed by setting a config flag or by Companion threshold (e.g., grants over a certain amount require approval card). Default: pre-approval not required for form-initiated grants up to standard amount; required for unusual amounts or distinguished requests.
Same as v0.6 §9.2. Three core assertions describe what each model is suited for. Updated through the engagement pipeline when new models ship.
When a person's credits exhaust (no other credit type available), the Companion presents the choice:
> "Your trial credits are used up. You've built [N engagements] and [M Library artifacts] in your time here. What would you like to do? > > Add your own API key — keep going with Loomworks at full capability. The Companion gets smarter with your own Sonnet key, and everything you've built stays. > > Hold for now — your account stays accessible for [N weeks]. You can come back any time within that window and add a key. After that, the account closes. > > Close the account now — clean exit. Everything you've built goes away."
The Companion is suitability-aware (per v0.6 §9.4) — for a user who's been doing specification work, the "add key" option mentions Sonnet's reasoning depth specifically.
Same as v0.6 §10. Trial / Maker / Professional / Team. License tier gates feature capability; credit type gates model selection (system-key path).
person.account_status:
active — normal operating stateexhausted — credits hit zero, choice not yet made (transient, typically resolved within one conversation)suspended — user chose hold; expires_at set N weeks out; reactivation via original auth restores accessdeleted — anonymized; email hash in registry; FORAY records persistTransitions:
active ──exhaust──→ exhausted
exhausted ──add_key──→ active (now Maker)
exhausted ──suspend──→ suspended
exhausted ──delete──→ deleted
suspended ──reactivate──→ active (no fresh credits)
suspended ──expires_at_passed──→ deleted
expires_at set at suspension to N weeks out (default: 21 days, admin-adjustable). Reactivation requires authenticating with the original auth method (passkey or org SSO from the original signup). Reactivation does NOT issue fresh credits — credits at suspension were zero (that's how we got to the choice). Reactivation just restores access to the existing account so the user can add their own key.
Triggered by either user choice (delete now) or suspension expiry. Sequence:
deleted_<hash>@deleted.local, identifying fields nulled, account_status = 'deleted'.last_grant_status = 'deleted', last_status_at = now(). Hash retained for future eligibility checks.The deletion is FORAY-attested. Subsequent reconciliation can verify the deletion was complete and consistent.
A future signup attempt with the same email (by hash) returns INELIGIBLE_DELETED from check_email_eligibility. The user can still create an account, but as a Maker with their own key (no trial credits issue). The Authority can override this for specific cases via Operator decision through the engagement pipeline.
Same as v0.6 §11. Per-instance credit schema. Cross-instance consumption reporting. DUNIN7 HQ (Instance A) holds the Authority and the registry.
Public marketing website. Required for the form-initiated grant flow. See §16 (What this does not build).
All v0.6 decisions carry forward except D5 (which is now obsolete — see §7.6). New v0.7 decisions D13–D17.
| # | Decision | Setting |
|---|----------|---------|
| D1 | Trial credit amount | 10,000 per grant (admin-settable per code) |
| D2 | Referral credit amount (conversion credit to referrer) | 10,000 |
| D3 | Oracle rate seeding | Per-credit-type, Anthropic-mirrored |
| D4 | Grant token format | Opaque 64-char tokens (replaces freeform codes from v0.6) |
| D5 | ~~Signup gate~~ | Obsolete — trial path is grant-claim by construction |
| D6 | Credit exhaustion behavior | Three-choice: add-key / suspend / delete |
| D7 | Integration seam | In-process for alpha |
| D8 | Default referrer rate limit | 5 referrals per 30 days, admin-adjustable per person |
| D9 | License tier pricing | Maker $0, Pro $29, Team $19/seat (min 3) |
| D10 | Credit data location | Engine DB, credit schema |
| D11 | Migration management | Single Alembic chain, schema-qualified |
| D12 | Consumption recording timing | Fire-and-forget |
| D13 | Email hashing scheme | SHA-256 of (a) lowercase trimmed email and (b) aggressively normalized form. Both hashes stored. Match on either = same human. |
| D14 | Suspension period | 21 days from suspension to deletion. Admin-adjustable. |
| D15 | Deletion warning window | 24 hours before deletion fires. Email reminder with reactivation link. |
| D16 | Account state machine | active / exhausted / suspended / deleted with transitions per §12.2 |
| D17 | Companion authority for form-initiated grants | Autonomous up to default amount (10,000). Approval card required for amounts above default or for INELIGIBLE_DELETED override. |
Same as v0.6 §14. System key store, three-tier resolution, token capture, person table, FORAY hooks, trigger evaluator, SSE channel.
Phase 47 (alpha substrate):
Credit schema:
credit.foray_action_flows tablecredit.asset_balances table + triggercredit.credit_grant table (replaces invitation_code)credit.email_grant_registry tablecredit.oracle_rate_config tableEngine modifications:
referred_by, license_tier, account_status, expires_at, previous_status_change_at columns on personsystem_config seeds for model selection, default amounts, suspension periodPOST /claim/grant endpoint (token validation, email verification, person creation/attachment, credit issuance)POST /admin/grants endpoint (Operator-curated grant issuance)Estimated: ~80–100 new tests for Phase 47 (up from v0.6's 60–80 because of the grant lifecycle, email registry, and account state additions).
Phase 48+ (Companion intelligence and lifecycle automation):
loomworks-hosting-cost-analysis-v0_1.md — hosting comparisonloomworks-deployment-strategy-v0_1.md — fleet architecture (needs v0.2 for co-located credit schema; needs to add public marketing website as a deployment artifact)loomworks-white-label-multilanguage-analysis-v0_1.md — codebase audit, i18n roadmaploomworks-phase-47-scoping-note-v0_3.md — Phase 47 construction scope (this document drives v0.3)loomworks-queued-directions-and-deferred-work-v0_2.md — agent capability development entriesv0.1 (2026-05-07). Conventional accounting.
v0.2 (2026-05-07). FORAY-native.
v0.3 (2026-05-07). External engagement.
v0.4 (2026-05-07). Full system architecture, nine open decisions.
v0.5 (2026-05-07). Co-located data, two-engagement architecture, trigger-based accounting, per-tier model selection.
v0.6 (2026-05-07). Model-identified credits, suitability-aware Companion, per-credit-type routing.
v0.7 (2026-05-07). Three architectural insights, all decisions extended:
invitation_code table renamed to credit_grant. New email_grant_registry table. Person table gains account_status, expires_at, previous_status_change_at.issue_grant, claim_grant, check_email_eligibility, suspend_account, reactivate_account, delete_account.Discovery trajectory: conventional accounting → FORAY-native → external engagement → full architecture → co-located infrastructure engagements → model-identified credits → grant-based delivery with email registry and lifecycle accountability. Seven iterations. The credit became the instruction (v0.6); the credit became something delivered, not requested (v0.7).
DUNIN7 — Done In Seven LLC — Miami, Florida Invitation codes, credit system, and referral architecture — scoping note v0.7 — 2026-05-07