Building a Deterministic Order Notification & GAAP Ledger System for Stripe Checkouts
What Was Done
Adam Cherry Comics operates via a Stripe checkout Lambda (`adam-cherry-checkout`) that was silently eating order data — customers bought comics, payment succeeded, but Adam received no notification and had no record of what was owed. We built a deterministic, fully automated reconciliation system that:
- Verifies incoming Stripe webhook signatures and sends order emails to Adam + archival copy to business email
- Writes every transaction to a DynamoDB double-entry ledger with GAAP accounting principles
- Automatically calculates splits (Stripe fees, your 6% cut, balance due Adam)
- Generates monthly consignment statements on a schedule
- Backfilled all historical orders with balanced journal entries
- Requires zero manual intervention after initial setup — you get visibility but never become a gate
Architecture & Technical Details
Order Notification Flow
The Lambda at `/Users/cb/ac-ops/lambda/checkout.py` now serves both the checkout endpoint and the Stripe webhook handler. On webhook arrival:
POST /checkout (from Stripe)
↓
Verify X-Stripe-Signature header (HMAC-SHA256)
↓
Extract session ID, total amount, customer email/address
↓
Send order email via SES to Adam + CB business email
↓
Post four double-entry journal lines to DynamoDB
↓
Return 200 to Stripe (idempotent on retry)
Signature verification uses Stripe's signing secret (stored in the Lambda's environment). This prevents accidental processing of crafted webhooks. The implementation reads the raw request body (not parsed JSON) to compute the HMAC — critical, because Stripe's signature is computed on the exact byte sequence.
Double-Entry Ledger in DynamoDB
The ledger table `acc-adam-ledger` records every transaction as exactly four debit-credit pairs. For a $10 order with $0.59 Stripe fee and your 6% cut ($0.60):
Entry 1: DEBIT Accounts Receivable (Adam's balance) $10.00
CREDIT Revenue — Comic Sales $10.00
Entry 2: DEBIT Fee Expense (Stripe) $0.59
CREDIT Accounts Receivable (Adam) $0.59
Entry 3: DEBIT Fee Expense (Your 6%) $0.60
CREDIT Accounts Receivable (Adam) $0.60
Entry 4: DEBIT Cash $8.81
CREDIT Accounts Receivable (Adam) $8.81
Each entry is keyed by transaction ID + deterministic sequence number, so webhook retries from Stripe don't double-post. The ledger schema includes debit_account, credit_account, amount_cents, transaction_id, ts_unix, and description. Every query sums debits and credits; they must match exactly or the backfill script exits loudly.
Infrastructure & Automation
AWS Resources
- Lambda:
adam-cherry-checkout(us-east-1), 30s timeout, environment variableSTRIPE_WEBHOOK_SECRET - DynamoDB:
acc-adam-ledger, on-demand billing, GSI ontransaction_idfor idempotency checks - SES: Verified sending identity (
dangerouscentaur@gmail.com), templates for order + monthly statement - EventBridge: Rule firing on the 1st of each month, invoking the statement Lambda
- IAM: Scoped policy on the Lambda execution role allowing
dynamodb:PutItem,dynamodb:Query, andses:SendEmail
Deployment & Tooling
The workspace at `/Users/cb/ac-ops/` contains:
tools/deploy.sh— builds the Lambda zip, uploads to S3, updates function codetools/setup_webhook.sh— creates the webhook endpoint in API Gateway and configures Stripe (requires Stripe API key)tools/setup_schedule.sh— creates EventBridge rule and grants Lambda permissiontools/test_webhook.sh/test_webhook_remote.py— local signature generation + remote invocation for end-to-end testing
Deployment is single-command: ./tools/deploy.sh. The script is idempotent and safe to run repeatedly.
Key Decisions
Why Double-Entry Bookkeeping?
A single "balance" field is easy to lose track of. Double-entry forces you to explain where every penny came from and where it went. Your bank statement reconciles against the ledger; Adam's monthly statement is generated directly from the same data. No separate "accounts" or "memo" fields needed — the structure is the truth.
Why Not SNS/SQS?
Stripe webhooks are synchronous expects a 2xx response within seconds. If you introduce a queue, Stripe retries on timeout even though the work will eventually succeed. We verify the signature upfront and write to DynamoDB (single-region, <1ms latency). If SES is slow, we return 200 anyway and retry the email asynchronously — the order is safe.
Why EventBridge Instead of Lambda Polling?
EventBridge is event-driven and serverless. A cron rule fires exactly once per month, invoking a statement-generator Lambda. No EC2 polling loop, no risk of double-sends from concurrent invocations — EventBridge guarantees delivery and deduplication.
Why Stripe Connect?
Until now, you hold all money and manually transfer Adam's share. Stripe Connect lets Adam's Express account receive payments directly while you retain a portion for fees. The ledger already tracks what he's owed; the Connect account is just the payment rail. Setup requires Adam to click through Stripe's onboarding URL (one-time), after which payouts are fully automated.
Verification & Testing
The system was tested end-to-end:
- Generated a valid Stripe webhook signature locally (test data)
- Sent it to the API Gateway endpoint, verified order email arrived in Adam's inbox
- Queried the DynamoDB table, confirmed four balanced journal entries
- Ran the statement generator, verified monthly statement email was sent
- Backfilled all historical orders (4 total), each booked correctly with zero balance mismatches
All operations are idempotent — replaying the same webhook or statement date produces the same output ledger, never duplicates.
What's Next
- Stripe Connect Express Account: One-time manual signup via Stripe dashboard. Once approved, I generate an onboarding email draft that you review and Adam clicks.
- Payout Automation: Once Adam's Connect account is live, a Lambda function will use the ledger to calculate monthly payouts and initiate transfers automatically.
- Monitoring: Add CloudWatch alarms on Lambda errors and ledger reconciliation mismatches (debit ≠ credit).
- Audit Trail: Optionally store failed webhook attempts in S3 for compliance.
The system is live now. Every order fires the webhook, Adam gets notified within seconds, and the ledger posts automatically — no human in the loop.