{"id":50189574,"url":"https://github.com/puneethkumarck/stablepay-hackathon","last_synced_at":"2026-05-25T12:03:56.743Z","repository":{"id":351942383,"uuid":"1200456533","full_name":"Puneethkumarck/stablepay-hackathon","owner":"Puneethkumarck","description":"Cross-border USD→INR remittance on Solana — Colosseum Frontier Hackathon","archived":false,"fork":false,"pushed_at":"2026-05-18T16:18:22.000Z","size":453569,"stargazers_count":0,"open_issues_count":20,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-18T18:24:47.197Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Puneethkumarck.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"docs/ROADMAP.md","authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-03T12:41:07.000Z","updated_at":"2026-04-26T15:52:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/Puneethkumarck/stablepay-hackathon","commit_stats":null,"previous_names":["puneethkumarck/stablepay-hackathon"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/Puneethkumarck/stablepay-hackathon","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fstablepay-hackathon","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fstablepay-hackathon/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fstablepay-hackathon/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fstablepay-hackathon/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Puneethkumarck","download_url":"https://codeload.github.com/Puneethkumarck/stablepay-hackathon/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Puneethkumarck%2Fstablepay-hackathon/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33473712,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-25T06:32:55.349Z","status":"ssl_error","status_checked_at":"2026-05-25T06:32:35.322Z","response_time":57,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-05-25T12:03:48.071Z","updated_at":"2026-05-25T12:03:56.736Z","avatar_url":"https://github.com/Puneethkumarck.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# StablePay\n\n[![CI](https://github.com/Puneethkumarck/stablepay-hackathon/actions/workflows/ci.yml/badge.svg)](https://github.com/Puneethkumarck/stablepay-hackathon/actions/workflows/ci.yml)\n[![Java](https://img.shields.io/badge/Java-25-orange)](https://jdk.java.net/25/)\n[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-4.0.5-6DB33F?logo=springboot\u0026logoColor=white)](https://spring.io/projects/spring-boot)\n[![Stripe](https://img.shields.io/badge/Stripe-On--Ramp-635BFF?logo=stripe\u0026logoColor=white)](https://stripe.com/)\n[![Solana](https://img.shields.io/badge/Solana-devnet-9945FF?logo=solana\u0026logoColor=white)](https://solana.com/)\n[![Anchor](https://img.shields.io/badge/Anchor-0.32.1-blue)](https://www.anchor-lang.com/)\n[![Go](https://img.shields.io/badge/Go-1.26-00ADD8?logo=go\u0026logoColor=white)](https://go.dev/)\n\n\u003e **Instant cross-border remittances on Solana. No seed phrases. No app for recipients. Guaranteed delivery.**\n\n![StablePay — Instant cross-border remittances on Solana](docs/images/hero-banner.png)\n\nStablePay is a consumer-facing remittance application for the **USD → INR** corridor, built on USDC/Solana. It combines MPC wallet abstraction, a custom Anchor escrow program, and Temporal durable workflows to deliver a seamless sender-to-recipient experience — the recipient claims funds via an SMS link, no crypto knowledge required.\n\n\u003e Built for the [Colosseum Frontier Hackathon](https://www.colosseum.org/) (April 6 – May 11, 2026)\n\n---\n\n## The Problem We're Solving\n\n![Traditional vs StablePay — Cross-border payment comparison](docs/images/stablepay-vs-traditional-remittance.png)\n\n**Verified on-chain:** Each remittance costs **$0.002** total (3 transactions × 0.000005 SOL). E2E tested with 10/10 customers completing in 22–60 seconds.\n\n---\n\n## Architecture Overview\n\n![StablePay Platform Architecture](docs/images/platform-architecture.png)\n\n```mermaid\ngraph TB\n    subgraph \"Application Layer\"\n        A[REST API — Spring MVC]\n        A1[\"/api/wallets\"]\n        A2[\"/api/remittances\"]\n        A3[\"/api/fx\"]\n        A4[\"/api/claims\"]\n        A5[\"/api/funding-orders\"]\n        A6[\"/webhooks/stripe\"]\n        A7[\"/api/auth\"]\n    end\n\n    subgraph \"Domain Layer — Zero Framework Dependencies\"\n        B[Handlers — Use Cases]\n        C[Models — Immutable Records]\n        D[Ports — Interfaces]\n        E[Exceptions — SP-XXXX Codes]\n    end\n\n    subgraph \"Infrastructure Adapters\"\n        F[JPA + PostgreSQL + Flyway]\n        G[MPC gRPC Client]\n        H[Solana RPC — sol4k]\n        I[ExchangeRate-API + Redis Cache]\n        J[Temporal Workflows]\n        K[Twilio SMS]\n        L[Razorpay UPI Disbursement]\n        S[Stripe Payments]\n        T[Google OAuth + JWT Auth]\n    end\n\n    subgraph \"External Systems\"\n        M[MPC Sidecar x2 — Go + tss-lib]\n        N[Solana Devnet — Anchor Escrow]\n        O[open.er-api.com]\n        P[Twilio API]\n        Q[Razorpay API]\n        R[Stripe API]\n    end\n\n    A --\u003e B\n    B --\u003e D\n    D --\u003e F\n    D --\u003e G\n    D --\u003e H\n    D --\u003e I\n    D --\u003e J\n    D --\u003e K\n    D --\u003e L\n    D --\u003e S\n    D --\u003e T\n    G --\u003e M\n    H --\u003e N\n    I --\u003e O\n    K --\u003e P\n    L --\u003e Q\n    S --\u003e R\n```\n\n**Dependency rule:** `domain` → nothing. `application` → `domain`. `infrastructure` → `domain`. Never the reverse.\n\n---\n\n## The Payment Lifecycle: Step by Step\n\n![StablePay Payment Lifecycle](docs/images/payment-lifecycle.png)\n\n```mermaid\nsequenceDiagram\n    participant Sender as Sender (Mobile/API)\n    participant API as StablePay API\n    participant Stripe as Stripe\n    participant MPC as MPC Sidecar x2\n    participant DB as PostgreSQL\n    participant Temporal as Temporal Workflow\n    participant Solana as Solana Devnet\n    participant SMS as Twilio SMS\n    participant Recipient as Recipient (Web)\n    participant Razorpay as Razorpay UPI\n\n    Note over Sender,API: Use Case 0 — Social Login (Google)\n    Sender-\u003e\u003eAPI: POST /api/auth/social {provider: \"GOOGLE\", idToken}\n    API-\u003e\u003eAPI: Verify Google ID token (JWKS)\n    API-\u003e\u003eDB: Upsert user + social_identity\n    API-\u003e\u003eMPC: gRPC GenerateKey (DKG ceremony — first login only)\n    MPC--\u003e\u003eAPI: solanaAddress + publicKey + keyShareData\n    API-\u003e\u003eDB: INSERT wallet (first login only)\n    API--\u003e\u003eSender: 201/200 {accessToken, refreshToken, user, wallet}\n\n    Note over Sender,Stripe: Use Case 1 — Fund Wallet (Stripe On-Ramp)\n    Sender-\u003e\u003eAPI: POST /api/wallets/{id}/fund {amount} + Bearer token\n    API-\u003e\u003eStripe: Create PaymentIntent\n    Stripe--\u003e\u003eAPI: clientSecret + paymentIntentId\n    API-\u003e\u003eDB: INSERT funding_order (PAYMENT_CONFIRMED)\n    API--\u003e\u003eSender: 201 {fundingId, clientSecret}\n    Stripe-\u003e\u003eAPI: Webhook: payment_intent.succeeded\n    API-\u003e\u003eTemporal: Start WalletFundingWorkflow\n    Temporal-\u003e\u003eSolana: Transfer SOL (rent) + create ATA + transfer USDC\n    Temporal-\u003e\u003eDB: UPDATE funding_order → FUNDED\n\n    Note over Sender,API: Use Case 2 — Get FX Rate\n    Sender-\u003e\u003eAPI: GET /api/fx/USD-INR\n    API-\u003e\u003eAPI: Check Redis cache\n    API--\u003e\u003eSender: {rate: 84.50, source, expiresAt}\n\n    Note over Sender,Razorpay: Use Case 3 — Send Remittance\n    Sender-\u003e\u003eAPI: POST /api/remittances {phone, amount} + Bearer token\n    API-\u003e\u003eDB: Reserve sender balance\n    API-\u003e\u003eAPI: Lock FX rate, calculate INR\n    API-\u003e\u003eDB: INSERT remittance (INITIATED)\n    API-\u003e\u003eDB: INSERT claim_token (48h expiry)\n    API-\u003e\u003eTemporal: Start RemittanceLifecycleWorkflow\n    API--\u003e\u003eSender: 201 {remittanceId, status: INITIATED}\n\n    Note over Temporal,Solana: Workflow Phase 1 — Escrow\n    Temporal-\u003e\u003eMPC: Sign escrow deposit transaction\n    MPC--\u003e\u003eTemporal: Ed25519 signature\n    Temporal-\u003e\u003eSolana: Submit deposit (USDC → PDA vault)\n    loop Poll getSignatureStatuses (3s interval, max 40 attempts)\n        Temporal-\u003e\u003eSolana: Check confirmation status\n        Solana--\u003e\u003eTemporal: PROCESSED / CONFIRMED / FINALIZED\n    end\n    Temporal-\u003e\u003eDB: UPDATE status → ESCROWED\n\n    Note over Temporal,SMS: Workflow Phase 2 — Notify\n    Temporal-\u003e\u003eSMS: Send claim link via SMS\n    SMS--\u003e\u003eRecipient: \"Claim your funds: https://...\"\n\n    Note over Temporal,Recipient: Workflow Phase 3 — Wait\n    Temporal-\u003e\u003eTemporal: Await claim signal (48h timeout)\n\n    Note over Recipient,Razorpay: Use Case 4 — Claim Funds\n    Recipient-\u003e\u003eAPI: GET /api/claims/{token}\n    API--\u003e\u003eRecipient: {amountUsdc, amountInr, fxRate}\n    Recipient-\u003e\u003eAPI: POST /api/claims/{token} {upiId}\n    API-\u003e\u003eDB: UPDATE claim_token (claimed=true, upiId)\n    API-\u003e\u003eTemporal: Signal claimSubmitted(upiId)\n\n    Note over Temporal,Razorpay: Workflow Phase 4 — Deliver\n    Temporal-\u003e\u003eSolana: Release escrow to recipient\n    loop Poll getSignatureStatuses (3s interval, max 40 attempts)\n        Temporal-\u003e\u003eSolana: Check confirmation status\n    end\n    Temporal-\u003e\u003eDB: UPDATE status → CLAIMED\n    Temporal-\u003e\u003eRazorpay: Disburse INR to UPI\n    Temporal-\u003e\u003eDB: UPDATE status → DELIVERED\n```\n\n---\n\n## Use Case 0: Social Login + Wallet Creation\n\n\u003e **Google sign-in, instant wallet.** On first login the backend verifies the Google ID token, creates a user record, and runs a 2-of-2 MPC DKG ceremony to produce an Ed25519 Solana wallet. No seed phrases. Returning users get their existing wallet.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Controller as AuthController\n    participant Handler as SocialLoginHandler\n    participant Google as GoogleIdTokenVerifier\n    participant UserRepo as UserRepository\n    participant WalletHandler as CreateWalletHandler\n    participant MPC as MpcWalletGrpcClient\n    participant Sidecar0 as MPC Sidecar 0\n    participant Sidecar1 as MPC Sidecar 1\n    participant JWT as JwtTokenIssuer\n\n    Client-\u003e\u003eController: POST /api/auth/social {provider: \"GOOGLE\", idToken}\n    Controller-\u003e\u003eHandler: handle(\"GOOGLE\", idToken, ip, userAgent)\n    Handler-\u003e\u003eGoogle: verify(idToken)\n    Google--\u003e\u003eHandler: {sub, email, email_verified}\n\n    alt New user\n        Handler-\u003e\u003eUserRepo: save(User{id: UUID, email})\n        Handler-\u003e\u003eWalletHandler: handle(userId)\n        WalletHandler-\u003e\u003eMPC: generateKey()\n        MPC-\u003e\u003eSidecar0: gRPC GenerateKey (ceremonyId, threshold=1, parties=2)\n        Sidecar0-\u003e\u003eSidecar1: P2P DKG round messages (port 7000↔7001)\n        Sidecar1-\u003e\u003eSidecar0: P2P DKG round messages\n        Note over Sidecar0,Sidecar1: Ed25519 DKG ceremony completes\n        Sidecar0--\u003e\u003eMPC: {solanaAddress, publicKey, keyShareData}\n        MPC--\u003e\u003eWalletHandler: GeneratedKey\n    else Returning user\n        Handler-\u003e\u003eUserRepo: findBySocialIdentity(provider, sub)\n        UserRepo--\u003e\u003eHandler: existing User + Wallet\n    end\n\n    Handler-\u003e\u003eJWT: issue(userId)\n    JWT--\u003e\u003eHandler: accessToken + refreshToken\n    Handler--\u003e\u003eController: LoginResult\n    Controller--\u003e\u003eClient: 201 Created (new) / 200 OK (returning)\n```\n\n```\nPOST /api/auth/social\nContent-Type: application/json\n\n{ \"provider\": \"GOOGLE\", \"idToken\": \"\u003cgoogle-id-token\u003e\" }\n```\n\n```json\n{\n  \"accessToken\": \"\u003cjwt\u003e\",\n  \"refreshToken\": \"\u003copaque-token\u003e\",\n  \"tokenType\": \"Bearer\",\n  \"expiresIn\": 900,\n  \"user\": {\n    \"id\": \"7d4718ba-a6f3-485c-b89b-77afa2caf206\",\n    \"email\": \"user@gmail.com\"\n  },\n  \"wallet\": {\n    \"id\": 16,\n    \"solanaAddress\": \"CrsMdkbkAQRz7srMgeTe9sanoiHkeQBCKnhhVR9DAd18\",\n    \"availableBalance\": 0,\n    \"totalBalance\": 0,\n    \"createdAt\": \"2026-04-22T06:52:59.379393834Z\",\n    \"updatedAt\": \"2026-04-22T06:52:59.379393834Z\"\n  }\n}\n```\n\n### Additional Auth Endpoints\n\n| Endpoint | Auth | Description |\n|---|---|---|\n| `POST /api/auth/refresh` | Public | Rotate refresh token, issue new access token |\n| `POST /api/auth/logout` | Bearer JWT | Revoke all refresh tokens (204 No Content) |\n| `GET /api/wallets/me` | Bearer JWT | Get authenticated user's wallet |\n\n### What Happens Inside the MPC Sidecars\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│                   MPC Key Generation (DKG)                       │\n├─────────────────────────────────────────────────────────────────┤\n│                                                                  │\n│  1. Backend sends gRPC GenerateKey to Sidecar 0 (port 50051)    │\n│                                                                  │\n│  2. Sidecar 0 registers ceremony in P2P CeremonyRegistry        │\n│     └─ Creates buffered channel for round messages               │\n│                                                                  │\n│  3. Both sidecars run tss-lib Ed25519 DKG protocol               │\n│     ├─ Round 1: Commitment exchange (P2P port 7000↔7001)         │\n│     ├─ Round 2: Share distribution                               │\n│     └─ Round 3: Key derivation                                   │\n│                                                                  │\n│  4. Result: Both parties hold a key share                        │\n│     ├─ Neither party has the full private key                    │\n│     ├─ Public key (Ed25519) derived cooperatively                │\n│     └─ Solana address = Base58(publicKey)                        │\n│                                                                  │\n│  5. Sidecar 0 returns to backend:                                │\n│     ├─ solanaAddress: Base58-encoded Solana address              │\n│     ├─ publicKey: Raw Ed25519 public key bytes                   │\n│     └─ keyShareData: Serialized key share (stored in DB)         │\n│                                                                  │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Error Paths\n\n| Condition | Error Code | HTTP |\n|---|---|---|\n| User already has a wallet | SP-0008 | 409 Conflict |\n| MPC ceremony fails | SP-0010 | 500 |\n| gRPC timeout (\u003e30s) | SP-0010 | 500 |\n\n---\n\n## Use Case 1: Fund Wallet (Stripe On-Ramp)\n\n\u003e **Real Stripe integration.** The sender pays via Stripe (card), which triggers a webhook. A Temporal workflow then transfers SOL (for rent/fees), creates an Associated Token Account, and transfers USDC from the treasury to the sender's MPC wallet on-chain.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Controller as FundingController\n    participant Handler as InitiateFundingHandler\n    participant Writer as FundingOrderWriter\n    participant Stripe as StripePaymentAdapter\n    participant DB as PostgreSQL\n    participant Webhook as StripeWebhookController\n    participant Complete as CompleteFundingHandler\n    participant Temporal as WalletFundingWorkflow\n    participant Treasury as TreasuryServiceAdapter\n    participant Solana as Solana Devnet\n\n    Client-\u003e\u003eController: POST /api/wallets/1/fund {amount: 1.00}\n    Controller-\u003e\u003eWriter: persist funding order\n    Writer-\u003e\u003eDB: INSERT funding_order (PAYMENT_CONFIRMED)\n    Controller-\u003e\u003eHandler: initiate funding\n    Handler-\u003e\u003eStripe: Create PaymentIntent ($1.00)\n    Stripe--\u003e\u003eHandler: paymentIntentId + clientSecret\n    Handler--\u003e\u003eClient: 201 {fundingId, clientSecret}\n\n    Note over Stripe,Webhook: Stripe fires webhook\n    Stripe-\u003e\u003eWebhook: POST /webhooks/stripe (payment_intent.succeeded)\n    Webhook-\u003e\u003eComplete: handle(fundingId)\n    Complete-\u003e\u003eTemporal: Start WalletFundingWorkflow\n\n    Note over Temporal,Solana: Temporal orchestrates on-chain funding\n    Temporal-\u003e\u003eTreasury: checkTreasuryBalance(1.00 USDC)\n    Temporal-\u003e\u003eTreasury: ensureSolBalance(senderAddress)\n    Treasury-\u003e\u003eSolana: Transfer SOL for rent + fees\n    Temporal-\u003e\u003eTreasury: createAtaIfNeeded(senderAddress)\n    Treasury-\u003e\u003eSolana: Create Associated Token Account\n    Temporal-\u003e\u003eTreasury: transferUsdc(senderAddress, 1.00)\n    Treasury-\u003e\u003eSolana: SPL token transfer (treasury → sender ATA)\n    Temporal-\u003e\u003eDB: UPDATE funding_order → FUNDED\n```\n\n```\nPOST /api/wallets/1/fund\nContent-Type: application/json\n\n{ \"amount\": 1.00 }\n```\n\n```json\n{\n  \"fundingId\": \"b2c8a29a-fcca-487f-979b-bf355c82faeb\",\n  \"stripePaymentIntentId\": \"pi_3TO0YZ3nnME1dfOB0dz1SICr\",\n  \"stripeClientSecret\": \"pi_...secret_...\",\n  \"walletId\": 1,\n  \"amountUsdc\": 1.00,\n  \"status\": \"PAYMENT_CONFIRMED\"\n}\n```\n\n### Funding Order Status Machine\n\n```\nPAYMENT_CONFIRMED → FUNDED     (webhook + Temporal workflow succeeds)\nPAYMENT_CONFIRMED → FAILED     (webhook: payment_intent.payment_failed)\nFUNDED → REFUND_INITIATED      (manual refund)\nREFUND_INITIATED → REFUNDED    (refund completed)\n```\n\n### Error Paths\n\n| Condition | Error Code | HTTP |\n|---|---|---|\n| Wallet not found | SP-0006 | 404 |\n| Treasury balance insufficient | SP-0007 | 503 |\n| Funding already in progress for wallet | SP-0022 | 409 |\n| Stripe PaymentIntent creation failed | SP-0021 | 502 |\n\n---\n\n## Use Case 2: Get FX Rate\n\n\u003e **Real-time rates with fallback.** FX rates come from ExchangeRate-API with Redis caching (5-minute TTL). If the API is unreachable, a hardcoded fallback rate of 84.50 is used.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Controller as FxRateController\n    participant Handler as GetFxRateQueryHandler\n    participant Adapter as ExchangeRateApiAdapter\n    participant Redis as Redis Cache\n    participant API as open.er-api.com\n\n    Client-\u003e\u003eController: GET /api/fx/USD-INR\n    Controller-\u003e\u003eHandler: handle(\"USD\", \"INR\")\n    Handler-\u003e\u003eAdapter: getRate(\"USD\", \"INR\")\n\n    alt Cache Hit\n        Adapter-\u003e\u003eRedis: GET fxRate::USD\n        Redis--\u003e\u003eAdapter: Cached FxQuote\n    else Cache Miss\n        Adapter-\u003e\u003eAPI: GET /v6/latest/USD\n        API--\u003e\u003eAdapter: {rates: {INR: 84.50, ...}}\n        Adapter-\u003e\u003eRedis: SET fxRate::USD (5m TTL)\n    else API Failure\n        Note over Adapter: Fallback: rate=84.50, source=\"fallback\"\n    end\n\n    Adapter--\u003e\u003eHandler: FxQuote{rate, source, timestamp, expiresAt}\n    Handler--\u003e\u003eController: FxQuote\n    Controller--\u003e\u003eClient: 200 OK\n```\n\n```\nGET /api/fx/USD-INR\n```\n\n```json\n{\n  \"rate\": 84.500000,\n  \"source\": \"open.er-api.com\",\n  \"timestamp\": \"2026-04-13T10:00:00Z\",\n  \"expiresAt\": \"2026-04-13T10:01:00Z\"\n}\n```\n\n### Error Paths\n\n| Condition | Error Code | HTTP |\n|---|---|---|\n| Unsupported corridor (e.g., EUR-INR) | SP-0009 | 400 |\n\n---\n\n## Use Case 3: Send Remittance\n\n\u003e **The core flow.** Reserves the sender's balance, locks the FX rate, generates a claim token, and starts a Temporal durable workflow that orchestrates the entire escrow-to-delivery lifecycle.\n\n```mermaid\nsequenceDiagram\n    participant Client\n    participant Handler as CreateRemittanceHandler\n    participant WalletRepo as WalletRepository\n    participant FxProvider as ExchangeRateApiAdapter\n    participant RemitRepo as RemittanceRepository\n    participant ClaimRepo as ClaimTokenRepository\n    participant Temporal as TemporalWorkflowStarter\n    participant DB as PostgreSQL\n\n    Client-\u003e\u003eHandler: handle(principalId, \"+919876543210\", 100.00)\n\n    Note over Handler,DB: Step 1 — Reserve Balance\n    Handler-\u003e\u003eWalletRepo: findByUserId(principalId)\n    WalletRepo--\u003e\u003eHandler: Wallet{available: 100.00}\n    Handler-\u003e\u003eHandler: wallet.reserveBalance(100.00)\n    Note over Handler: available: 100→0, total: 100 (unchanged)\n    Handler-\u003e\u003eWalletRepo: save(reserved wallet)\n\n    Note over Handler,FxProvider: Step 2 — Lock FX Rate\n    Handler-\u003e\u003eFxProvider: getRate(\"USD\", \"INR\")\n    FxProvider--\u003e\u003eHandler: FxQuote{rate: 84.50}\n    Handler-\u003e\u003eHandler: INR = 100.00 × 84.50 = ₹8,450.00\n\n    Note over Handler,DB: Step 3 — Create Remittance\n    Handler-\u003e\u003eHandler: remittanceId = UUID.randomUUID()\n    Handler-\u003e\u003eRemitRepo: save(Remittance{status: INITIATED})\n    RemitRepo-\u003e\u003eDB: INSERT INTO remittances\n\n    Note over Handler,DB: Step 4 — Generate Claim Token\n    Handler-\u003e\u003eHandler: token = UUID.randomUUID()\n    Handler-\u003e\u003eClaimRepo: save(ClaimToken{expires: now+48h})\n    ClaimRepo-\u003e\u003eDB: INSERT INTO claim_tokens\n    Handler-\u003e\u003eRemitRepo: save(remittance + claimTokenId)\n\n    Note over Handler,Temporal: Step 5 — Start Workflow\n    Handler-\u003e\u003eTemporal: startWorkflow(remittanceId, senderAddr, phone, amount, token)\n    Temporal--\u003e\u003eHandler: Workflow started (async)\n\n    Handler--\u003e\u003eClient: 201 Created\n```\n\n```\nPOST /api/remittances\nAuthorization: Bearer \u003cjwt\u003e\nContent-Type: application/json\n\n{\n  \"recipientPhone\": \"+919876543210\",\n  \"amountUsdc\": 1.00\n}\n```\n\n```json\n{\n  \"id\": 10,\n  \"remittanceId\": \"8ce317d2-639e-4054-bcae-204706dc2c9a\",\n  \"recipientPhone\": \"+919876543210\",\n  \"amountUsdc\": 1.0,\n  \"amountInr\": 93.61,\n  \"fxRate\": 93.605785,\n  \"status\": \"INITIATED\",\n  \"escrowPda\": null,\n  \"claimTokenId\": \"82a56560-ad6f-4b97-a26d-12c36b722f58\",\n  \"smsNotificationFailed\": false,\n  \"createdAt\": \"2026-04-22T06:53:15.632889681Z\",\n  \"updatedAt\": \"2026-04-22T06:53:15.646040707Z\",\n  \"expiresAt\": null\n}\n```\n\n### What Happens After the API Returns\n\nThe Temporal workflow takes over asynchronously. The sender gets an immediate response, and the workflow progresses through the escrow lifecycle in the background.\n\n### Error Paths\n\n| Condition | Error Code | HTTP |\n|---|---|---|\n| Sender wallet not found | SP-0006 | 404 |\n| Insufficient balance | SP-0002 | 400 |\n| Unsupported corridor | SP-0009 | 400 |\n\n---\n\n## Temporal Workflow: The Remittance Lifecycle\n\n\u003e **Guaranteed delivery.** If the process crashes at any point, Temporal resumes exactly where it left off. Every remittance reaches a terminal state — delivered, refunded, or failed.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e INITIATED: POST /api/remittances\n    INITIATED --\u003e ESCROWED: depositEscrow activity succeeds\n\n    ESCROWED --\u003e CLAIMED: Recipient claims within 48h\n    ESCROWED --\u003e REFUNDED: 48h timeout — no claim\n\n    CLAIMED --\u003e DELIVERED: INR disbursement succeeds\n    CLAIMED --\u003e DISBURSEMENT_FAILED: INR disbursement fails\n\n    INITIATED --\u003e FAILED: Deposit escrow fails\n\n    note right of ESCROWED\n        USDC locked in Solana PDA\n        SMS sent to recipient\n        Workflow awaits claim signal\n    end note\n\n    note right of DELIVERED\n        Escrow released on-chain\n        INR sent to UPI\n        Terminal state\n    end note\n\n    note right of REFUNDED\n        USDC returned to sender\n        Escrow closed on-chain\n        Terminal state\n    end note\n\n    note right of DISBURSEMENT_FAILED\n        Escrow released (irreversible)\n        INR payout failed\n        Requires manual resolution\n    end note\n```\n\n### Workflow Activities — In Execution Order\n\n```\n┌─────────────────────────────────────────────────────────────────┐\n│              RemittanceLifecycleWorkflow.execute()                │\n├─────────────────────────────────────────────────────────────────┤\n│                                                                  │\n│  Phase 1: ESCROW DEPOSIT                                         │\n│  ├─ Activity: depositEscrow (60s timeout, 3 retries, 2s backoff)│\n│  │   ├─ Fetch wallet's keyShareData from DB                     │\n│  │   ├─ Build Solana escrow deposit instruction                  │\n│  │   ├─ MPC-sign transaction (gRPC → sidecar)                   │\n│  │   └─ Submit to Solana devnet → returns signature              │\n│  ├─ Poll: awaitTransactionConfirmation(signature)                │\n│  │   ├─ Calls getSignatureStatuses RPC every 3 seconds           │\n│  │   ├─ Max 40 attempts (~120s total polling window)             │\n│  │   ├─ Accepts CONFIRMED or FINALIZED as success                │\n│  │   └─ Throws SP-0012 on timeout, SP-0031 on on-chain failure  │\n│  └─ Status Update: INITIATED → ESCROWED                         │\n│                                                                  │\n│  Phase 2: SMS NOTIFICATION                                       │\n│  ├─ Activity: sendClaimSms (30s timeout, 3 retries, 5s backoff) │\n│  │   ├─ Build claim URL: {claimBaseUrl}/{claimToken}             │\n│  │   └─ Send via Twilio (or log in dev mode)                     │\n│  └─ On failure: set smsNotificationFailed=true, continue         │\n│                                                                  │\n│  Phase 3: AWAIT CLAIM                                            │\n│  ├─ Workflow.await(48 hours, () -\u003e claimReceived)                │\n│  │                                                               │\n│  │   ┌─ PATH A: Claim signal received ──────────────────────┐   │\n│  │   │  Activity: releaseEscrow (60s, 3 retries, 2s backoff)│   │\n│  │   │  Poll: awaitTransactionConfirmation (3s × 40 max)     │   │\n│  │   │  Status Update: ESCROWED → CLAIMED                    │   │\n│  │   │  Activity: disburseInr (45s, NO retry) via Razorpay   │   │\n│  │   │  ├─ Success: Status → DELIVERED                       │   │\n│  │   │  └─ Failure: Status → DISBURSEMENT_FAILED             │   │\n│  │   └──────────────────────────────────────────────────────┘   │\n│  │                                                               │\n│  │   ┌─ PATH B: 48h timeout — no claim ────────────────────┐   │\n│  │   │  Activity: refundEscrow (60s, 3 retries, 2s backoff) │   │\n│  │   │  Poll: awaitTransactionConfirmation (3s × 40 max)     │   │\n│  │   │  Status Update: ESCROWED → REFUNDED                   │   │\n│  │   └──────────────────────────────────────────────────────┘   │\n│  └                                                               │\n│                                                                  │\n└─────────────────────────────────────────────────────────────────┘\n```\n\n### Re-Sign on Retry\n\nSolana blockhashes expire in ~60 seconds. If a deposit or release fails and retries, the workflow requests a **fresh MPC signature** with a new blockhash — it never replays a stale transaction.\n\n### Disbursement Does Not Retry\n\nOnce escrow is released on-chain, the USDC is gone. If the INR disbursement fails after release, retrying could cause duplicate payouts. The workflow marks the status as `DISBURSEMENT_FAILED` for manual resolution.\n\n---\n\n## Use Case 4: Claim Funds (Recipient)\n\n\u003e **No app required.** The recipient opens an SMS link, sees how much they'll receive in INR, enters their UPI ID, and submits. The Temporal workflow wakes up and completes the delivery.\n\n### Step 1: View Claim Details\n\n```mermaid\nsequenceDiagram\n    participant Recipient\n    participant API as ClaimController\n    participant Handler as GetClaimQueryHandler\n    participant ClaimRepo as ClaimTokenRepository\n    participant RemitRepo as RemittanceRepository\n\n    Recipient-\u003e\u003eAPI: GET /api/claims/{token}\n    API-\u003e\u003eHandler: handle(token)\n    Handler-\u003e\u003eClaimRepo: findByToken(token)\n    ClaimRepo--\u003e\u003eHandler: ClaimToken{remittanceId, expiresAt}\n    Handler-\u003e\u003eRemitRepo: findByRemittanceId(remittanceId)\n    RemitRepo--\u003e\u003eHandler: Remittance{amountUsdc: 100, amountInr: 8450, fxRate: 84.50}\n    Handler--\u003e\u003eAPI: ClaimDetails\n    API--\u003e\u003eRecipient: 200 OK\n```\n\n```\nGET /api/claims/a1b2c3d4-token-uuid\n```\n\n### Step 2: Submit Claim with UPI ID\n\n```mermaid\nsequenceDiagram\n    participant Recipient\n    participant API as ClaimController\n    participant Handler as SubmitClaimHandler\n    participant ClaimRepo as ClaimTokenRepository\n    participant RemitRepo as RemittanceRepository\n    participant Signaler as TemporalClaimSignaler\n    participant Workflow as RemittanceLifecycleWorkflow\n\n    Recipient-\u003e\u003eAPI: POST /api/claims/{token} {upiId: \"raj@upi\"}\n    API-\u003e\u003eHandler: handle(token, \"raj@upi\")\n\n    Note over Handler: Validation Chain\n    Handler-\u003e\u003eClaimRepo: findByToken(token)\n    Handler-\u003e\u003eHandler: ✓ Token exists (else SP-0011)\n    Handler-\u003e\u003eHandler: ✓ Not already claimed (else SP-0012)\n    Handler-\u003e\u003eHandler: ✓ Not expired (else SP-0013)\n    Handler-\u003e\u003eRemitRepo: findByRemittanceId(...)\n    Handler-\u003e\u003eHandler: ✓ Remittance exists (else SP-0010)\n    Handler-\u003e\u003eHandler: ✓ Status == ESCROWED (else SP-0014)\n\n    Handler-\u003e\u003eClaimRepo: save(claimed=true, upiId=\"raj@upi\")\n    Handler-\u003e\u003eSignaler: signalClaim(remittanceId, token, \"raj@upi\")\n\n    Signaler-\u003e\u003eSignaler: Resolve claim authority Solana address\n    Signaler-\u003e\u003eWorkflow: claimSubmitted(ClaimSignal)\n    Note over Workflow: claimReceived=true → unblocks await\n\n    Handler--\u003e\u003eAPI: ClaimDetails\n    API--\u003e\u003eRecipient: 200 OK\n```\n\n```\nPOST /api/claims/a1b2c3d4-token-uuid\nContent-Type: application/json\n\n{ \"upiId\": \"raj@upi\" }\n```\n\n### After the Signal: Workflow Completes Delivery\n\n```\n┌──────────────────────────────────────────────────────────────┐\n│         What Happens After claimSubmitted Signal               │\n├──────────────────────────────────────────────────────────────┤\n│                                                                │\n│  1. Workflow.await() unblocks (claimReceived = true)           │\n│                                                                │\n│  2. releaseEscrow activity                                     │\n│     ├─ Build Solana claim instruction                          │\n│     ├─ Transfer USDC from escrow PDA → destination address     │\n│     ├─ Close vault token account (reclaim rent)                │\n│     └─ Escrow status on-chain: Active → Claimed                │\n│                                                                │\n│  3. Status update: ESCROWED → CLAIMED                          │\n│                                                                │\n│  4. disburseInr activity                                       │\n│     ├─ Call Razorpay Payout API                                │\n│     ├─ Transfer INR to recipient's UPI ID                      │\n│     └─ INR credited to recipient's bank via UPI                │\n│                                                                │\n│  5. Status update: CLAIMED → DELIVERED ✓                       │\n│                                                                │\n└──────────────────────────────────────────────────────────────┘\n```\n\n### Claim Validation Rules\n\n| # | Check | Fails With | HTTP |\n|---|---|---|---|\n| 1 | Token exists in database | SP-0011 | 404 |\n| 2 | Token not already claimed | SP-0012 | 409 |\n| 3 | Token not expired (48h window) | SP-0013 | 410 |\n| 4 | Remittance exists | SP-0010 | 404 |\n| 5 | Remittance status is ESCROWED | SP-0014 | 409 |\n\n---\n\n## On-Chain Escrow Program\n\n![StablePay Solana Escrow Architecture](docs/images/solana-escrow.png)\n\n**Program ID:** `7C2zsbhgDnxQuC1Nd2rzXQfsfnKazQWFpoUJNqS8zWij`\n\nCustom Anchor program managing USDC escrow on Solana devnet.\n\n```mermaid\nstateDiagram-v2\n    [*] --\u003e Active: deposit(amount, deadline)\n    Active --\u003e Claimed: claim() — by claim authority\n    Active --\u003e Refunded: refund() — after deadline\n    Active --\u003e Cancelled: cancel() — by sender\n\n    note right of Active\n        USDC locked in PDA vault\n        seeds: [\"escrow\", remittance_id]\n    end note\n```\n\n### Instructions\n\n| Instruction | Caller | What It Does |\n|---|---|---|\n| `deposit(amount, deadline)` | Sender (MPC-signed) | Create escrow PDA, transfer USDC to vault, set 48h deadline |\n| `claim()` | Backend (claim authority) | Transfer vault USDC to recipient, close accounts |\n| `refund()` | Anyone (after deadline) | Return vault USDC to sender, close accounts |\n| `cancel()` | Sender only | Return USDC before claim, close accounts |\n\n### Escrow Account\n\n```rust\npub struct Escrow {\n    pub sender: Pubkey,           // Sender wallet (MPC-derived)\n    pub claim_authority: Pubkey,  // Backend authority for claim\n    pub mint: Pubkey,             // USDC mint address\n    pub amount: u64,              // Locked amount (6 decimals)\n    pub deadline: i64,            // Unix timestamp for refund eligibility\n    pub status: EscrowStatus,     // Active | Claimed | Refunded | Cancelled\n    pub bump: u8,                 // Canonical PDA bump\n    pub remittance_id: Pubkey,    // Links on-chain to off-chain\n}\n```\n\n### PDA Derivation\n\n| Account | Seeds |\n|---|---|\n| Escrow | `[\"escrow\", remittance_id]` |\n| Vault | `[\"vault\", escrow_pubkey]` |\n\n---\n\n## Solana Accounting Model\n\nFor a detailed visual walkthrough of every wallet, PDA, and token account involved in a remittance — from program deployment through escrow deposit, claim, and INR disbursement — see **[Solana Accounting Model](docs/SOLANA_ACCOUNTING_MODEL.md)**.\n\nCovers: account types (wallets, ATAs, PDAs, mints), the full lifecycle of a $25 remittance (8 acts), final ledger state, the refund path, and why PDAs enable trustless escrow.\n\n---\n\n## Complete Error Code Reference\n\n| Code | HTTP | Exception | Description |\n|---|---|---|---|\n| SP-0002 | 400 | InsufficientBalanceException | Wallet balance too low for remittance |\n| SP-0003 | 400 | MethodArgumentNotValidException | Request validation failure |\n| SP-0006 | 404 | WalletNotFoundException | Wallet not found by ID or userId |\n| SP-0007 | 503 | TreasuryDepletedException | Treasury has insufficient funds |\n| SP-0008 | 409 | WalletAlreadyExistsException | Wallet already exists for userId |\n| SP-0009 | 400 | UnsupportedCorridorException | Currency pair not supported |\n| SP-0010 | 404/500 | RemittanceNotFoundException / MpcKeyGenerationException | Remittance not found / MPC ceremony failed |\n| SP-0011 | 404 | ClaimTokenNotFoundException | Claim token not found |\n| SP-0012 | 409/500 | ClaimAlreadyClaimedException / SolanaTransactionException | Claim already submitted / TX confirmation timeout |\n| SP-0013 | 410 | ClaimTokenExpiredException | Claim token past 48h expiry |\n| SP-0014 | 409 | InvalidRemittanceStateException | Invalid state for operation |\n| SP-0016 | 409 | InvalidRemittanceStateException | Invalid status transition |\n| SP-0017 | 500 | SmsDeliveryException | SMS delivery failed |\n| SP-0018 | 502 | DisbursementException | INR disbursement failed |\n| SP-0020 | 404 | FundingOrderNotFoundException | Funding order not found |\n| SP-0021 | 502 | FundingFailedException | Stripe payment / funding failed |\n| SP-0022 | 409 | FundingAlreadyInProgressException | Funding already in progress for wallet |\n| SP-0026 | 400 | InvalidWebhookSignatureException | Stripe webhook signature invalid |\n| SP-0031 | 500 | SolanaTransactionException | Transaction failed on-chain |\n| SP-0032 | 401 | InvalidIdTokenException | Invalid Google ID token |\n| SP-0033 | 401 | EmailNotVerifiedException | Google email not verified |\n| SP-0034 | 400 | UnsupportedAuthProviderException | Unsupported auth provider (only GOOGLE) |\n| SP-0035 | 401 | InvalidRefreshTokenException | Invalid refresh token |\n| SP-0036 | 401 | RefreshTokenExpiredException | Refresh token expired |\n| SP-0040 | 401 | SecurityAuthenticationEntryPoint | Authentication required (missing/invalid JWT) |\n\n---\n\n## Quick Start\n\n### Prerequisites\n\n- Java 25 (`sdk install java 25-tem`)\n- Docker + Docker Compose\n- Go 1.26 (for MPC sidecar)\n- Solana CLI 2.2.7 + Anchor CLI 0.32.1 (for on-chain program)\n- Node.js 22+ (for Anchor tests)\n\n### Full Stack (Docker Compose)\n\n```bash\nmake up\n```\n\n```\n============================================\n  StablePay Dev Stack\n============================================\n  Backend API:    http://localhost:8080\n  Swagger UI:     http://localhost:8080/swagger-ui.html\n  Health:         http://localhost:8080/actuator/health\n  Temporal UI:    http://localhost:8088\n  PostgreSQL:     localhost:5432\n  Redis:          localhost:6379\n  MPC Sidecar 0:  localhost:50051 (gRPC)\n  MPC Sidecar 1:  localhost:50052 (gRPC)\n============================================\n```\n\n### Infrastructure Only (Local Backend Dev)\n\n```bash\nmake infra\ncd backend \u0026\u0026 ./gradlew bootRun\n```\n\n### Individual Components\n\n```bash\n# Backend — compile + format + all tests\ncd backend \u0026\u0026 ./gradlew build\n\n# Anchor program\nanchor build \u0026\u0026 anchor test\n\n# MPC sidecar\ncd mpc-sidecar \u0026\u0026 go build ./... \u0026\u0026 go test ./... -v -count=1 -timeout 120s\n```\n\n### Makefile Targets\n\n| Target | Description |\n|---|---|\n| `make up` | Build backend + start full Docker Compose stack (8 services) |\n| `make down` | Stop all services |\n| `make infra` | Start infrastructure only (for local backend dev) |\n| `make logs` | Follow Docker Compose logs |\n| `make clean` | Stop all services and remove volumes |\n\n### Try the API\n\nA Postman collection is available at [`docs/StablePay.postman_collection.json`](docs/StablePay.postman_collection.json).\n\nInteractive Swagger UI: http://localhost:8080/swagger-ui.html\n\n---\n\n## Testing\n\n```bash\n# Backend: all tests + formatting\ncd backend \u0026\u0026 ./gradlew build\n\n# Unit tests only\ncd backend \u0026\u0026 ./gradlew test\n\n# Integration tests with TestContainers\ncd backend \u0026\u0026 ./gradlew integrationTest\n\n# Anchor program tests (799 lines, TypeScript on localnet)\nanchor test\n\n# MPC sidecar tests\ncd mpc-sidecar \u0026\u0026 go test ./... -v -count=1 -timeout 120s\n```\n\n### CI Pipeline\n\nGitHub Actions runs **7 jobs** on every push to `main` and every PR:\n\n```mermaid\ngraph TD\n    A[Spotless Check] --\u003e B[Unit Tests]\n    A --\u003e C[Integration Tests]\n    B --\u003e D[Build JAR]\n    C --\u003e D\n    E[MPC Sidecar Tests]\n    F[Anchor Build] --\u003e G[Anchor Tests]\n```\n\n---\n\n## Tech Stack\n\n| Component | Technology |\n|---|---|\n| Backend | Java 25, Spring Boot 4.0.5, Spring MVC, JPA |\n| Database | PostgreSQL 18, Flyway 12.3 |\n| Cache | Redis 8 |\n| Workflows | Temporal 1.29.5 (SDK 1.34.0) |\n| On-chain | Rust, Anchor 0.32.1, Solana 2.2.7 (devnet) |\n| MPC | Go 1.26, bnb-chain/tss-lib (fystack fork) |\n| Solana SDK | sol4k 0.7.0 |\n| On-ramp | Stripe (card payments + webhooks) |\n| Off-ramp | Razorpay UPI Payouts |\n| SMS | Twilio 11.3.6 |\n| Mapping | MapStruct 1.6.3 |\n| Resilience | Resilience4j 2.3.0 |\n| API Docs | springdoc-openapi 3.0.2 |\n| Build | Gradle 9.4.1 (Kotlin DSL), Jib |\n| Testing | JUnit 5, BDDMockito, AssertJ, ArchUnit 1.4.1, TestContainers 1.21.4 |\n| CI | GitHub Actions (7 jobs) |\n\n---\n\n## Project Structure\n\n```\nstablepay-hackathon/\n├── backend/                          # Spring Boot API\n│   └── src/\n│       ├── main/java/com/stablepay/\n│       │   ├── application/          # Controllers, DTOs, config\n│       │   ├── domain/               # Models, handlers, ports\n│       │   │   ├── auth/             #   Social login, JWT, refresh tokens\n│       │   │   ├── wallet/           #   MPC wallet management\n│       │   │   ├── remittance/       #   Core remittance flow\n│       │   │   ├── funding/          #   Stripe funding orders\n│       │   │   ├── claim/            #   SMS claim tokens\n│       │   │   ├── fx/               #   FX rate quotes\n│       │   │   └── common/           #   Shared ports (SMS, disbursement)\n│       │   └── infrastructure/       # Adapters\n│       │       ├── db/               #   JPA + Flyway (8 migrations)\n│       │       ├── auth/             #   Google ID token verifier + JWT issuer\n│       │       ├── temporal/         #   Workflows + activities\n│       │       ├── mpc/              #   gRPC client to sidecars\n│       │       ├── solana/           #   RPC + escrow instruction builder + tx confirmation\n│       │       ├── stripe/           #   Stripe payments + webhook verification\n│       │       ├── razorpay/         #   Razorpay UPI disbursement\n│       │       ├── fx/               #   ExchangeRate-API + Redis cache\n│       │       └── sms/              #   Twilio + logging fallback\n│       ├── test/                     # 65 unit test files\n│       └── integration-test/         # 23 integration test files\n├── programs/stablepay-escrow/        # Anchor program (Rust)\n│   └── src/\n│       ├── lib.rs                    # 4 instructions\n│       ├── instructions/             # deposit, claim, refund, cancel\n│       ├── state/                    # Escrow account + EscrowStatus enum\n│       ├── errors.rs                 # 10 custom error codes\n│       └── constants.rs              # PDA seeds\n├── mpc-sidecar/                      # MPC threshold signing (Go)\n│   ├── cmd/sidecar/                  # Entry point\n│   ├── internal/\n│   │   ├── tss/                      # DKG + Ed25519 signing\n│   │   ├── p2p/                      # Ceremony registry + TCP coordination\n│   │   ├── server/                   # gRPC (GenerateKey, Sign, HealthCheck)\n│   │   └── config/                   # Environment-based config\n│   └── proto/                        # Protobuf definitions (sidecar + p2p)\n├── tests/                            # Anchor E2E tests (TypeScript, 799 lines)\n├── docs/                             # Architecture, standards, ADRs\n├── docker-compose.yml                # 8 services\n├── Makefile                          # Build + orchestration\n└── Anchor.toml\n```\n\n---\n\n## Contributing\n\nAll work goes through feature branches and pull requests. Never commit directly to `main`.\n\n```bash\ngit checkout -b feature/STA-42-add-claim-page\ncd backend \u0026\u0026 ./gradlew build\ngit push -u origin feature/STA-42-add-claim-page\ngh pr create --title \"STA-42: Add claim page\"\n```\n\nBranch naming: `feature/STA-{N}-description` · Commit messages: `feat(STA-{N}): description`\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuneethkumarck%2Fstablepay-hackathon","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpuneethkumarck%2Fstablepay-hackathon","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpuneethkumarck%2Fstablepay-hackathon/lists"}