> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sprinter.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Authorization Webhook Handler

> TypeScript implementation for handling card authorization webhooks with JIT credit draws via the Sprinter Credit API

## Overview

When a cardholder swipes their card, the card network sends an authorization webhook to your backend. You have \~2 seconds to check credit, execute an on-chain draw, and respond.

```
Card swipe → Visa → Card issuer → Your webhook endpoint
  ├─ Validate signature
  ├─ Look up wallet from card ID
  ├─ GET /credit/accounts/{account}/info   → check credit
  ├─ GET /credit/accounts/{account}/draw   → build draw calldata
  ├─ Execute on-chain via delegated signer
  └─ Respond: approved / declined
```

<Info>
  This example uses [Rain](https://www.rain.xyz) as the card issuer. Adapt signature validation and response format for your card program.
</Info>

## Implementation

<Steps>
  <Step title="Set Up Configuration">
    ```typescript theme={null}
    import express from "express";
    import crypto from "crypto";
    import { ethers } from "ethers";

    const app = express();
    app.use(express.json({ verify: (req: any, _res, buf) => { req.rawBody = buf; } }));

    const SPRINTER_API = "https://api.sprinter.tech";
    const RAIN_WEBHOOK_SECRET = process.env.RAIN_WEBHOOK_SECRET!;
    const SETTLEMENT_ADDRESS = process.env.SETTLEMENT_ADDRESS!;
    const RPC_URL = process.env.RPC_URL!;
    const SIGNER_PRIVATE_KEY = process.env.SIGNER_PRIVATE_KEY!;

    const provider = new ethers.JsonRpcProvider(RPC_URL);
    const signer = new ethers.Wallet(SIGNER_PRIVATE_KEY, provider);
    ```
  </Step>

  <Step title="Define Types and Helpers">
    ```typescript theme={null}
    interface ContractCall {
      to: string;
      data: string;
      value: string;
    }

    interface CreditInfo {
      data: {
        USDC: {
          totalCollateralValue: string;
          principal: string;
          interest: string;
          healthFactor: string;
          dueDate: string | null;
        };
      };
    }

    // Use Redis or a database in production
    const processed = new Map<string, { approved: boolean; txHash?: string }>();

    function validateSignature(rawBody: Buffer, signature: string): boolean {
      const expected = crypto
        .createHmac("sha256", RAIN_WEBHOOK_SECRET)
        .update(rawBody)
        .digest("hex");
      return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    }

    async function getWalletForCard(cardId: string): Promise<string | null> {
      // TODO: query your database
      throw new Error(`Implement getWalletForCard for cardId: ${cardId}`);
    }

    async function getCreditInfo(account: string): Promise<CreditInfo> {
      const res = await fetch(`${SPRINTER_API}/credit/accounts/${account}/info`);
      if (!res.ok) throw new Error(`Credit info failed: ${res.status}`);
      return res.json();
    }

    async function buildDrawCalls(
      account: string,
      amountUsdc: string,
      receiver: string
    ): Promise<ContractCall[]> {
      const url = new URL(`${SPRINTER_API}/credit/accounts/${account}/draw`);
      url.searchParams.set("amount", amountUsdc);
      url.searchParams.set("receiver", receiver);
      const res = await fetch(url.toString());
      if (!res.ok) throw new Error(`Draw calldata failed: ${res.status}`);
      const data = await res.json();
      return data.calls;
    }

    async function executeCalls(calls: ContractCall[]): Promise<string> {
      let lastTxHash = "";
      for (const call of calls) {
        const tx = await signer.sendTransaction({
          to: call.to,
          data: call.data,
          value: call.value || "0",
        });
        const receipt = await tx.wait();
        if (!receipt || receipt.status !== 1) {
          throw new Error(`Transaction reverted: ${tx.hash}`);
        }
        lastTxHash = tx.hash;
      }
      return lastTxHash;
    }

    /** $1.00 = 100 cents = 1_000_000 USDC units (6 decimals) */
    function centsToUsdcUnits(cents: number): string {
      return (cents * 10_000).toString();
    }
    ```
  </Step>

  <Step title="Handle the Webhook">
    ```typescript theme={null}
    app.post("/webhooks/rain/authorization", async (req, res) => {
      try {
        // 1. Validate webhook signature
        const signature = req.headers["x-rain-signature"] as string;
        if (!signature || !validateSignature((req as any).rawBody, signature)) {
          return res.status(401).json({ error: "Invalid signature" });
        }

        const { id, cardId, amount } = req.body;

        // 2. Idempotency — return cached result if already processed
        const cached = processed.get(id);
        if (cached) return res.json(cached);

        // 3. Look up user wallet
        const wallet = await getWalletForCard(cardId);
        if (!wallet) {
          processed.set(id, { approved: false });
          return res.json({ approved: false, reason: "unknown_card" });
        }

        // 4. Check available credit and health factor
        const info = await getCreditInfo(wallet);
        const available = parseFloat(info.data.USDC.totalCollateralValue);
        const healthFactor = parseFloat(info.data.USDC.healthFactor);

        if (amount / 100 > available) {
          processed.set(id, { approved: false });
          return res.json({ approved: false, reason: "insufficient_credit" });
        }

        if (healthFactor < 1.3) {
          processed.set(id, { approved: false });
          return res.json({ approved: false, reason: "low_health_factor" });
        }

        // 5. Build and execute the credit draw
        const calls = await buildDrawCalls(wallet, centsToUsdcUnits(amount), SETTLEMENT_ADDRESS);
        const txHash = await executeCalls(calls);

        processed.set(id, { approved: true, txHash });
        return res.json({ approved: true, txHash });
      } catch (error) {
        // Fail closed — always decline on error
        console.error("Authorization error:", error);
        return res.json({ approved: false, reason: "internal_error" });
      }
    });

    app.listen(3001, () => console.log("Webhook handler running on :3001"));
    ```
  </Step>
</Steps>

## Delegated Credit Draws

Card authorizations must complete without user interaction. Your backend needs to draw credit on behalf of users. There are two on-chain approaches:

### Option A: Non-Custodial (Smart Accounts)

Users deploy a smart account (ERC-4337) and grant your backend a scoped session key or module that can only call the credit draw function. The user retains full custody.

<CardGroup cols={3}>
  <Card title="ERC-7579 Session Keys" icon="key">
    User grants a scoped session key that can only call the credit draw function. Expires automatically.
  </Card>

  <Card title="Pre-signed Permits" icon="file-signature">
    User pre-signs EIP-2612 permits for USDC transfers up to a spending limit.
  </Card>

  <Card title="Smart Account Modules" icon="wallet">
    User's smart wallet delegates draw authority to your backend via a custom module.
  </Card>
</CardGroup>

### Option B: Operator Contract (Server-Side)

Deploy an [`ExclusiveOperator`](https://github.com/sprintertech/remote-collateral-contracts/blob/main/contracts/operator/ExclusiveOperator.sol) contract that lets your backend draw credit on behalf of opted-in users. This is the pattern used by card programs that need server-side control over credit draws.

**Setup (one-time):**

```solidity theme={null}
// Deploy with your backend address as the authorized caller
ExclusiveOperator operator = new ExclusiveOperator(
    creditHubAddress,    // Sprinter Credit Hub
    controllerAddress,   // Credit Hub Controller
    adminAddress,        // Contract admin (your multisig)
    callerAddress,       // Your backend's signing address
    7 days               // Revoke delay — users must wait 7 days to revoke
);
```

**User onboarding:**

```solidity theme={null}
// 1. User sets the operator on their credit position
creditHub.setOperator(address(operator));

// 2. User whitelists your settlement address as a credit receiver
operator.addCreditReceiver(settlementAddress);
```

**Drawing credit at swipe time:**

```solidity theme={null}
// Your backend calls this directly — no user signature needed
operator.openCreditLine(
    borrowerAddress,     // User's wallet
    settlementAddress,   // Whitelisted receiver
    amount               // USDC amount in wei
);
```

**Safety guarantees:**

* `withdraw()` is **disabled** — the operator can never touch collateral
* Credit can only go to **whitelisted receivers** the user has explicitly approved
* Revocation has a **time delay** (e.g. 7 days) — prevents users from revoking mid-billing-cycle while the operator still has outstanding credit exposure
* Users can schedule revocation at any time via `revoke()`, then finalize after the delay with `finalizeRevoke()`

<Warning>
  The caller address can draw credit from any opted-in user account. Secure the private key with HSM or KMS in production — never store it in environment variables on shared infrastructure.
</Warning>

## Latency Budget

Card networks expect a response within \~2 seconds:

| Step                                  |     Target    | Notes                   |
| ------------------------------------- | :-----------: | ----------------------- |
| Signature validation                  |     \< 1ms    | Local crypto            |
| Database lookup (card → wallet)       |    \< 10ms    | Index on card ID        |
| `GET /credit/accounts/{account}/info` |    \< 100ms   | Sprinter API            |
| `GET /credit/accounts/{account}/draw` |    \< 100ms   | Sprinter API            |
| On-chain execution                    |   \< 1000ms   | Dedicated Base RPC node |
| **Total**                             | **\< 1300ms** | \~700ms buffer          |

<Tip>
  Use a dedicated RPC node on Base. Public endpoints will exceed the time budget under load.
</Tip>

## Production Checklist

<AccordionGroup>
  <Accordion title="Infrastructure" icon="server" defaultOpen={true}>
    * [ ] Replace in-memory idempotency map with Redis or database
    * [ ] Use a dedicated Base RPC node (Alchemy, QuickNode, or self-hosted)
    * [ ] Store signer key in HSM or cloud KMS (AWS KMS, GCP Cloud KMS)
  </Accordion>

  <Accordion title="Application" icon="code">
    * [ ] Implement `getWalletForCard()` against your user database
    * [ ] Log all authorization decisions for audit trail
    * [ ] Load test under expected peak authorization volume
  </Accordion>

  <Accordion title="Monitoring" icon="chart-line">
    * [ ] Monitor authorization latency (p99 \< 2s)
    * [ ] Alert on elevated decline rates
    * [ ] Track on-chain execution success rate
  </Accordion>
</AccordionGroup>

## Environment Variables

| Variable              | Description                                          |
| --------------------- | ---------------------------------------------------- |
| `RAIN_WEBHOOK_SECRET` | Webhook signing secret from your card issuer         |
| `SETTLEMENT_ADDRESS`  | Your card program's USDC settlement address on Base  |
| `RPC_URL`             | Base RPC endpoint (dedicated node recommended)       |
| `SIGNER_PRIVATE_KEY`  | Delegated signer private key (HSM/KMS in production) |
