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

# Healthcare Escrow — MedConnect

> **See also:** [API Reference Examples](/contracts-sdk/examples/overview) — executable TypeScript walkthroughs.

## The Business

**MedConnect** is a healthcare platform that connects patients with specialist doctors for consultations, lab work, and follow-up care. Patients pay upfront, but their funds are held in escrow until the doctor confirms the service was delivered. If the service is not delivered within the agreed timeframe, the patient gets a full refund.

## Why Oak?

MedConnect needs a trustless escrow mechanism that:

* Holds patient payments securely until service confirmation
* Allows the platform to confirm delivery on behalf of the provider
* Enables automatic refunds if service is not delivered
* Tracks fees (platform booking fee, protocol fee) transparently
* Works with **any accepted ERC-20** on the campaign's token whitelist (examples below use USDC for readability)

## Oak Contract Used

| Contract                | Purpose                                                                                                                |
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **CampaignInfoFactory** | Creates the CampaignInfo contract that holds NFT receipts and the accepted token list                                  |
| **TreasuryFactory**     | Deploys the PaymentTreasury clone linked to the CampaignInfo                                                           |
| **PaymentTreasury**     | Holds patient funds until the platform confirms service delivery. Supports line items, external fees, and refund flows |

### Multi-token support

Payments specify **`paymentToken`**; the contract reverts unless **`CampaignInfo.isTokenAccepted(paymentToken)`** is true. The campaign may accept **several ERC-20s** for one logical currency; pending, confirmed, fee, and refund accounting is **per token address** in each token's **native decimals**. Snippets in this guide use **USDC** as a stand-in—use any whitelisted token your `GlobalParams` / campaign configuration allows. Resolve the list with **`globalParams.getTokensForCurrency(currency)`** or read the campaign's cached copy via **`campaign.getAcceptedTokens()`** (same addresses the factory stored at creation).

## Roles

| Role                                 | Who                  | On-Chain Functions                                                                                                                                                                                                                               |
| ------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Platform Admin**                   | MedConnect backend   | `createPayment`, `createPaymentBatch`, `confirmPayment`, `confirmPaymentBatch`, `cancelPayment`, `claimRefund(paymentId, address)` (non-NFT), `claimExpiredFunds`, `claimNonGoalLineItems`, `pauseTreasury`, `unpauseTreasury`, `cancelTreasury` |
| **Platform Admin or Campaign Owner** | MedConnect or clinic | `withdraw`, `cancelTreasury`                                                                                                                                                                                                                     |
| **Patient (Buyer)**                  | Sarah                | ERC-20 `approve`, `processCryptoPayment`, `claimRefundSelf(paymentId)` (NFT payments)                                                                                                                                                            |
| **Protocol Admin**                   | Oak protocol         | Receives protocol fees (via `disburseFees`)                                                                                                                                                                                                      |
| **Any caller**                       | Anyone               | `disburseFees`, all read functions (`getPaymentData`, `getRaisedAmount`, `getExpectedAmount`, `paused`, etc.)                                                                                                                                    |

## Integration Flow

### Step 1: Create a CampaignInfo contract

> **Role: Any caller** — `createCampaign` is permissionless.

Before deploying a PaymentTreasury, MedConnect needs a CampaignInfo contract. This holds NFT receipts for crypto payments and defines the accepted token list.

```typescript theme={null}
import {
  createOakContractsClient, keccak256, toHex,
  getCurrentTimestamp, addDays, CHAIN_IDS, CAMPAIGN_INFO_FACTORY_EVENTS,
} from "@oaknetwork/contracts-sdk";

const oak = createOakContractsClient({
  chainId: CHAIN_IDS.CELO_TESTNET_SEPOLIA,
  rpcUrl: process.env.RPC_URL,
  privateKey: process.env.PLATFORM_PRIVATE_KEY as `0x${string}`,
});

const factory = oak.campaignInfoFactory(CAMPAIGN_INFO_FACTORY_ADDRESS);

const platformHash = keccak256(toHex("medconnect"));
const identifierHash = keccak256(toHex("medconnect-escrow-2026"));
const now = getCurrentTimestamp();

const txHash = await factory.createCampaign({
  creator: PLATFORM_ADMIN_ADDRESS,
  identifierHash,
  selectedPlatformHash: [platformHash],
  campaignData: {
    launchTime: now,
    deadline: addDays(now, 365),
    goalAmount: 0n,
    currency: toHex("USD", { size: 32 }),
  },
  nftName: "MedConnect Receipts",
  nftSymbol: "MCR",
  nftImageURI: "ipfs://QmXyz.../medconnect-receipt.png",
  contractURI: "ipfs://QmXyz.../metadata.json",
});

const receipt = await oak.waitForReceipt(txHash);

let campaignInfoAddress: `0x${string}` | undefined;
for (const log of receipt.logs) {
  try {
    const decoded = factory.events.decodeLog({
      topics: log.topics as [`0x${string}`, ...`0x${string}`[]],
      data: log.data as `0x${string}`,
    });
    if (decoded.eventName === CAMPAIGN_INFO_FACTORY_EVENTS.CampaignCreated) {
      campaignInfoAddress = decoded.args?.campaignInfoAddress as `0x${string}`;
      break;
    }
  } catch { /* log from a different contract */ }
}
```

### Step 2: Deploy the PaymentTreasury

> **Role: Any caller** — `deploy` on TreasuryFactory is permissionless (the implementation must have been registered and approved during platform onboarding).

MedConnect deploys a PaymentTreasury linked to the CampaignInfo from Step 1.

```typescript theme={null}
import { TREASURY_FACTORY_EVENTS } from "@oaknetwork/contracts-sdk";

const treasuryFactory = oak.treasuryFactory(TREASURY_FACTORY_ADDRESS);

const deployTxHash = await treasuryFactory.deploy(
  platformHash,
  campaignInfoAddress!,
  2n, // PaymentTreasury implementation ID
);

const deployReceipt = await oak.waitForReceipt(deployTxHash);

let treasuryAddress: `0x${string}` | undefined;
for (const log of deployReceipt.logs) {
  try {
    const decoded = treasuryFactory.events.decodeLog({
      topics: log.topics as [`0x${string}`, ...`0x${string}`[]],
      data: log.data as `0x${string}`,
    });
    if (decoded.eventName === TREASURY_FACTORY_EVENTS.TreasuryDeployed) {
      treasuryAddress = decoded.args?.treasuryAddress as `0x${string}`;
      break;
    }
  } catch { /* log from a different contract */ }
}

const treasury = oak.paymentTreasury(treasuryAddress!);
```

### Step 3: Patient books appointment — two independent payment flows

Sarah books a cardiology consultation with Dr. Rivera. The appointment costs 150 USDC broken down into two line items: consultation (120 USDC) and lab work (30 USDC).

MedConnect supports two payment methods — they are **not** sequential steps:

#### Flow A: Off-chain / fiat payment (`createPayment`)

> **Role: Platform Admin** — only the platform admin can create payment records.

MedConnect creates a payment record on-chain. The **`createPayment` transaction does not pull ERC-20 from the buyer's wallet** — it only records the obligation and pending accounting. Sarah pays through off-chain rails (credit card, insurance billing, etc.). Before MedConnect can call `confirmPayment`, **the treasury must actually hold enough of the payment token on-chain** (for example the platform deposits USDC after fiat settlement). The contract checks the treasury's ERC-20 balance when confirming; if the tokens are not there, `confirmPayment` reverts.

```typescript theme={null}
import { toHex } from "@oaknetwork/contracts-sdk";

const paymentId = toHex("medconnect-appt-12345", { size: 32 });
const buyerId = toHex("patient-sarah-001", { size: 32 });
const itemId = toHex("cardiology-consult", { size: 32 });

const lineItems = [
  { typeId: toHex("consultation", { size: 32 }), amount: 120_000000n },  // 120 USDC (6 decimals)
  { typeId: toHex("lab-work", { size: 32 }),      amount: 30_000000n },  // 30 USDC
];

const externalFees = [
  { feeType: toHex("platform-booking-fee", { size: 32 }), feeAmount: 5_000000n },  // 5 USDC
];

// Simulate first to catch errors before spending gas
await treasury.simulate.createPayment(
  paymentId, buyerId, itemId, USDC_TOKEN_ADDRESS,
  150_000000n,                                       // total: 150 USDC
  BigInt(Math.floor(Date.now() / 1000) + 7 * 86400), // expires in 7 days
  lineItems, externalFees,
);

// Send the transaction
const txHash = await treasury.createPayment(
  paymentId, buyerId, itemId, USDC_TOKEN_ADDRESS,
  150_000000n,
  BigInt(Math.floor(Date.now() / 1000) + 7 * 86400),
  lineItems, externalFees,
);
await oak.waitForReceipt(txHash);
```

At this point the payment record exists on-chain and pending amounts are tracked, but **no ERC-20 was transferred in this transaction**. Sarah completes payment off-chain; your operations then **fund the treasury** with the agreed token amount before you confirm (how you bridge or deposit is product-specific).

##### Confirm payment (platform admin)

> **Role: Platform Admin** — only the platform admin can call `confirmPayment` for payments created with `createPayment`.

Dr. Rivera completes the consultation and marks it as delivered. After off-chain verification **and** once the treasury holds the required ERC-20 balance, the backend calls `confirmPayment` to move accounting from pending to confirmed (and optionally mint an NFT if you pass Sarah's wallet as `buyerAddress`).

```typescript theme={null}
await treasury.simulate.confirmPayment(paymentId, SARAH_WALLET_ADDRESS);

const txHash = await treasury.confirmPayment(paymentId, SARAH_WALLET_ADDRESS);
await oak.waitForReceipt(txHash);
```

The payment status is now **confirmed**. Funds are ready for fee disbursement and withdrawal.

#### Flow B: On-chain crypto payment (`processCryptoPayment`)

> **Role: Any caller** — `processCryptoPayment` is permissionless, but the buyer must first approve the treasury to transfer their ERC-20 tokens.

This is a **standalone operation** — it creates the payment record AND transfers ERC-20 tokens in a single transaction. It does **not** require or complete a prior `createPayment` call. An NFT is minted to Sarah as proof of payment.

Sarah opens the MedConnect app, sees the \$150 charge, and approves the transfer:

```typescript theme={null}
import { erc20Abi } from "viem";

const usdc = { address: USDC_TOKEN_ADDRESS, abi: erc20Abi };

// Sarah approves the treasury to spend up to 150 USDC
const approveTx = await walletClient.writeContract({
  ...usdc,
  functionName: "approve",
  args: [TREASURY_ADDRESS, 150_000000n],
});
await publicClient.waitForTransactionReceipt({ hash: approveTx });
```

Now the payment can be processed:

```typescript theme={null}
const txHash = await treasury.processCryptoPayment(
  paymentId, itemId, SARAH_WALLET_ADDRESS, USDC_TOKEN_ADDRESS,
  150_000000n,
  lineItems, externalFees,
);
await oak.waitForReceipt(txHash);
```

### Step 4: Read the final treasury state

> **Role: Any caller** — all read functions are public.

MedConnect's dashboard shows the current state of the escrow pool.

```typescript theme={null}
const [raised, available, lifetime, refunded] = await oak.multicall([
  () => treasury.getRaisedAmount(),
  () => treasury.getAvailableRaisedAmount(),
  () => treasury.getLifetimeRaisedAmount(),
  () => treasury.getRefundedAmount(),
]);
```

### Step 5: Disburse fees

> **Role: Any caller** — `disburseFees` is permissionless. Fees are sent to the Protocol Admin and Platform Admin automatically.

Before the provider can withdraw, accumulated protocol and platform fees are distributed.

```typescript theme={null}
const txHash = await treasury.disburseFees();
await oak.waitForReceipt(txHash);
```

### Step 6: Withdraw settled funds

> **Role: Platform Admin or Campaign Owner** — either party can trigger withdrawal. Funds are always sent to the campaign owner (Dr. Rivera's clinic).

The settled amount (minus fees) is sent to the campaign owner.

```typescript theme={null}
const txHash = await treasury.withdraw();
await oak.waitForReceipt(txHash);
```

### Alternative: Cancellation and refund flows

> Three distinct paths exist depending on payment state and type:

**A) Cancel an unconfirmed off-chain payment (before `confirmPayment`):**

> **Role: Platform Admin** — `cancelPayment` works only on unconfirmed, non-expired, non-crypto payments. The transaction **removes the pending payment record** from contract accounting; it **does not** automatically return ERC-20 that may already sit in the treasury—handle any token recovery operationally if you deposited before cancelling. Off-chain refunds (card reversal, etc.) are handled by MedConnect outside this call.

```typescript theme={null}
await treasury.cancelPayment(paymentId);
```

**B) Refund a confirmed off-chain payment (non-NFT):**

> **Role: Platform Admin** — `claimRefund(paymentId, refundAddress)` refunds a confirmed payment where no NFT was minted. The contract verifies the payment is confirmed and has `tokenId == 0`.

```typescript theme={null}
await treasury.claimRefund(paymentId, SARAH_WALLET_ADDRESS);
```

**C) Refund a crypto payment (NFT was minted via `processCryptoPayment`):**

> **Role: Any caller (NFT owner)** — `claimRefundSelf(paymentId)` is for crypto payments (auto-confirmed on creation). The contract looks up the NFT owner, burns the NFT, and sends the refundable amount to that owner. No prior `cancelPayment` is needed — crypto payments cannot be cancelled via `cancelPayment`.

Before calling `claimRefundSelf`, the NFT owner must approve the treasury to manage the NFT. All pledge NFTs live on the **CampaignInfo** contract (not the treasury itself), so approval uses the CampaignInfo SDK entity:

```typescript theme={null}
const campaign = oak.campaignInfo(CAMPAIGN_INFO_ADDRESS);
await campaign.approve(TREASURY_ADDRESS, tokenId);

await treasury.claimRefundSelf(paymentId);
```

### Step 7: Claim non-goal line items

> **Role: Platform Admin** — only the platform admin can claim non-goal line items.

If the payment included line items that don't count toward the campaign goal (e.g., platform commission, processing fees), these accumulate separately. The platform admin can claim them at any time after confirmation.

```typescript theme={null}
const txHash = await treasury.claimNonGoalLineItems(USDC_TOKEN_ADDRESS);
await oak.waitForReceipt(txHash);
```

### Step 8: Pause, unpause, or cancel the treasury

**Pause the treasury:**

> **Role: Platform Admin** — only the platform admin can pause and unpause.

If MedConnect needs to halt operations for compliance or investigation:

```typescript theme={null}
const txHash = await treasury.pauseTreasury(toHex("compliance-review", { size: 32 }));
await oak.waitForReceipt(txHash);

const isPaused = await treasury.paused();
```

While paused, no payments, confirmations, refunds, or withdrawals can occur.

**Unpause the treasury:**

> **Role: Platform Admin**

```typescript theme={null}
const txHash = await treasury.unpauseTreasury(toHex("review-complete", { size: 32 }));
await oak.waitForReceipt(txHash);
```

**Cancel the treasury permanently:**

> **Role: Platform Admin or Campaign Owner** — either party can cancel.

Cancellation is irreversible. After cancellation, backers can still claim refunds for confirmed NFT payments, but no new payments, confirmations, or withdrawals can happen.

```typescript theme={null}
const txHash = await treasury.cancelTreasury(toHex("treasury-shutdown", { size: 32 }));
await oak.waitForReceipt(txHash);

const isCancelled = await treasury.cancelled();
```

### Batch operations

> **Role: Platform Admin** — batch create and confirm are platform-admin-only.

For high-volume platforms, PaymentTreasury supports batch operations to reduce gas costs and prevent nonce conflicts.

**Batch create payments:**

```typescript theme={null}
const paymentIds = [
  toHex("appt-001", { size: 32 }),
  toHex("appt-002", { size: 32 }),
];
const buyerIds = [
  toHex("patient-sarah", { size: 32 }),
  toHex("patient-john", { size: 32 }),
];
const itemIds = [
  toHex("cardiology", { size: 32 }),
  toHex("dermatology", { size: 32 }),
];
const tokens = [USDC_TOKEN_ADDRESS, USDC_TOKEN_ADDRESS];
const amounts = [150_000000n, 200_000000n];
const expirations = [
  BigInt(Math.floor(Date.now() / 1000) + 7 * 86400),
  BigInt(Math.floor(Date.now() / 1000) + 7 * 86400),
];

const txHash = await treasury.createPaymentBatch(
  paymentIds, buyerIds, itemIds, tokens, amounts, expirations,
  [lineItems1, lineItems2], [externalFees1, externalFees2],
);
await oak.waitForReceipt(txHash);
```

**Batch confirm payments:**

```typescript theme={null}
const txHash = await treasury.confirmPaymentBatch(
  [toHex("appt-001", { size: 32 }), toHex("appt-002", { size: 32 })],
  [SARAH_WALLET_ADDRESS, JOHN_WALLET_ADDRESS],
);
await oak.waitForReceipt(txHash);
```

## Architecture Diagram

**MedConnect Healthcare Escrow Flow**

```mermaid theme={null}
sequenceDiagram
    participant Patient as Patient (Sarah)
    participant MedConnect as MedConnect (Platform Admin)
    participant Treasury as PaymentTreasury

    Patient->>MedConnect: Books appointment

    rect rgb(40, 40, 60)
    Note over Patient,Treasury: Flow A — Off-chain / fiat payment
    MedConnect->>Treasury: createPayment(...)<br/>[Platform Admin]
    Note over Treasury: Payment recorded<br/>(no pull from buyer)
    Patient->>MedConnect: Pays off-chain<br/>(insurance, credit card)
    MedConnect->>Treasury: Tokens sent to treasury<br/>(deposit / bridge / ops)
    Note over Treasury: ERC-20 balance must cover confirm
    Note over MedConnect: Doctor confirms delivery
    MedConnect->>Treasury: confirmPayment(...)<br/>[Platform Admin]
    Note over Treasury: Pending → confirmed (Flow A only)
    end

    rect rgb(40, 60, 40)
    Note over Patient,Treasury: Flow B — On-chain crypto payment
    Patient->>Treasury: ERC-20 approve()
    Note over Treasury: Treasury approved to spend
    MedConnect->>Treasury: processCryptoPayment(...)<br/>[Any caller]
    Note over Treasury: Pull tokens, already confirmed + NFT
    end

    rect rgb(60, 60, 40)
    Note over Patient,Treasury: Both flows (after Flow A confirm or Flow B)
    MedConnect->>Treasury: claimNonGoalLineItems()<br/>[Platform Admin]
    Note over Treasury: Non-goal items claimed
    MedConnect->>Treasury: disburseFees()<br/>[Any caller]
    Note over Treasury: Fees → Protocol + Platform
    MedConnect->>Treasury: withdraw()<br/>[Admin or Owner]
    Note over Treasury: Funds → Provider
    end

    rect rgb(60, 40, 40)
    Note over Patient,Treasury: Pause / Cancel (optional)
    MedConnect->>Treasury: pauseTreasury() / cancelTreasury()<br/>[Platform Admin / Owner]
    Note over Treasury: Treasury state updated
    end
```

## Key Takeaways

* **ERC-20 approval is required** — the buyer must `approve` the treasury contract before `processCryptoPayment` can transfer tokens
* **`createPayment` path** — the `createPayment` transaction does not pull ERC-20 from the buyer; fund the treasury before `confirmPayment` (the contract checks balance on confirm)
* **`processCryptoPayment` path** — pulls tokens and confirms in one transaction; use `disburseFees` / `withdraw` afterward—do not call `confirmPayment` for these payments
* **Multi-token** — `paymentToken` must be on the campaign's accepted list; balances and refunds are tracked per ERC-20 (each token's decimals)
* **Funds stay in the treasury** — held under contract rules until withdrawal, disbursement, or refund flows
* **Role-based access** — `createPayment`/`confirmPayment`/`cancelPayment` are platform-admin-only; `processCryptoPayment` and `disburseFees` are permissionless; `withdraw` requires admin or owner
* **Three cancellation/refund paths** — `cancelPayment` deletes unconfirmed off-chain records (no on-chain refund); `claimRefund(paymentId, address)` refunds confirmed non-NFT payments (platform admin); `claimRefundSelf(paymentId)` refunds crypto/NFT payments directly (NFT owner, no prior cancel needed; burns pledge NFT — requires prior ERC-721 approval on the CampaignInfo contract)
* **Line items** allow granular tracking (consultation vs. lab work) with configurable goal-counting, fees, and refund rules
* **Non-goal line items** (e.g., platform commission) can be claimed separately via `claimNonGoalLineItems`
* **Batch operations** — `createPaymentBatch` and `confirmPaymentBatch` for high-volume platforms
* **Pause / cancel controls** — platform admin can pause; either admin or owner can permanently cancel
* **External fees** track platform charges transparently on-chain (informational only, no financial impact)
* **Simulate before send** catches errors before spending gas
* **`multicall`** batches multiple reads into a single RPC call for dashboard views
