Building a Self-Contained Payment Logging System: Divorcing Tenant Infrastructure from Agent Infrastructure

Problem Statement

A property management operation was mixing tenant-facing infrastructure with agent-facing infrastructure, creating security and operational concerns. Tenants received credential emails from an agent's domain (queenofsandiego.com) instead of the property management company domain (dangerouscentaur.com). Additionally, payment logging required manual chat-based entry with no audit trail or integration with the tenant portal. The requirement: build a completely self-contained payment logging system within the dangerouscentaur.com domain that allows payments (specifically Zelle transfers) to be logged by simply forwarding bank emails to an automated system.

Architecture Overview

The solution involved three primary components:

  • Email Ingestion Layer: SES-verified dangerouscentaur.com domain with ImprovMX forwarding rules
  • Command Processing Layer: Google Apps Script (GAS) email command handler in the queenofsandiego.com project that parses forwarded Zelle emails
  • Payment Logging Layer: New Lambda function endpoint (log_payment admin action) within the existing receipt-action Lambda
  • Persistence Layer: Shared receipts.json stored in S3 bucket dc-sites-uploads

Infrastructure Changes

SES Domain Verification

Set up full SES verification for dangerouscentaur.com to enable production email sending:

# Initiated SES domain identity for dangerouscentaur.com
# Retrieved DKIM tokens via AWS CLI
# Added DKIM records to Namecheap DNS

aws ses verify-domain-identity \
  --domain dangerouscentaur.com \
  --region us-west-2

The DKIM token triplets were added as CNAME records in Namecheap's DNS management interface. This provides cryptographic proof to major email providers (Gmail, Outlook, etc.) that dangerouscentaur.com is a legitimate mail origin.

ImprovMX Email Alias Setup

Created a dedicated inbox alias under the dangerouscentaur.com domain using ImprovMX (which provides free email forwarding for custom domains). The alias payments@dangerouscentaur.com was configured to forward all incoming mail to the GAS webhook endpoint. This creates a clean, on-brand email address that tenants use to send payment confirmations.

Payment Logging Workflow

Step 1: Email Credential Distribution

Updated /repos/sites/dangerouscentaur/demos/3028fiftyfirststreet.92105.dangerouscentaur.com/index.html with freshly generated tenant credentials (temporary passwords with bcrypt hashes). Deployed the updated portal to S3 bucket 3028fiftyfirststreet-92105-dangerouscentaur-com, then invalidated the CloudFront distribution cache (distribution ID: E1EXAMPLE) to force immediate propagation.

Sent credential emails via AWS SES to both tenants using the noreply@dangerouscentaur.com sender address, clearly marked as coming from the property management company, not the agent.

Step 2: Payment Email Forwarding

When a tenant makes a Zelle payment, they receive a confirmation email from their bank. Instead of manually logging this in chat, they simply forward the bank email to payments@dangerouscentaur.com. The ImprovMX service receives this email and makes an HTTP POST request to a Google Apps Script webhook.

Step 3: GAS Command Parsing

The GAS script (file: /repos/sites/queenofsandiego.com/WarmLeadResponder.gs) includes a doPost(e) function that receives inbound emails from ImprovMX. The handler:

  • Parses the raw email content (headers and body)
  • Extracts key metadata: sender, timestamp, subject, and body text
  • Uses regex pattern matching to identify Zelle payment confirmations (looking for patterns like "You sent $X.XX to [tenant name]" or similar bank-specific formats)
  • Constructs a JSON payload with the payment details and tenant identifier
  • Makes an authenticated HTTPS request to the log_payment Lambda endpoint
// Simplified GAS handler (actual implementation includes full Zelle parsing)
function doPost(e) {
  const emailData = parseEmailFromImprovMX(e.postData.contents);
  const paymentInfo = extractZellePaymentInfo(emailData.body);
  
  if (paymentInfo) {
    callLogPaymentLambda(paymentInfo, getAdminToken());
  }
  
  return HtmlService.createHtmlOutput("OK");
}

Step 4: Lambda Payment Logging

The receipt-action Lambda function (deployed to AWS Lambda) includes a new admin action handler for log_payment. This function:

  • Validates the ADMIN_TOKEN from the request header (stored as Lambda environment variable)
  • Parses the payment JSON (tenant ID, amount, payment method, timestamp)
  • Reads the current receipts.json from S3 bucket dc-sites-uploads at key receipts.json
  • Appends a new payment record with full metadata (bank confirmation details, forwarded email address, parsing timestamp)
  • Writes the updated receipts.json back to S3 with versioning enabled
  • Returns a success response with the payment ID for audit purposes
def lambda_handler(event, context):
    # Extract admin token from headers
    token = event.get('headers', {}).get('X-Admin-Token', '')
    
    if token != os.environ['ADMIN_TOKEN']:
        return {
            'statusCode': 403,
            'body': json.dumps({'error': 'Unauthorized'})
        }
    
    # Parse payment from body
    body = json.loads(event.get('body', '{}'))
    payment_record = {
        'tenant_id': body['tenant_id'],
        'amount': body['amount'],
        'method': 'zelle',
        'email_forwarded_from': body['email_from'],
        'logged_at': datetime.now().isoformat(),
        'source': 'email_forward'
    }
    
    # Read, update, write receipts.json
    receipts = read_receipts_from_s3()
    receipts['payments'].append(payment_record)
    write_receipts_to_s3(receipts)
    
    return {
        'statusCode': 200,
        'body': json.dumps({'payment_id': generate_id()})
    }

Key Design Decisions

Why use ImprovMX instead of direct SES receiving? ImprovMX provides free email forwarding with webhook capabilities, eliminating the need to manage SES receipt rule sets, S3 storage of raw emails, and SNS-to-Lambda triggers. This reduces operational overhead and keeps the architecture simpler.

Why authenticate GAS-to-Lambda with a token instead of IAM? The GAS service runs outside AWS infrastructure, so AWS SigV4 authentication would be complex. A shared admin token (