Monaco architecture sketch (composer candidate)¶
Signatures only. Bodies panic("not implemented") or // TODO. Types encode invariants.
Module map¶
monaco/
├── packages/domain/ # Go module — pure math, no I/O
│ ├── money.go # USDCMicros, ShareUnits, rounding
│ ├── nav.go # NavUSDCOnly, NavMarked, PotNAV
│ ├── shares.go # SharesForDeposit, equity
│ ├── pnl.go # NetUsdcIn, PercentReturn, rank
│ ├── votes.go # Tally, threshold, expiry
│ └── redeem.go # SliceUsdc, stock sell fractions
│
├── apps/backend/
│ ├── main.go # wire deps, register routes, M3 dev routes flag
│ └── internal/
│ ├── config/ # DATABASE_URL, Privy, relayer keypair (fail startup if missing M1)
│ ├── middleware/ # auth session, request id
│ ├── handler/ # HTTP parse/encode only
│ │ ├── auth.go
│ │ ├── me.go
│ │ ├── groups.go
│ │ ├── deposits.go # M2+
│ │ ├── swap.go # M3 order status GETs
│ │ ├── dev/stub_buy.go # M3 only — DELETE in M4
│ │ ├── proposals.go # M4
│ │ ├── votes.go # M4
│ │ ├── portfolio.go # M4 group pot + boards
│ │ └── redeem.go # M4
│ ├── service/ # orchestration; calls ledger + adapters
│ │ ├── auth.go
│ │ ├── group.go
│ │ ├── deposit.go # intent + poller tick
│ │ ├── buy_on_pass.go # M4 — replaces stub
│ │ └── redeem.go
│ ├── ledger/ # sole writer: shares, net_usdc_in, nav_snapshots
│ │ ├── ledger.go
│ │ └── types.go
│ ├── governance/ # proposals, votes, tally, expiry job
│ │ └── governance.go
│ ├── treasury/ # Privy sign, RPC balance reads
│ │ └── treasury.go
│ ├── swap/ # Jupiter quote/execute/poll; xStock sell
│ │ └── swap.go
│ ├── privy/ # auth verify, wallet create, sign tx
│ ├── jupiter/ # wire client — private
│ ├── xstocks/ # mint resolver — private
│ ├── pyth/ # equity marks — private
│ └── db/ # SQL repos — private to above
│
├── apps/mobile/Monaco/
│ ├── API/
│ │ ├── MonacoAPIClient.swift
│ │ └── DTOs/ # hand-written Codable mirrors Go JSON
│ ├── Features/ # M1–M5 screens per milestone
│ │ ├── Auth/
│ │ ├── Groups/
│ │ ├── Deposit/
│ │ ├── DevSwap/ # M3 debug — remove UI M5
│ │ ├── Proposals/
│ │ ├── Portfolio/
│ │ ├── Home/
│ │ ├── Redeem/
│ │ └── Settings/
│ └── Display/ # percent/P&L formatters (M5 tests)
│
└── supabase/migrations/ # one migration per milestone tranche
Dependency rule: handler → service → (ledger | governance | swap | treasury) → db / adapters. service and ledger import packages/domain. Nothing in packages/domain imports apps/backend.
packages/domain¶
// money.go
package domain
type USDCMicros int64 // invariant: >= 0
type ShareUnits string // decimal string; invariant: > 0 when crediting/debiting
func (u USDCMicros) IsZero() bool
func ParseShareUnits(s string) (ShareUnits, error)
// nav.go
type NavMode int
const (
NavUSDCOnly NavMode // M2: treasury USDC / total shares; empty → $1/share
NavMarked NavMode // M4: USDC + Σ(token units × mark)
)
type NavInput struct {
Mode NavMode
TreasuryUsdc USDCMicros
TotalShares ShareUnits // zero ⇒ first-deposit price $1
Positions []MarkedPosition // ignored unless NavMarked
}
type MarkedPosition struct {
Symbol string
Units string // decimal
MarkUsdc USDCMicros // per-unit from Pyth or fill
CostBasis USDCMicros // Jupiter fill — display only
AfterHours bool
}
type PotNAV struct {
TotalUsdc USDCMicros
PerShareUsdc USDCMicros // PotNAV / total shares; empty pot uses 1_000_000 ($1)
}
func ComputePotNAV(in NavInput) (PotNAV, error)
// shares.go
func SharesForDeposit(deposited USDCMicros, nav PotNAV) (ShareUnits, error)
// invariant: shares = deposited / nav.PerShareUsdc
func MemberEquity(memberShares, totalShares ShareUnits, nav PotNAV) (USDCMicros, error)
// invariant: (member/total) × pot
// pnl.go
type NetUsdcIn USDCMicros // sweeps in − redeem payouts; per member per group
func PercentReturn(equity USDCMicros, netIn NetUsdcIn) (*float64, error)
// nil when netIn == 0 (skip board row)
type LeaderboardRow struct {
SubjectID string
DisplayName string
Equity USDCMicros
NetIn NetUsdcIn
PctReturn float64
DollarPnL USDCMicros
}
func RankByPercentReturn(rows []LeaderboardRow) []LeaderboardRow
// votes.go
type VoteThreshold string
const (
ThresholdUnanimous VoteThreshold = "unanimous"
ThresholdMajority VoteThreshold = "majority"
)
type VoteChoice string
const (
VoteYes VoteChoice = "yes"
VoteNo VoteChoice = "no"
)
type ProposalStatus string
const (
ProposalOpen ProposalStatus = "open"
ProposalPassed ProposalStatus = "passed"
ProposalFailed ProposalStatus = "failed"
ProposalExpired ProposalStatus = "expired"
)
type VoterSetMode string
const (
VoterSetAllMembers VoterSetMode = "all_members"
VoterSetNamed VoterSetMode = "named_subset"
)
type VoteTallyInput struct {
Threshold VoteThreshold
VoterIDs []string // resolved voter set at proposal time
Cast map[string]VoteChoice
ExpiresAt time.Time
Now time.Time
}
func TallyProposal(in VoteTallyInput) (ProposalStatus, error)
// redeem.go
type RedeemSliceInput struct {
SharesRedeemed ShareUnits
MemberShares ShareUnits
TotalShares ShareUnits
Nav PotNAV
Positions []MarkedPosition
}
type RedeemSlice struct {
UsdcOwed USDCMicros
StockSellFrac float64 // sharesRedeemed/totalShares applied per position
}
func ComputeRedeemSlice(in RedeemSliceInput) (RedeemSlice, error)
apps/backend — core types¶
// internal/ledger/types.go
package ledger
type TxSignature string // Solana sig — idempotency key for sweeps/fills/payouts
type SnapshotReason string
const (
SnapshotDeposit SnapshotReason = "deposit"
SnapshotFill SnapshotReason = "fill"
SnapshotRedeem SnapshotReason = "redeem"
)
type SweepCredit struct {
GroupID uuid.UUID
UserID uuid.UUID
Signature TxSignature
UsdcMicros domain.USDCMicros
NavMode domain.NavMode
Marked []domain.MarkedPosition // required when NavMarked
}
type RedeemDebit struct {
GroupID uuid.UUID
UserID uuid.UUID
Shares domain.ShareUnits
RedeemID uuid.UUID // idempotency for full redeem flow
PayoutProof PayoutProof
}
type PayoutProof struct {
Pubkey string
Message string
Signature string // ed25519 — verified before debit
}
type ShareBalance struct {
GroupID uuid.UUID
UserID uuid.UUID
Shares domain.ShareUnits
}
// internal/ledger/ledger.go
type Ledger interface {
ApplySweepCredit(ctx context.Context, in SweepCredit) error
// idempotent on Signature; rejects if USDC only in member wallet (caller must confirm treasury credit)
DebitShares(ctx context.Context, in RedeemDebit) (domain.PotNAV, error)
// row-lock share_ledger; returns NAV at debit for slice math; idempotent on RedeemID
RecordRedeemPayout(ctx context.Context, redeemID uuid.UUID, sig TxSignature, usdc domain.USDCMicros) error
// decrements net_usdc_in; writes nav_snapshot; idempotent on payout sig
GetShareBalance(ctx context.Context, groupID, userID uuid.UUID) (ShareBalance, error)
GetTotalShares(ctx context.Context, groupID uuid.UUID) (domain.ShareUnits, error)
WriteSnapshot(ctx context.Context, groupID uuid.UUID, reason SnapshotReason, nav domain.PotNAV) error
}
// internal/governance/governance.go
type JoinPolicy struct {
Mode string // "open" | "password"
Password *string
}
type CreateGroupInput struct {
Name string
CreatorID uuid.UUID
JoinPolicy JoinPolicy
VoterSetMode domain.VoterSetMode
VoterMemberIDs []uuid.UUID // min 1 when named
Threshold domain.VoteThreshold
VoteExpiry time.Duration
}
type Proposal struct {
ID uuid.UUID
GroupID uuid.UUID
ProposerID uuid.UUID
Symbol string
UsdcAmount domain.USDCMicros
Status domain.ProposalStatus
ExpiresAt time.Time
}
type Governance interface {
CreateGroup(ctx context.Context, in CreateGroupInput) (uuid.UUID, error) // M4 replaces M1 thin create
JoinGroup(ctx context.Context, groupID, userID uuid.UUID, password *string) error
CreateProposal(ctx context.Context, p Proposal) (uuid.UUID, error)
// M4-T12: refuse if swap.PreviewBuy returns no route
CastVote(ctx context.Context, proposalID, voterID uuid.UUID, choice domain.VoteChoice) error
CloseExpired(ctx context.Context, now time.Time) error // cron/tick
OnPassed(ctx context.Context, proposalID uuid.UUID) error // emits to buy_on_pass
}
// internal/swap/swap.go
type OrderSide string
const (
SideBuy OrderSide = "buy"
SideSell OrderSide = "sell"
)
type SwapOrder struct {
ID uuid.UUID
GroupID uuid.UUID
Side OrderSide
InputMint string
OutputMint string
Status string // pending|success|failed
}
type Swap interface {
PreviewBuy(ctx context.Context, symbol string, usdc domain.USDCMicros) (bool, error) // false ⇒ no route
Buy(ctx context.Context, groupID uuid.UUID, symbol string, usdc domain.USDCMicros, idempotencyKey string) (TxSignature, error)
SellSlice(ctx context.Context, groupID uuid.UUID, positions []domain.MarkedPosition, frac float64, idempotencyKey string) (TxSignature, error)
// polls Jupiter execute until Success code 0; persists jupiter_orders + fills + cost_basis
}
// internal/treasury/treasury.go
type WalletKind string
const (
WalletMember WalletKind = "member"
WalletTreasury WalletKind = "treasury"
)
type Treasury interface {
MemberUsdcBalance(ctx context.Context, userID uuid.UUID) (domain.USDCMicros, error)
TreasuryBalances(ctx context.Context, groupID uuid.UUID) (usdc domain.USDCMicros, tokens []TokenBalance, error)
SweepUsdcToTreasury(ctx context.Context, userID, groupID uuid.UUID, amount domain.USDCMicros) (TxSignature, error)
SendUsdcPayout(ctx context.Context, groupID uuid.UUID, destPubkey string, amount domain.USDCMicros) (TxSignature, error)
// relayer pays SOL fees on all signed txs
}
type TokenBalance struct {
Mint string
Symbol string
Units string
}
// internal/service/deposit.go
type DepositService interface {
StartIntent(ctx context.Context, userID, groupID uuid.UUID, expected domain.USDCMicros) (uuid.UUID, error)
PollIntent(ctx context.Context, intentID uuid.UUID) (DepositStatus, error)
// tick: detect member balance → sweep → on confirmed sig call Ledger.ApplySweepCredit(NavUSDCOnly)
}
// internal/service/buy_on_pass.go (M4)
type BuyOnPassService interface {
Execute(ctx context.Context, proposalID uuid.UUID) error
// load passed proposal → swap.Buy → ledger.WriteSnapshot(fill) → update proposal linked fill
}
// internal/service/redeem.go (M4)
type RedeemService interface {
Complete(ctx context.Context, req RedeemRequest) (RedeemResult, error)
}
type RedeemRequest struct {
GroupID uuid.UUID
UserID uuid.UUID
Shares domain.ShareUnits // or derive from UsdcTarget via inverse equity
UsdcTarget *domain.USDCMicros
PayoutProof ledger.PayoutProof
}
type RedeemResult struct {
RedeemID uuid.UUID
UsdcPaid domain.USDCMicros
PayoutSig ledger.TxSignature
}
// ordering inside Complete:
// 1. verify PayoutProof
// 2. ledger.DebitShares (txn)
// 3. swap.SellSlice if stock positions
// 4. treasury.SendUsdcPayout
// 5. ledger.RecordRedeemPayout
Postgres data models (by milestone)¶
M1¶
| Table | Key columns |
|---|---|
users |
id, privy_user_id UNIQUE, display_name, created_at |
member_wallets |
id, user_id UNIQUE FK, privy_wallet_id, solana_address, created_at |
groups |
id, name, creator_user_id FK, created_at — M4 adds policy columns |
treasuries |
id, group_id UNIQUE FK, privy_wallet_id, solana_address, created_at |
M1 also inserts group_members(creator) so M2 deposit has membership FK.
M2¶
| Table | Key columns |
|---|---|
deposit_intents |
id, user_id, group_id, expected_usdc_micros, status (pending|swept|failed), created_at |
sweep_tx_log |
signature PK, intent_id, group_id, user_id, usdc_micros, status, confirmed_at |
share_ledger |
group_id, user_id PK, shares NUMERIC, updated_at |
member_net_usdc_in |
group_id, user_id PK, net_micros — increment on sweep credit |
NAV in M2: domain.ComputePotNAV(NavUSDCOnly{TreasuryUsdc, TotalShares}).
M3¶
| Table | Key columns |
|---|---|
jupiter_orders |
id, group_id, side, input_mint, output_mint, notional_micros, status, idempotency_key UNIQUE |
fills |
id, order_id, signature UNIQUE, input_amount, output_amount, price_usdc, filled_at |
cost_basis |
group_id, symbol PK, units, avg_cost_usdc, updated_at |
M3 dev-only route (deleted M4):
POST /v1/dev/groups/{groupId}/execute-buy
Body: { "symbol": "AAPLx", "usdc_micros": 10000000 }
→ swap.Buy directly, no proposal
M4 (extends M1 groups, adds governance + snapshots)¶
| Table | Key columns |
|---|---|
groups + |
join_mode, join_password_hash, voter_set_mode, threshold, vote_expiry_seconds |
group_members |
group_id, user_id PK, joined_at |
group_voters |
group_id, user_id — when named subset |
proposals |
id, group_id, proposer_id, symbol, usdc_micros, status, expires_at, fill_signature NULL |
votes |
proposal_id, voter_id PK, choice, cast_at |
nav_snapshots |
id, group_id, pot_nav_micros, nav_per_share_micros, total_shares, reason, created_at |
redeem_requests |
id, group_id, user_id, shares, status, payout_pubkey, payout_sig NULL |
payout_proofs |
redeem_id, message, signature — audit |
Marked NAV: treasury RPC balances + pyth marks → domain.NavMarked.
M4 deletes: handler/dev/stub_buy.go, route registration, mobile DevSwap from main flow.
M5¶
No new tables. Swift Codable DTOs mirror M4 JSON responses.
HTTP handlers (signatures)¶
// M1
func (h *AuthHandler) PostSession(w http.ResponseWriter, r *http.Request) // Privy token → user + member wallet
func (h *MeHandler) Get(w http.ResponseWriter, r *http.Request)
func (h *GroupsHandler) Post(w http.ResponseWriter, r *http.Request) // M1 thin; M4 full CreateGroupInput
func (h *GroupsHandler) GetByID(w http.ResponseWriter, r *http.Request)
// M2
func (h *DepositsHandler) Post(w http.ResponseWriter, r *http.Request) // start intent
func (h *DepositsHandler) GetByID(w http.ResponseWriter, r *http.Request) // poll status + shares credited
func (h *GroupsHandler) GetShareBalance(w http.ResponseWriter, r *http.Request)
func (h *GroupsHandler) GetTreasuryUsdc(w http.ResponseWriter, r *http.Request)
// M3
func (h *DevStubBuyHandler) PostExecuteBuy(w http.ResponseWriter, r *http.Request) // DELETE M4
func (h *SwapHandler) GetOrder(w http.ResponseWriter, r *http.Request)
func (h *SwapHandler) GetCostBasis(w http.ResponseWriter, r *http.Request)
// M4
func (h *GroupsHandler) PostJoin(w http.ResponseWriter, r *http.Request)
func (h *CatalogHandler) GetSearch(w http.ResponseWriter, r *http.Request) // xStocks symbols
func (h *ProposalsHandler) Post(w http.ResponseWriter, r *http.Request)
func (h *ProposalsHandler) GetByID(w http.ResponseWriter, r *http.Request)
func (h *VotesHandler) Post(w http.ResponseWriter, r *http.Request)
func (h *PortfolioHandler) GetGroup(w http.ResponseWriter, r *http.Request) // pot, you, member board
func (h *PortfolioHandler) GetHome(w http.ResponseWriter, r *http.Request) // group + people boards
func (h *RedeemHandler) Post(w http.ResponseWriter, r *http.Request)
Per-milestone request flows¶
M1 — Auth and wallets¶
Mobile Privy login
→ POST /v1/auth/session { privy_token }
→ privy.Verify → db.UpsertUser → privy.CreateMemberWallet (idempotent)
→ GET /v1/me { user_id, display_name, member_wallet_address }
Create group (thin)
→ POST /v1/groups { name }
→ db.InsertGroup → privy.CreateTreasuryWallet → db.InsertTreasury
→ db.InsertGroupMember(creator)
→ GET /v1/groups/{id} { name, treasury_address }
M2 — Deposits and sweeps¶
POST /v1/groups/{id}/deposits { expected_usdc }
→ deposit_intents row (pending)
Background poller (or GET poll triggers tick)
→ treasury.MemberUsdcBalance > 0
→ treasury.SweepUsdcToTreasury → sweep_tx_log(signature)
→ RPC confirm signature
→ ledger.ApplySweepCredit(NavUSDCOnly) — idempotent on signature
→ domain.SharesForDeposit
→ share_ledger += shares
→ member_net_usdc_in += swept
→ nav_snapshots (reason=deposit) [optional M2; required M4-T37]
Reject path: USDC in member wallet but sweep not confirmed → zero share_ledger change
M3 — Jupiter buy and sell (stub)¶
POST /v1/dev/groups/{id}/execute-buy { symbol, usdc_micros } // no vote
→ xstocks.ResolveMint(symbol)
→ swap.PreviewBuy — 404 if no route
→ swap.Buy → poll Jupiter execute Success+0
→ fills + cost_basis rows (idempotent on signature)
Sell (backend-only prep for M4 redeem)
→ swap.SellSlice or swap.Buy inverse path for treasury xStock → USDC
M4 — Domain logic¶
Create group (full)
→ governance.CreateGroup { join, voter set, threshold, expiry }
Join
→ POST /v1/groups/{id}/join { password? }
→ group_members row
Propose buy
→ POST /v1/proposals { group_id, symbol, usdc_micros }
→ swap.PreviewBuy must pass
→ proposals row (open), expires_at = now + group.vote_expiry
Vote
→ POST /v1/proposals/{id}/votes { choice }
→ domain.TallyProposal → update status
→ on passed: buy_on_pass.Execute
→ swap.Buy (treasury signs)
→ ledger.WriteSnapshot(fill)
→ link proposal.fill_signature
→ DELETE dev stub route
Portfolio
→ treasury.TreasuryBalances + pyth marks
→ domain.ComputePotNAV(NavMarked)
→ per-member domain.MemberEquity + domain.PercentReturn
→ domain.RankByPercentReturn (skip net_in=0)
Redeem (debit-first)
→ POST /v1/redeems { group_id, shares | usdc_target, payout_proof }
→ verify ed25519 ownership proof
→ ledger.DebitShares (row lock) → PotNAV at debit
→ domain.ComputeRedeemSlice
→ swap.SellSlice for stock fraction (skip if USDC-only pot)
→ treasury.SendUsdcPayout to verified pubkey only
→ ledger.RecordRedeemPayout → nav_snapshot(redeem)
→ boards recompute from net_usdc_in + current marks
Open decisions (M4-T39–T41): ticket files only, no branching logic chosen.
M5 — Mobile product UI¶
Session gate → HomeBoardsView (group + people)
→ GroupDetailView (pot, you, member board, deposit, propose, redeem)
→ CreateGroupView / JoinGroupView
→ ProposalDetailView (vote yes/no, status chips)
→ RedeemView (slider, payout proof signer, dust min)
→ Settings/Advanced (explorer links, debug addresses)
MonacoAPIClient methods map 1:1 to M4 routes.
Display formatters unit-tested; no domain math in Swift.
Swift API client (sketch)¶
// API/MonacoAPIClient.swift
final class MonacoAPIClient {
init(baseURL: URL, tokenProvider: () async throws -> String)
func openSession(privyAccessToken: String) async throws -> SessionDTO
func me() async throws -> MeDTO
func createGroup(_ req: CreateGroupRequest) async throws -> GroupDTO
func joinGroup(id: UUID, password: String?) async throws
func groupPortfolio(id: UUID) async throws -> GroupPortfolioDTO
func startDeposit(groupId: UUID, expectedUsdc: Decimal) async throws -> DepositIntentDTO
func depositStatus(id: UUID) async throws -> DepositIntentDTO
func searchCatalog(query: String) async throws -> [CatalogSymbolDTO]
func proposeBuy(groupId: UUID, symbol: String, usdc: Decimal) async throws -> ProposalDTO
func castVote(proposalId: UUID, choice: VoteChoiceDTO) async throws -> ProposalDTO
func homeBoards() async throws -> HomeBoardsDTO
func redeem(_ req: RedeemRequestDTO) async throws -> RedeemResultDTO
}
// DTOs mirror Go json tags — example
struct GroupPortfolioDTO: Codable {
let pot: PotDTO // usdc + positions with mark, after_hours
let you: MemberSliceDTO // equity, pct_return, dollar_pnl
let members: [LeaderboardRowDTO]
}
Red-flag self-screen¶
| Flag | Check |
|---|---|
| Shallow module | Handlers do not coordinate multi-step flows; RedeemService.Complete owns ordering. |
| Information leakage | Jupiter/Privy structs stay in internal/jupiter, internal/privy. HTTP uses DTOs. |
| Temporal decomposition | No validate/persist packages; ledger+governance own their knowledge. |
| Pass-through | No handler → db direct; services add policy (idempotency, NAV mode, proof verify). |
Idempotency keys¶
| Operation | Key |
|---|---|
| Sweep credit | Solana tx signature |
| Jupiter buy/sell | signature or Jupiter execute_request_id |
| Share debit | redeem_requests.id |
| Payout | Solana payout signature |
| Auth wallet provision | privy_user_id |