Comprehensive guidance for AI coding agents working on the MockGatehub project.
MockGatehub is a Go mock implementation of the GateHub API, designed to support local development and testing of wallet applications that integrate with GateHub. It removes the dependency on real credentials and services, enabling fully local development, automated testing without API rate limits, and predictable behavior for CI/CD pipelines.
- API Compliance: MockGatehub must be a drop-in GateHub replacement. Applications expect exact API format compliance.
- Sandbox Parity Only: Focus on happy paths and sandbox behavior. Production GateHub features are out of scope.
- Multi-Currency Required: Support all 11 currencies (XRP, USD, EUR, GBP, ZAR, MXN, SGD, CAD, EGG, PEB, PKR).
- Immutable Vault UUIDs: Vault identifiers are hardcoded in
internal/consts/consts.goand must never change (application databases store these). - Redis Required for Webhooks: The webhook queue always needs Redis, even in in-memory storage mode.
- Language: Go 1.24+
- HTTP Router: chi v5 (lightweight, idiomatic)
- Storage: Dual backend (memory for tests, Redis for runtime)
- Webhook Queue: Redis-backed sorted-set job queue with background worker
- Logging: Zap structured logging
- Testing: testify (unit), godog/Cucumber (BDD E2E)
- Containerization: Docker multi-stage build
- CI/CD: GitHub Actions with semantic-release
mockgatehub/
├── cmd/mockgatehub/ # Application entry point
│ ├── main.go # Server setup, routing, startup
│ └── main_test.go # Route registration tests
├── internal/ # Private application code
│ ├── auth/ # HMAC signature generation/validation
│ │ ├── middleware.go # HTTP auth middleware (public endpoint whitelist)
│ │ ├── middleware_test.go
│ │ ├── signature.go # Signature generation and verification
│ │ └── signature_test.go
│ ├── config/ # Centralized configuration
│ │ └── config.go # Environment variable loading with defaults
│ ├── consts/ # Constants
│ │ └── consts.go # Currencies, vault IDs, rates, statuses, events
│ ├── handler/ # HTTP request handlers
│ │ ├── handler.go # Handler struct, RequestLogger, health check, root/iframe handlers
│ │ ├── handler_test.go # Handler tests
│ │ ├── helpers.go # sendJSON, sendError, setCORSHeaders, decodeJSON
│ │ ├── auth.go # /auth/v1 endpoints (tokens, managed users)
│ │ ├── identity.go # /id/v1 endpoints (KYC flow, iframe)
│ │ ├── core.go # /core/v1 endpoints (wallets, transactions)
│ │ ├── cards.go # /cards/v1 endpoints (full card lifecycle)
│ │ ├── fees.go # Fee configuration and admin endpoints
│ │ ├── fees_test.go # Fee calculation and API tests
│ │ └── rates.go # /rates/v1 endpoints (rates, vaults)
│ ├── logger/ # Logging setup
│ │ └── logger.go # Zap dual-core logger (stdout + stderr)
│ ├── models/ # Domain & API models
│ │ ├── models.go # User, Wallet, Transaction
│ │ ├── api.go # Request/response DTOs
│ │ ├── messages.go # Complex response types (user profile, wallet balance)
│ │ └── cards.go # Card-related models (Customer, Card, Account, etc.)
│ ├── storage/ # Storage layer
│ │ ├── interface.go # Storage contract (~30 methods)
│ │ ├── memory.go # In-memory implementation (sync.RWMutex)
│ │ ├── memory_test.go
│ │ ├── redis.go # Redis implementation (JSON + atomic ops)
│ │ ├── redis_test.go
│ │ └── seeder.go # Pre-seeds test users with balances
│ ├── utils/ # Utilities
│ │ └── utils.go # UUID, mock XRPL address, transaction hash
│ └── webhook/ # Webhook delivery system
│ ├── manager.go # Webhook manager (enqueue, send, sign)
│ ├── manager_test.go
│ ├── queue.go # Redis sorted-set backed job queue
│ ├── worker.go # Background worker (polls every 5s)
│ └── job.go # Job struct and serialization
├── features/ # Gherkin BDD feature files (8 files)
│ ├── auth_user_kyc.feature
│ ├── cards.feature
│ ├── fee_configuration.feature
│ ├── organization_configuration.feature
│ ├── rates_and_vaults.feature
│ ├── service_health.feature
│ ├── signature_authentication.feature
│ ├── transactions.feature
│ └── wallets_and_balances.feature
├── test/integration/ # Go integration tests
│ └── integration_test.go
├── testenv/ # E2E test environment
│ ├── docker-compose.yml # Redis (26380) + MockGatehub (25151)
│ ├── godog_test.go # Godog BDD runner (//go:build e2e)
│ ├── *_steps.go # Step definitions for each feature area
│ ├── helpers.go # Test helpers
│ ├── http_client.go # Signed HTTP client for tests
│ ├── services.go # Docker service management
│ ├── test_context.go # Shared test state
│ ├── types.go # Test constants
│ └── README.md # Test environment documentation
├── web/ # Static web assets
│ ├── index.html # Deposit/withdraw iframe
│ └── kyc-iframe.html # KYC onboarding iframe
├── docs/ # GateHub API reference docs
├── Dockerfile # Multi-stage Docker build
├── Makefile # Build, test, lint, clean targets
├── .releaserc.json # Semantic-release configuration
├── go.mod # Go module (chi, redis, godog, zap, testify, uuid)
├── README.md # User-facing project documentation
└── AGENTS.md # This file
- Dependency Injection: Handler receives storage and webhook manager via constructor
- Interface-Based Storage: Enables swapping memory/Redis without code changes
- Table-Driven Tests: Use testify assertions with comprehensive coverage
- Idiomatic Go: Standard project layout, effective Go patterns
- Minimal Dependencies: chi, redis, uuid, testify, zap, godog
All environment variables are centralized in config.Load():
| Env Var | Default | Notes |
|---|---|---|
MOCKGATEHUB_PORT |
8080 |
HTTP server port |
LOG_LEVEL |
info |
Zap log level |
MOCKGATEHUB_REDIS_URL |
"" |
Enables Redis storage if set |
MOCKGATEHUB_REDIS_DB |
0 |
Redis database number |
WEBHOOK_URL |
"" |
Webhook delivery target |
WEBHOOK_SECRET |
mock-secret |
Webhook HMAC signing secret |
WEBHOOK_MIN_DELAY_SEC |
0.05 → clamped ≥ 2 |
Minimum delay before webhook delivery |
MOCKGATEHUB_ENFORCE_AUTHENTICATION |
true |
Enable/disable HMAC middleware |
MOCKGATEHUB_VALID_CREDENTIALS |
local-test-app-id:local-test-app-secret |
Format: appId:secret,appId2:secret2 |
DEFAULT_ORGANIZATION_ID |
default-org |
Organization ID for callback routing |
Interface (interface.go) defines ~30 methods covering:
- Users: CRUD operations
- Card Customers: CRUD + lookup by source ID
- Card Accounts: CRUD
- Customer Delivery Addresses: create + list
- Cards: CRUD + list by customer/account + limits
- Card Transactions: CRUD + index by card
- Wallets: CRUD + list by user
- Transactions: CRUD + status update
- Balances: get/add/deduct per user per currency
- 3DS Challenges: CRUD + list pending
- Organizations: CRUD for organization configuration
Memory (memory.go): sync.RWMutex for thread safety, maps for all entities.
Redis (redis.go): JSON serialization, INCRBYFLOAT for atomic balance updates. Keys follow patterns like user:{id}, wallet:{address}, balance:{userID}:{currency}.
Seeder (seeder.go): Pre-creates two test users:
- User 1:
00000000-0000-0000-0000-000000000001,testuser1@mockgatehub.local, 10,000 USD, KYCaction_required - User 2:
00000000-0000-0000-0000-000000000002,testuser2@mockgatehub.local, 10,000 EUR, KYCaction_required
Signature Format: HMAC-SHA256(timestamp|method|full_url|body, secret), hex-encoded. Empty segments are stripped.
Webhook Signatures: Different algorithm — HMAC-SHA256(json_body, hex_decoded_secret) via GenerateGateHubWebhookSignature.
Middleware (middleware.go): Applied globally. Skips public endpoints:
/health,/,/iframe/onboarding,/iframe/submit,/transaction/complete,/api/user-currencies,/admin/fees
Validates x-gatehub-app-id, x-gatehub-timestamp, x-gatehub-signature headers. Reconstructs full URL using X-Forwarded-Proto/X-Forwarded-Host for proxy environments.
Architecture: Redis-backed job queue with a background worker and organization-aware callback routing.
- Queue (
queue.go): Redis sorted set (webhooks:queue) + per-job hash (webhooks:job:{id}). Jobs are scored byNotBeforetimestamp. - Worker (
worker.go): Polls every 5 seconds, processes batches of 10 ready jobs. - Manager (
manager.go):SendAsync(eventType, userID, data, offsetDelaySeconds)enqueues jobs.send()performs HTTP delivery with HMAC signing. Supports organization-aware callback routing and 2FA callback test workflows. - Job (
job.go): Tracks ID, event type, user ID, data, attempts, status, timestamps.
Callback Routing: The webhook manager resolves the callback URL per event:
- Looks up the organization config using
DEFAULT_ORGANIZATION_ID - If the organization has an
apiBaseUrl, routes callbacks there - Falls back to the global
WEBHOOK_URLenvironment variable
2FA Callback Test: When an organization config is updated via PATCH /auth/v1/users/organization/{orgID}, a test job is enqueued with a 3-second delay. The worker executes INITIATE and VERIFY callbacks to the organization's apiBaseUrl to verify connectivity.
Retry policy: 10 max attempts, 30-second fixed backoff between retries.
Payload format:
{
"uuid": "...",
"timestamp": "1768920404045",
"event_type": "core.deposit.completed",
"user_uuid": "...",
"environment": "sandbox",
"data": { ... }
}type Handler struct {
store storage.Storage
webhookManager *webhook.Manager
tokenToUser sync.Map // Maps bearer tokens → user UUIDs
feeConfig *FeeConfig // Thread-safe fee percentages
}Constructor: NewHandler(store, webhookManager) — initializes with NewFeeConfig() (0% defaults).
Handler files:
handler.go— Handler struct, RequestLogger, HealthCheck, RootHandler, TransactionCompleteHandler, processDeposit, processWithdrawal, GetUserCurrencieshelpers.go— sendJSON, sendError, setCORSHeaders, sendJSONWithCORS, sendErrorWithCORS, decodeJSONauth.go— CreateToken, CreateManagedUser, GetManagedUser, UpdateManagedUserEmailidentity.go— GetUser, StartKYC, UpdateKYCState, OverrideRiskLevel, KYCIframe, KYCIframeSubmitcore.go— CreateWallet, GetUserWallets, GetWallet, GetWalletBalance, CreateTransaction, GetTransactioncards.go— Full card lifecycle (~20 handlers)fees.go— FeeConfig, GetFees, SetFees, CalculateFeerates.go— GetCurrentRates, GetVaults
Thread-safe FeeConfig with depositFeePercent and withdrawalFeePercent (default 0%). Admin API at GET/PUT /admin/fees for runtime configuration. Validates 0–100 range.
Fee logic: CalculateFee(amount, percent) = round(amount * percent / 100, 2)
- Deposits: net credited = amount - fee
- Withdrawals: total deducted = amount + fee
- Hosted transfers: always 0% fee
POST /core/v1/transactionscreates transaction inpending(status1)- If webhook URL configured: sends pending webhook immediately, then after ~2s goroutine delay marks
completed(status100), credits balance, sends completed webhook - If no webhook URL: completes synchronously
Transaction types: 0=Withdrawal, 1=Deposit, 2=Hosted
Transaction statuses: 1=Pending, 100=Completed, 3=Failed
POST /id/v1/users/{userID}/hubs/{gatewayID}— sets user toaction_requiredGET /iframe/onboarding?bearer={token}[&user_id={uuid}]— servesweb/kyc-iframe.htmlPOST /iframe/submit— parses multipart or URL-encoded form, updates user toaccepted, sendsid.verification.acceptedwebhook with 2s offset delay- Iframe posts
{ type: 'OnboardingCompleted', value: JSON.stringify({ applicantStatus: 'submitted' }) }to parent window
Token → user mapping: /auth/v1/tokens stores bearer → user UUID. The iframe submit handler resolves user_id from this mapping if not provided in the form.
Webhook events: id.verification.accepted, id.verification.rejected, id.verification.action_required, core.deposit.completed, cards.card.created, cards.transaction.event, cards.3ds.auth_3ds_confirmation
Card statuses: Active, Blocked, TemporaryBlocked, Replaced, SoftDelete, AccountBlocked, InCreation
| Location | Type | Description |
|---|---|---|
internal/*/ |
Unit | Package-level tests (handler, auth, storage, webhook, fees) |
test/integration/ |
Integration | Full user journey, API compliance, multi-currency |
testenv/ |
E2E (BDD) | Godog/Cucumber tests driven by features/*.feature |
# All tests (unit + integration)
make unit-tests
# BDD E2E tests (builds Docker, runs against containers)
make e2e-tests
# Both
make test
# Coverage
make coveragetestenv/docker-compose.yml runs:
- Redis on port
26380 - MockGatehub on port
25151(built from Dockerfile, auth enforced)
Tests use //go:build e2e tag. Each feature area has a *_steps.go file implementing step definitions.
service_health.feature— health endpointauth_user_kyc.feature— user creation, KYC flowwallets_and_balances.feature— wallet provisioningtransactions.feature— deposits, hosted transfers, formattingrates_and_vaults.feature— exchange rates, vault listingcards.feature— full card lifecycle (~17 scenarios)signature_authentication.feature— HMAC validation (~12 scenarios)fee_configuration.feature— fee setup and application (~16 scenarios)organization_configuration.feature— organization config update (~6 scenarios)
PR Validation (pr-validation.yml):
- Validates PR title (Conventional Commits)
- Runs unit and E2E tests
- Docker build (no push) with GHA cache
Release (release.yml), runs on push to main:
- All tests (unit + E2E)
- semantic-release to determine version and create tag
- Docker build and push to
ghcr.io/interledger/mockgatehub(linux/amd64 + linux/arm64)
feat→ minor releasefix,perf,docs,refactor,build,ci→ patch releasechore,test→ no release- Tag format:
v${version} - Config:
.releaserc.json
go mod tidy
go test ./...
go build ./cmd/mockgatehubdocker build -t local-mockgatehub .
make e2e-testsIt's safe to log sensitive values in MockGatehub — this is a development/testing mock, not a production system.
Use zap structured logging:
logger.Info("deposit created",
zap.String("user_id", userID),
zap.String("amount", amountStr),
zap.String("currency", currency),
)Log levels:
Info— normal operations (user created, deposit received)Warn— non-fatal issues (invalid input, fallback behavior)Error— failed operations (database error, webhook failure)Debug— detailed diagnostics (token resolution, balance calculations)
- Define models in
internal/models/api.goorinternal/models/cards.go - Implement handler in
internal/handler/{domain}.go - Register route in
cmd/mockgatehub/main.gosetupRoutes() - Write unit tests in
internal/handler/{domain}_test.go - Add BDD scenarios in
features/{domain}.featureand step definitions intestenv/{domain}_steps.go - Update
README.mdAPI section
- Update
internal/storage/interface.go - Implement in both
memory.goANDredis.go - Add tests covering new functionality
- Update seeder if affecting test user creation
- Update
internal/consts/consts.go - NEVER change existing vault UUIDs
- Update all test files with hardcoded values
- Update README.md tables
- All tests pass:
make test - Coverage acceptable:
make coverage(aim for ≥80%) - Docker build succeeds:
docker build -t local-mockgatehub . - Application works with MockGatehub
Must review before coding:
internal/consts/consts.go— all constantsinternal/storage/interface.go— storage contractinternal/models/models.go— domain modelscmd/mockgatehub/main.go— routing configuration
Frequently modified:
internal/handler/*.go— API endpoint implementationsinternal/storage/memory.go— in-memory storage logicinternal/webhook/manager.go— webhook delivery
Tests failing with Redis: Ensure Redis is running and DB is clean (redis-cli -n 1 FLUSHDB)
Docker build fails: Verify go.mod and go.sum are present, check for syntax errors (go build ./...)
Webhooks not arriving: Check WEBHOOK_URL, verify app backend is running, check Docker logs
E2E tests fail: Ensure Docker is running, port 25151 and 26380 are free
When MockGatehub runs behind the Interledger App stack:
# 1) Find Kratos identity ID by email
docker compose -f local/docker-compose.yaml exec -T postgres \
psql -U postgres -d kratos -c \
"SELECT i.id FROM identities i \
JOIN identity_credentials ic ON ic.identity_id = i.id \
JOIN identity_credential_identifiers ici ON ici.identity_credential_id = ic.id \
WHERE ici.identifier='716461-sender-p2p@example.com';"
# 2) Find wallet_id using identity ID
docker compose -f local/docker-compose.yaml exec -T postgres \
psql -U postgres -d backend -c \
"SELECT wallet_id FROM user_wallets WHERE user_id='IDENTITY_ID';"
# 3) Find GateHub wallet address (provider_id)
docker compose -f local/docker-compose.yaml exec -T postgres \
psql -U postgres -d backend -c \
"SELECT id, provider_id, send_currency, receive_currency \
FROM linked_accounts WHERE wallet_id='WALLET_ID' AND provider='gatehub';"
# 4) Query MockGatehub balances
curl -sk https://mockgatehub.interledger.test/core/v1/wallets/PROVIDER_ID/balances | jq- ALWAYS run all tests after changes:
make test - Maintain API compatibility: Applications rely on exact GateHub response format
- Never modify vault UUIDs or currency codes: These are immutable
- Test both storage backends: Changes must work with memory AND Redis
- Update AGENTS.md if you discover outdated guidance: Keep instructions fresh
- Use zap structured logging: Always include context fields for debugging
- Webhook queue requires Redis: Even in in-memory storage mode
Last Updated: February 2026 Maintainers: Interledger Foundation