Checklist for any backend change that moves or records money, including ledgers, shares, swaps, transfers and balances. Use before writing or reviewing code that touches USDC, token amounts, share math, the relayer or Privy signing.
Instal·la
npx skillscat add munyon-canyon/monaco/money-change Instal·la des del registre de SkillsCat.
SKILL.md
Money change
Money bugs are silent and permanent. Every item below is required. A PR that touches money says in its body how it meets each one.
Types
- USDC amounts are
money.Micros. Signed deltas and P&L aremoney.SignedMicros. Token amounts aremoney.BaseUnits, which carry their decimals. All three live inapps/backend/internal/platform/money/money.go. - Build them with
money.ParseMicros,money.MicrosFromUint64ormoney.NewBaseUnitsat the boundary. Arithmetic goes throughAdd,SubandDelta, which return an error on overflow and underflow. Handle the error. Never unwrap touint64to do math. - Share math is
money.MulDiv(a, b, c). It multiplies before it divides throughmath/bigand rounds down, so rounding favors the pot. Never divide first. - Floats are banned in money code by
forbidigoinapps/backend/.golangci.yml. Display formatting is the client's job. - Chain values use the types in
apps/backend/internal/platform/chain/chain.go:chain.SolanaAddress,chain.Signatureandchain.Mint.
The write
- One transaction. Every balance change happens inside
uow.Do(db.UnitOfWork.Doinapps/backend/internal/platform/db/uow.go). Nothing else opens or commits a transaction, andforbidigofailsBeginorCommitoutsideapps/backend/internal/platform/db. - Event in the same transaction. Append the event with
tx.Events.Appendinside the sameuow.Doas the ledger write, asapps/backend/internal/modules/system/app/record_ping.godoes. A consumer that reacts to money reads the event, never the other module's tables. - Guarded update. Every state or balance change is an
UPDATE ... WHERE id = $1 AND <expected state>returning the row count (:execrowsin sqlc). Zero rows means someone else moved it first. Return a conflict or treat it as already done. Never read, check in Go, then write.db.GuardedUpdateinapps/backend/internal/platform/db/guarded.goenforces one row.EchoPinginapps/backend/queries/system/pings.sqlis the guarded pattern. - Idempotent. Commands carry an idempotency key. Consumers are deduped by
event_deliveries(see thenats-consumerskill), but a handler must still converge when it runs twice. - No outbound call inside the transaction. Sign, quote and broadcast happen outside
uow.Do. Record intent before the call and the result after it, each in its own transaction.
On-chain transfers
The relayer package is apps/backend/internal/platform/chain/relayer.
- Build and sign.
relayer.NewTransfers(r, signer).Build(ctx, spec)returns arelayer.SignedTx. The member or treasury wallet signs through Privy (apps/backend/internal/platform/chain/privy/sign.go), then the relayer co-signs as fee payer.SignedTx.Signatureis final before anything is sent. - Store the signature before broadcast. Write
SignedTx.SignatureandLastValidBlockHeightto the row in auow.Dothat commits before you callBroadcast. After a crash, the stored signature is how recovery finds the transaction on chain instead of sending it twice. - Broadcast.
Transfers.Broadcast(ctx, signed)sends it and fails when the RPC reports a different signature. - Confirm. A later step confirms by the stored signature and moves the row with a guarded update.
- Relayer floor. The relayer must hold more than
relayer.FloorLamports(0.001 SOL).Relayer.CheckFloorreturnsrelayer_underfundedat or under it, andrelayer.CheckBootfails api and worker boot in staging and production. Never lower the floor to get a test green.
Logs
- Inside
uow.Do, log atDebugonly. The transaction can roll back and retry, so anInfoline there can describe money that never moved.db.UnitOfWork.Doitself logstx.committedortx.rolled_backatInfo. No lint enforces this rule yet, so check it in review. - After the commit, log the change once with before and after attributes: the balance or state before, the balance or state after, the asset and the owning ID. Register the message in the area's
msgs_<area>.gounderapps/backend/internal/platform/observability.apps/backend/internal/platform/observability/registry_test.gofails an unregistered message. - Never log a private key, a Privy token or a raw signed transaction. The handler in
apps/backend/internal/platform/observability/redact.gomasks attrs named likekey,token,signatureorsecret, and long base58 and base64 values. Keep attr names honest so redaction applies.apps/backend/internal/platform/chain/relayer/redaction_test.goproves no key, token or signature reaches a log line. Add a case there when you add a signing path.
Tests
- Property test. Use
pgregory.net/rapid, asapps/backend/internal/platform/money/property_test.godoes. State the invariant: entries sum to zero per asset, shares never mint value, a round trip returns the input, rounding never favors the member. - Crash-point test. Name the fault point in the flow's
outcomescell ascrash:<point>, register a new point inapps/backend/internal/platform/faultpoint/faultpoint.go, and hit it withfaultpoint.Hitwhere the process could die. Test it withtestkit.CrashAtfromapps/backend/internal/testkit/crash.go, or with a flow script likeF00RecordPingCrashAfterPublishinapps/backend/internal/testkit/flows/f00.go. The run crashes at the point, restarts, and must reach the same end state with no double spend. Crash tests carry//go:build faultpoints. - Guarded-update test. Run the same transition twice and assert the second changes zero rows and moves no money.
- Failure outcomes. Each
errscode the flow can return is an outcome in the flow's file,packages/flows/backend/<id>.tsv, with its ownTestFlow<id>_<Command>_<Outcome>test. - Ledger check. Ledger rows go through
treasury'sapp.Ledger(PostCabalTxn,PostUserTxn,SetStatus), and tests seed a ledger withtestkit.NewLedger. A module that owns a ledger exportsLedgerCheck(config.Config) replay.LedgerCheckfrom its package and reads every mint from that configuration, never a constant.scripts/gen-registryregisters it, somonacoctl replay --verifyandmonacoctl verifyboth run it. - Money code gets 100% statement coverage.
apps/backend/coverage.excludelists no money package.
Checklist
- Amounts are
Micros,SignedMicrosorBaseUnits, with every arithmetic error handled. - Share math uses
money.MulDivand rounds in favor of the pot. - Each balance change and its event commit in one
uow.Do. - Each state change is a guarded update.
- No outbound call inside a transaction.
- A transfer's signature is stored before broadcast.
- Only
Debuglogs insideuow.Do. One post-commit log line has before and after attrs. - No secret, key or signed transaction reaches a log.
- A property test and a crash-point test cover the change.