• SettleMintSettleMint
    • Introduction
    • Market pain points
    • Lifecycle platform approach
    • Platform capabilities
    • Use cases
    • Compliance & security
    • Glossary
    • Core component overview
    • Frontend layer
    • API layer
    • Blockchain layer
    • Data layer
    • Deployment layer
    • System architecture
    • Smart contracts
    • Application layer
    • Data & indexing
    • Integration & operations
    • Performance
    • Quality
    • Getting started
    • Asset issuance
    • Platform operations
    • Troubleshooting
    • Development environment
    • Code structure
    • Smart contracts
    • API integration
      • Using the API
      • API reference
    • Data model
    • Deployment & ops
    • Testing and QA
    • Developer FAQ
Back to the application
  1. Documentation
  2. Developer guides
  3. API integration

Type-safe API integration for DALP asset operations

Build DALP lifecycle operations with type-safe ORPC procedures. This guide shows how to implement vault operations, DvP settlements, and yield claims through composable API patterns that connect React components to blockchain contracts and database state.

The Asset Tokenization Kit API layer bridges frontend components to blockchain operations and database state through ORPC—a type-safe RPC framework that enforces contracts at compile time. Unlike REST endpoints that accept arbitrary JSON, ORPC procedures validate inputs with Zod schemas and infer TypeScript types automatically, preventing runtime errors before deployment.

This architecture matters because DALP lifecycle operations—creating vaults, executing DvP settlements, distributing yield—require coordination across smart contracts, indexers, and database tables. A single operation might call a Hardhat deployment script, query TheGraph for indexed events, update PostgreSQL records, and invalidate TanStack Query caches. ORPC's middleware stack orchestrates these layers while maintaining end-to-end type safety from React form to Solidity function.

ORPC architecture for DALP operations

The API layer implements a layered architecture where procedures compose middleware to enrich context, enforce authorization, and inject services.

Rendering chart...

Why this architecture

Traditional REST APIs require manual validation, authentication checks scattered across endpoints, and hand-written TypeScript types that drift from runtime behavior. ORPC procedures centralize these concerns in middleware stacks that compose vertically—publicRouter adds error formatting and i18n, authRouter enforces authentication, onboardedRouter verifies KYC completion and injects identity claims.

For DALP operations, this means a token creation endpoint automatically validates that the user completed onboarding, loads their OnchainID claims, checks their wallet connection, and injects the system's token factory addresses—all before the handler executes. The handler focuses purely on business logic: validating bond parameters, deploying the contract via Hardhat, indexing the deployment in TheGraph, recording metadata in PostgreSQL.

Client setup and isomorphic execution

The ORPC client adapts its transport based on execution context—server-side it calls routers directly, client-side it sends HTTP requests. Import the pre-configured client:

import { orpc } from "@/orpc/orpc-client";

Server-side execution in loaders

TanStack Router loaders run on the server during SSR and prefetching. The ORPC client detects this environment and invokes handlers directly without HTTP overhead:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/vaults/$vaultId")({
  loader: async ({ params, context: { queryClient, orpc: orpcClient } }) => {
    // Direct router invocation - no HTTP request
    await queryClient.ensureQueryData(
      orpcClient.vault.read.queryOptions({
        input: { id: params.vaultId },
      })
    );

    // Prefetch related vault operations and reserve metrics
    await Promise.all([
      queryClient.ensureQueryData(
        orpcClient.vault.reserves.queryOptions({
          input: { vaultId: params.vaultId },
        })
      ),
      queryClient.ensureQueryData(
        orpcClient.vault.operations.queryOptions({
          input: { vaultId: params.vaultId, limit: 20 },
        })
      ),
    ]);

    return { vaultId: params.vaultId };
  },
  component: VaultDetailsPage,
});

function VaultDetailsPage() {
  const { vaultId } = Route.useLoaderData();

  // Data already cached from loader
  const { data: vault } = useSuspenseQuery(
    orpc.vault.read.queryOptions({ input: { id: vaultId } })
  );

  return <VaultDashboard vault={vault} />;
}

Client-side execution in components

Browser environments send HTTP requests to /api/rpc with automatic credential inclusion:

import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function VaultReserveRatio({ vaultId }: { vaultId: string }) {
  // HTTP request in browser, direct call in SSR
  const { data: reserves } = useSuspenseQuery(
    orpc.vault.reserves.queryOptions({
      input: { id: vaultId },
    })
  );

  const ratio = (reserves.balance / reserves.totalSupply) * 100;

  return (
    <div>
      Reserve Ratio: {ratio.toFixed(2)}%
      {ratio < 100 && <Alert>Undercollateralized vault</Alert>}
    </div>
  );
}

Queries: fetching DALP state

Queries read data from smart contracts (via TheGraph indexing), database tables, or computed aggregations. TanStack Query handles caching, background refetching, and stale data management.

Basic vault query with automatic type inference

import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function VaultOverview({ vaultId }: { vaultId: string }) {
  // Suspense boundary handles loading state
  const { data: vault } = useSuspenseQuery(
    orpc.vault.read.queryOptions({
      input: { id: vaultId },
    })
  );

  // TypeScript infers vault shape from Zod schema
  return (
    <div>
      <h1>{vault.name}</h1>
      <p>Address: {vault.address}</p>
      <p>Collateral: {vault.collateralToken}</p>
      <p>Reserve Balance: {vault.reserveBalance}</p>
    </div>
  );
}

The vault object's type is inferred from the procedure's output schema—no manual TypeScript interfaces required. If the backend schema changes (adding a lastRebalanceTimestamp field), TypeScript immediately flags components accessing the old shape.

Paginated DvP settlement history

DvP (Delivery versus Payment) settlements execute atomically—token delivery and payment occur in the same transaction. Query paginated settlement history with filtering:

import { useState } from "react";
import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function DvPSettlementHistory({ tokenId }: { tokenId: string }) {
  const [page, setPage] = useState(1);
  const limit = 20;

  const { data } = useSuspenseQuery(
    orpc.settlement.list.queryOptions({
      input: {
        tokenId,
        offset: (page - 1) * limit,
        limit,
        orderBy: "settledAt",
        orderDirection: "desc",
        status: "completed", // Filter for successful settlements
      },
    })
  );

  return (
    <div>
      <h2>DvP Settlement History</h2>
      <table>
        <thead>
          <tr>
            <th>Settlement ID</th>
            <th>Buyer</th>
            <th>Seller</th>
            <th>Token Amount</th>
            <th>Payment Amount</th>
            <th>Settled At</th>
          </tr>
        </thead>
        <tbody>
          {data.items.map((settlement) => (
            <tr key={settlement.id}>
              <td>{settlement.id}</td>
              <td>{settlement.buyer}</td>
              <td>{settlement.seller}</td>
              <td>{settlement.tokenAmount}</td>
              <td>{settlement.paymentAmount}</td>
              <td>{new Date(settlement.settledAt).toLocaleString()}</td>
            </tr>
          ))}
        </tbody>
      </table>
      <Pagination
        page={page}
        total={data.total}
        limit={limit}
        onPageChange={setPage}
      />
    </div>
  );
}

Conditional queries based on user state

Only execute queries when prerequisites are met:

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function UserYieldClaims({ userId }: { userId?: string }) {
  const { data } = useQuery(
    orpc.yield.claimHistory.queryOptions({
      input: { userId: userId! },
      enabled: !!userId, // Only query when user is authenticated
    })
  );

  if (!userId) return <p>Log in to view yield claims</p>;
  if (!data) return <p>Loading claim history...</p>;

  const totalClaimed = data.claims.reduce(
    (sum, claim) => sum + BigInt(claim.amount),
    0n
  );

  return (
    <div>
      <h3>Total Claimed: {totalClaimed.toString()}</h3>
      <ul>
        {data.claims.map((claim) => (
          <li key={claim.id}>
            {claim.tokenSymbol}: {claim.amount} ({claim.claimedAt})
          </li>
        ))}
      </ul>
    </div>
  );
}

Prefetching related data in loaders

Loaders prefetch interconnected data to avoid waterfall requests:

import { createFileRoute } from "@tanstack/react-router";

export const Route = createFileRoute("/bonds/$bondId")({
  loader: async ({ params, context: { queryClient, orpc: orpcClient } }) => {
    // Prefetch bond details
    await queryClient.ensureQueryData(
      orpcClient.bond.read.queryOptions({
        input: { id: params.bondId },
      })
    );

    // Prefetch related data in parallel
    await Promise.all([
      // Coupon payment schedule
      queryClient.ensureQueryData(
        orpcClient.bond.coupons.queryOptions({
          input: { bondId: params.bondId },
        })
      ),
      // Yield distribution history
      queryClient.ensureQueryData(
        orpcClient.yield.history.queryOptions({
          input: { tokenId: params.bondId, limit: 10 },
        })
      ),
      // Current bondholders
      queryClient.ensureQueryData(
        orpcClient.token.holders.queryOptions({
          input: { tokenId: params.bondId, limit: 50 },
        })
      ),
    ]);

    return { bondId: params.bondId };
  },
  component: BondDetailsPage,
});

function BondDetailsPage() {
  const { bondId } = Route.useLoaderData();

  // All data cached - no waterfalls
  const { data: bond } = useSuspenseQuery(
    orpc.bond.read.queryOptions({ input: { id: bondId } })
  );

  const { data: coupons } = useSuspenseQuery(
    orpc.bond.coupons.queryOptions({ input: { bondId } })
  );

  const { data: yields } = useSuspenseQuery(
    orpc.yield.history.queryOptions({ input: { tokenId: bondId, limit: 10 } })
  );

  return (
    <div>
      <BondHeader bond={bond} />
      <CouponSchedule coupons={coupons} />
      <YieldDistributions yields={yields} />
    </div>
  );
}

Mutations: DALP lifecycle operations

Mutations modify state—creating vaults, executing DvP settlements, claiming yield. These operations often trigger smart contract transactions that require waiting for blockchain confirmation.

Creating a stablecoin vault with reserve configuration

Stablecoin issuance requires configuring a DALP vault that holds reserve collateral:

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function CreateStablecoinVault() {
  const queryClient = useQueryClient();

  const createVault = useMutation({
    mutationFn: orpc.vault.create.mutate,
    onSuccess: (result) => {
      toast.success(
        `Vault deployed at ${result.vaultAddress}. Transaction: ${result.transactionHash}`
      );

      // Invalidate vault list to refetch
      queryClient.invalidateQueries({
        queryKey: orpc.vault.list.getQueryKey(),
      });
    },
    onError: (error) => {
      toast.error(`Vault creation failed: ${error.message}`);
      console.error("Vault deployment error:", error);
    },
  });

  const handleCreate = () => {
    createVault.mutate({
      name: "PUSD Reserve Vault",
      collateralToken: "0x...", // USDC address
      minimumReserveRatio: 100, // 100% collateralization
      rebalanceThreshold: 95, // Alert at 95%
    });
  };

  return (
    <button onClick={handleCreate} disabled={createVault.isPending}>
      {createVault.isPending ? "Deploying Vault..." : "Create Stablecoin Vault"}
    </button>
  );
}

Executing DvP settlement with optimistic updates

DvP settlements lock assets atomically—buyer receives tokens only if seller receives payment in the same transaction. Optimistic updates provide instant UI feedback:

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function ExecuteDvPSettlement({
  settlementId,
  tokenAmount,
  paymentAmount,
}: {
  settlementId: string;
  tokenAmount: string;
  paymentAmount: string;
}) {
  const queryClient = useQueryClient();

  const executeSettlement = useMutation({
    mutationFn: orpc.settlement.execute.mutate,

    // Optimistically update settlement status before blockchain confirmation
    onMutate: async (variables) => {
      // Cancel outgoing refetches
      await queryClient.cancelQueries({
        queryKey: orpc.settlement.read.getQueryKey({
          input: { id: settlementId },
        }),
      });

      // Snapshot previous value
      const previousSettlement = queryClient.getQueryData(
        orpc.settlement.read.getQueryKey({ input: { id: settlementId } })
      );

      // Optimistically update status to "executing"
      queryClient.setQueryData(
        orpc.settlement.read.getQueryKey({ input: { id: settlementId } }),
        (old) => ({
          ...old!,
          status: "executing",
          executedAt: new Date().toISOString(),
        })
      );

      return { previousSettlement };
    },

    // Revert on error
    onError: (error, variables, context) => {
      if (context?.previousSettlement) {
        queryClient.setQueryData(
          orpc.settlement.read.getQueryKey({ input: { id: settlementId } }),
          context.previousSettlement
        );
      }
      toast.error(`DvP settlement failed: ${error.message}`);
    },

    // Always refetch after success to get final blockchain state
    onSuccess: (result) => {
      queryClient.invalidateQueries({
        queryKey: orpc.settlement.read.getQueryKey({
          input: { id: settlementId },
        }),
      });
      toast.success(
        `DvP settlement executed. Transaction: ${result.transactionHash}`
      );
    },
  });

  return (
    <button
      onClick={() =>
        executeSettlement.mutate({
          settlementId,
          tokenAmount,
          paymentAmount,
        })
      }
      disabled={executeSettlement.isPending}
    >
      {executeSettlement.isPending
        ? "Executing Settlement..."
        : "Execute DvP Settlement"}
    </button>
  );
}

Claiming yield with transaction lifecycle tracking

Yield claims withdraw accumulated dividends, interest, or coupon payments:

import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { usePublicClient } from "wagmi";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function ClaimYield({
  tokenId,
  availableYield,
}: {
  tokenId: string;
  availableYield: string;
}) {
  const [txHash, setTxHash] = useState<string | null>(null);
  const [txStatus, setTxStatus] = useState<
    "pending" | "confirming" | "success" | "failed" | null
  >(null);
  const queryClient = useQueryClient();
  const publicClient = usePublicClient();

  const claimYield = useMutation({
    mutationFn: orpc.yield.claim.mutate,
    onSuccess: async (result) => {
      setTxHash(result.transactionHash);
      setTxStatus("confirming");
      toast.info("Yield claim submitted, waiting for confirmation...");

      try {
        // Wait for blockchain confirmation
        const receipt = await publicClient.waitForTransactionReceipt({
          hash: result.transactionHash as `0x${string}`,
        });

        if (receipt.status === "success") {
          setTxStatus("success");
          toast.success(
            `Yield claimed: ${availableYield} tokens transferred to your wallet`
          );

          // Invalidate queries to refetch updated balances
          queryClient.invalidateQueries({
            queryKey: orpc.yield.available.getQueryKey({
              input: { tokenId },
            }),
          });
          queryClient.invalidateQueries({
            queryKey: orpc.token.read.getQueryKey({
              input: { id: tokenId },
            }),
          });
        } else {
          setTxStatus("failed");
          toast.error("Transaction reverted on-chain");
        }
      } catch (error) {
        setTxStatus("failed");
        toast.error("Transaction confirmation failed");
        console.error("Yield claim error:", error);
      }
    },
    onError: (error) => {
      setTxStatus("failed");
      toast.error(`Yield claim failed: ${error.message}`);
    },
  });

  return (
    <div>
      <button
        onClick={() => claimYield.mutate({ tokenId })}
        disabled={claimYield.isPending || txStatus === "confirming"}
      >
        {claimYield.isPending || txStatus === "confirming"
          ? "Processing Claim..."
          : `Claim ${availableYield} Yield`}
      </button>

      {txHash && (
        <div>
          <p>Transaction: {txHash}</p>
          <p>Status: {txStatus}</p>
          <a
            href={`https://etherscan.io/tx/${txHash}`}
            target="_blank"
            rel="noopener noreferrer"
          >
            View on Etherscan
          </a>
        </div>
      )}
    </div>
  );
}

Type safety and validation

ORPC enforces type contracts at compile time and runtime through Zod schemas.

Input validation prevents invalid requests

TypeScript catches invalid inputs before compilation:

// ✅ Valid - all required fields provided
orpc.vault.create.mutate({
  name: "PUSD Reserve Vault",
  collateralToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC
  minimumReserveRatio: 100,
  rebalanceThreshold: 95,
});

// ❌ Type error - missing required field 'collateralToken'
orpc.vault.create.mutate({
  name: "PUSD Reserve Vault",
  minimumReserveRatio: 100,
  rebalanceThreshold: 95,
});

// ❌ Type error - invalid reserve ratio (must be positive number)
orpc.vault.create.mutate({
  name: "PUSD Reserve Vault",
  collateralToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
  minimumReserveRatio: -10, // Negative ratio invalid
  rebalanceThreshold: 95,
});

Output types inferred from schemas

Response shapes are automatically typed:

const { data: settlement } = useSuspenseQuery(
  orpc.settlement.read.queryOptions({ input: { id: "0x123" } })
);

// TypeScript infers these properties from Zod output schema
settlement.id; // string
settlement.status; // 'pending' | 'executing' | 'completed' | 'failed'
settlement.buyer; // `0x${string}`
settlement.seller; // `0x${string}`
settlement.tokenAmount; // string (BigInt serialized)
settlement.paymentAmount; // string (BigInt serialized)
settlement.settledAt; // string (ISO timestamp) | null
settlement.transactionHash; // `0x${string}` | null

Discriminated unions for polymorphic data

Use type guards to narrow discriminated unions:

const { data: token } = useSuspenseQuery(
  orpc.token.read.queryOptions({ input: { id: tokenId } })
);

// TypeScript narrows based on token type
if (token.typeId === "bond") {
  // TypeScript knows bond-specific fields exist
  console.log(token.maturityDate); // string
  console.log(token.couponRate); // number
  console.log(token.faceValue); // string
} else if (token.typeId === "equity") {
  // TypeScript knows equity-specific fields exist
  console.log(token.dividendPolicy); // 'quarterly' | 'annual' | 'none'
  console.log(token.votingRights); // boolean
} else if (token.typeId === "fund") {
  // TypeScript knows fund-specific fields exist
  console.log(token.navPerShare); // string
  console.log(token.managementFee); // number (basis points)
}

Error handling strategies

ORPC standardizes error responses with typed error codes.

Catching and categorizing mutation errors

import { useMutation } from "@tanstack/react-query";
import { useRouter } from "@tanstack/react-router";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function CreateBond() {
  const router = useRouter();

  const createBond = useMutation({
    mutationFn: orpc.bond.create.mutate,
    onError: (error) => {
      // Categorize errors by type
      if (error.message.includes("UNAUTHORIZED")) {
        toast.error("Please log in to create bonds");
        router.navigate({ to: "/login" });
      } else if (error.message.includes("FORBIDDEN")) {
        toast.error(
          "You must complete KYC onboarding to create bonds. Redirecting..."
        );
        router.navigate({ to: "/onboarding" });
      } else if (error.message.includes("BAD_REQUEST")) {
        toast.error(
          "Invalid bond configuration. Check maturity date and coupon rate."
        );
      } else if (error.message.includes("INSUFFICIENT_BALANCE")) {
        toast.error("Insufficient gas to deploy bond contract");
      } else {
        toast.error("Bond creation failed. Please try again.");
        console.error("Unexpected bond creation error:", error);
      }
    },
  });

  return (
    <button
      onClick={() =>
        createBond.mutate({
          /* ... */
        })
      }
    >
      Create Bond
    </button>
  );
}

Handling Zod validation errors with field details

import { useMutation } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

// Utility to parse Zod validation errors
function parseValidationError(
  error: Error
): Array<{ field: string; message: string }> {
  try {
    const parsed = JSON.parse(error.message);
    if (parsed.issues) {
      return parsed.issues.map(
        (issue: { path: string[]; message: string }) => ({
          field: issue.path.join("."),
          message: issue.message,
        })
      );
    }
  } catch {
    // Not a Zod validation error
  }
  return [];
}

function CreateVault() {
  const createVault = useMutation({
    mutationFn: orpc.vault.create.mutate,
    onError: (error) => {
      const validationErrors = parseValidationError(error);

      if (validationErrors.length > 0) {
        // Display field-specific errors
        validationErrors.forEach(({ field, message }) => {
          toast.error(`${field}: ${message}`);
        });
      } else {
        toast.error(`Vault creation failed: ${error.message}`);
      }
    },
  });

  return <VaultCreationForm onSubmit={createVault.mutate} />;
}

Global error handling and observability

Configure global error handlers to log to observability systems:

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      onError: (error, variables, context) => {
        // Log to console in development
        if (import.meta.env.DEV) {
          console.error("Mutation error:", error);
          console.debug("Variables:", variables);
          console.debug("Context:", context);
        }

        // Send to error tracking service
        if (window.Sentry) {
          window.Sentry.captureException(error, {
            tags: {
              category: "mutation",
              procedure: context?.procedure || "unknown",
            },
            extra: { variables, context },
          });
        }

        // Track mutation failures in analytics
        if (window.analytics) {
          window.analytics.track("Mutation Failed", {
            error: error.message,
            procedure: context?.procedure,
          });
        }
      },
    },
    queries: {
      onError: (error) => {
        // Log query errors less aggressively (avoid noise)
        if (import.meta.env.DEV) {
          console.warn("Query error:", error);
        }

        // Only send critical query errors to Sentry
        if (error.message.includes("INTERNAL_SERVER_ERROR")) {
          window.Sentry?.captureException(error, {
            tags: { category: "query" },
          });
        }
      },
    },
  },
});

Authentication and authorization context

Procedures enforce authentication through router middleware—publicRouter allows anonymous access, authRouter requires login, onboardedRouter requires KYC completion.

Public routes with optional authentication

Public endpoints adapt behavior based on authentication state:

// Backend handler adapts to authentication context
export const getTokenStats = publicRouter.stats.token.handler(
  async ({ context, input }) => {
    if (context.auth) {
      // Authenticated user - return personalized stats
      return getPersonalizedTokenStats(input.tokenId, context.auth.user.id);
    } else {
      // Anonymous user - return public stats only
      return getPublicTokenStats(input.tokenId);
    }
  }
);

// Frontend usage - no special handling needed
function TokenStats({ tokenId }: { tokenId: string }) {
  const { data: stats } = useSuspenseQuery(
    orpc.stats.token.queryOptions({ input: { tokenId } })
  );

  return <StatsDisplay stats={stats} />;
}

Authenticated routes requiring login

Auth router procedures throw UNAUTHORIZED if session is missing:

// Backend handler assumes authentication
export const getUserPortfolio = authRouter.portfolio.get.handler(
  async ({ context }) => {
    // context.auth guaranteed to exist
    const userId = context.auth.user.id;
    return getPortfolioForUser(userId);
  }
);

// Frontend handles auth errors globally
function UserPortfolio() {
  const { data: portfolio } = useSuspenseQuery(
    orpc.portfolio.get.queryOptions()
    // Automatically includes session cookie
    // Throws UNAUTHORIZED if not logged in (caught by error boundary)
  );

  return <PortfolioDashboard portfolio={portfolio} />;
}

Onboarded routes requiring KYC completion

Onboarded router procedures verify wallet connection and OnchainID claims:

// Backend handler assumes full onboarding
export const createToken = onboardedRouter.token.create.handler(
  async ({ context, input }) => {
    // context.auth, context.wallet, context.system all guaranteed
    const issuerAddress = context.wallet.address;
    const identityClaims = context.userClaimTopics;

    // Verify issuer has required claims
    if (!identityClaims.includes("KYC_VERIFIED")) {
      throw errors.FORBIDDEN("KYC verification required");
    }

    return deployTokenContract(input, issuerAddress, context.system);
  }
);

// Frontend enforces onboarding through routing
function CreateTokenPage() {
  const createToken = useMutation({
    mutationFn: orpc.token.create.mutate,
    // Throws FORBIDDEN if user hasn't completed KYC
  });

  return <TokenCreationForm onSubmit={createToken.mutate} />;
}

Observability and performance monitoring

Integrate API calls with observability dashboards to track latency, success rates, and error patterns.

Tracking API latency metrics

Instrument queries and mutations to measure performance:

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function ExecuteDvPSettlement({ settlementId }: { settlementId: string }) {
  const queryClient = useQueryClient();

  const executeSettlement = useMutation({
    mutationFn: async (input) => {
      const startTime = performance.now();

      try {
        const result = await orpc.settlement.execute.mutate(input);
        const duration = performance.now() - startTime;

        // Track mutation latency
        if (window.analytics) {
          window.analytics.track("DvP Settlement Executed", {
            settlementId: input.settlementId,
            duration,
            status: "success",
          });
        }

        // Log to console in development
        if (import.meta.env.DEV) {
          console.log(`DvP settlement executed in ${duration.toFixed(2)}ms`);
        }

        return result;
      } catch (error) {
        const duration = performance.now() - startTime;

        // Track mutation failure
        if (window.analytics) {
          window.analytics.track("DvP Settlement Failed", {
            settlementId: input.settlementId,
            duration,
            status: "failed",
            error: error.message,
          });
        }

        throw error;
      }
    },
    onSuccess: (result) => {
      toast.success(
        `DvP settlement executed. Check the Settlement Times dashboard for confirmation latency.`
      );

      queryClient.invalidateQueries({
        queryKey: orpc.settlement.read.getQueryKey({
          input: { id: settlementId },
        }),
      });
    },
  });

  return (
    <button onClick={() => executeSettlement.mutate({ settlementId })}>
      Execute Settlement
    </button>
  );
}

Monitoring mutation success rates

Track mutation outcomes to identify problematic operations:

import { QueryClient } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: {
    mutations: {
      onSuccess: (data, variables, context) => {
        // Track successful mutation
        if (window.analytics) {
          window.analytics.track("Mutation Success", {
            procedure: context?.procedure || "unknown",
            timestamp: new Date().toISOString(),
          });
        }
      },
      onError: (error, variables, context) => {
        // Track failed mutation
        if (window.analytics) {
          window.analytics.track("Mutation Failed", {
            procedure: context?.procedure || "unknown",
            error: error.message,
            errorCode: extractErrorCode(error),
            timestamp: new Date().toISOString(),
          });
        }
      },
    },
  },
});

function extractErrorCode(error: Error): string {
  // Extract structured error code from ORPC error
  const match = error.message.match(/\[([A-Z_]+)\]/);
  return match ? match[1] : "UNKNOWN";
}

Referencing observability dashboards

Link API operations to specific dashboards for troubleshooting:

function VaultReserveMonitor({ vaultId }: { vaultId: string }) {
  const { data: reserves } = useSuspenseQuery(
    orpc.vault.reserves.queryOptions({ input: { id: vaultId } })
  );

  const ratio = (reserves.balance / reserves.totalSupply) * 100;

  if (ratio < 100) {
    return (
      <Alert variant="warning">
        Vault undercollateralized at {ratio.toFixed(2)}%.
        <a href="/observability/vaults" target="_blank">
          Check the Vault Reserve Ratio dashboard
        </a>{" "}
        for real-time metrics and historical trends.
      </Alert>
    );
  }

  return (
    <div>
      Vault Reserve Ratio: {ratio.toFixed(2)}%
      <a href="/observability/vaults" target="_blank">
        View Dashboard
      </a>
    </div>
  );
}

Common integration patterns

Infinite scroll for transaction history

Implement infinite scrolling with useInfiniteQuery:

import { useInfiniteQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function InfiniteTransactionHistory({ tokenId }: { tokenId: string }) {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: orpc.transaction.list.getQueryKey({ input: { tokenId } }),
      queryFn: async ({ pageParam }) => {
        const result = await orpc.transaction.list.query({
          input: {
            tokenId,
            offset: pageParam,
            limit: 20,
            orderBy: "timestamp",
            orderDirection: "desc",
          },
        });
        return result;
      },
      initialPageParam: 0,
      getNextPageParam: (lastPage, allPages) => {
        const totalFetched = allPages.reduce(
          (sum, page) => sum + page.items.length,
          0
        );
        return totalFetched < lastPage.total ? totalFetched : undefined;
      },
    });

  const allTransactions = data?.pages.flatMap((page) => page.items) ?? [];

  return (
    <div>
      <h2>Transaction History</h2>
      <ul>
        {allTransactions.map((tx) => (
          <li key={tx.id}>
            {tx.type}: {tx.amount} {tx.tokenSymbol} ({tx.timestamp})
          </li>
        ))}
      </ul>

      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? "Loading..." : "Load More"}
        </button>
      )}
    </div>
  );
}

Dependent queries for complex data relationships

Chain queries where one depends on another's output:

import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function VaultWithSettlements({ vaultId }: { vaultId: string }) {
  // First query: get vault details
  const { data: vault } = useSuspenseQuery(
    orpc.vault.read.queryOptions({ input: { id: vaultId } })
  );

  // Second query: get tokens managed by this vault
  const { data: tokens } = useSuspenseQuery(
    orpc.token.list.queryOptions({
      input: { vaultId: vault.id },
    })
  );

  // Third query: get DvP settlements for those tokens
  const { data: settlements } = useSuspenseQuery(
    orpc.settlement.list.queryOptions({
      input: {
        tokenIds: tokens.items.map((t) => t.id),
        limit: 50,
      },
    })
  );

  return (
    <div>
      <VaultHeader vault={vault} />
      <TokenList tokens={tokens.items} />
      <SettlementHistory settlements={settlements.items} />
    </div>
  );
}

Polling for real-time yield updates

Poll queries to display live yield accumulation:

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function LiveYieldAccumulation({ tokenId }: { tokenId: string }) {
  const { data: yield } = useQuery({
    ...orpc.yield.available.queryOptions({ input: { tokenId } }),
    refetchInterval: 10000, // Refetch every 10 seconds
  });

  if (!yield) return <p>Loading yield data...</p>;

  return (
    <div>
      <h3>Available Yield: {yield.amount}</h3>
      <p>Last Updated: {new Date(yield.lastCalculatedAt).toLocaleString()}</p>
      <p>
        Yield updates automatically every 10 seconds. Check the{" "}
        <a href="/observability/yield" target="_blank">
          Yield Distribution dashboard
        </a>{" "}
        for detailed metrics.
      </p>
    </div>
  );
}

Batching parallel requests for dashboards

TanStack Query automatically batches concurrent requests:

import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function ComprehensiveDashboard() {
  // These queries run in parallel automatically
  const userQuery = useSuspenseQuery(orpc.user.me.queryOptions());

  const vaultsQuery = useSuspenseQuery(
    orpc.vault.list.queryOptions({ input: { limit: 10 } })
  );

  const settlementsQuery = useSuspenseQuery(
    orpc.settlement.list.queryOptions({
      input: { status: "completed", limit: 20 },
    })
  );

  const yieldsQuery = useSuspenseQuery(
    orpc.yield.summary.queryOptions({ input: { userId: userQuery.data.id } })
  );

  return (
    <div>
      <UserProfile user={userQuery.data} />
      <VaultGrid vaults={vaultsQuery.data.items} />
      <RecentSettlements settlements={settlementsQuery.data.items} />
      <YieldSummary yields={yieldsQuery.data} />
    </div>
  );
}

End-to-end workflow: bond issuance with yield distribution

This scenario demonstrates a complete DALP lifecycle: issuing a bond, tracking coupon payments, and claiming yield.

Step 1: issue bond with coupon schedule

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function IssueBond() {
  const queryClient = useQueryClient();

  const issueBond = useMutation({
    mutationFn: orpc.bond.create.mutate,
    onSuccess: (result) => {
      toast.success(
        `Bond issued at ${result.tokenAddress}. Transaction: ${result.transactionHash}`
      );

      // Invalidate bond list
      queryClient.invalidateQueries({
        queryKey: orpc.bond.list.getQueryKey(),
      });
    },
  });

  const handleIssue = () => {
    issueBond.mutate({
      name: "Corporate Bond 2025",
      symbol: "CB2025",
      faceValue: "1000000000000000000000", // 1000 tokens (18 decimals)
      couponRate: 500, // 5.00% (basis points)
      maturityDate: "2025-12-31T23:59:59Z",
      couponFrequency: "quarterly", // Pay coupon every 3 months
      paymentToken: "0x...", // USDC address for coupon payments
    });
  };

  return (
    <button onClick={handleIssue} disabled={issueBond.isPending}>
      {issueBond.isPending ? "Issuing Bond..." : "Issue Bond"}
    </button>
  );
}

Step 2: monitor yield accumulation

import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function BondYieldTracker({ bondId }: { bondId: string }) {
  const { data: yield } = useQuery({
    ...orpc.yield.available.queryOptions({ input: { tokenId: bondId } }),
    refetchInterval: 30000, // Check every 30 seconds
  });

  const { data: nextCoupon } = useQuery(
    orpc.bond.nextCoupon.queryOptions({ input: { bondId } })
  );

  if (!yield || !nextCoupon) return <p>Loading yield data...</p>;

  return (
    <div>
      <h3>Bond Yield Tracker</h3>
      <p>Available to Claim: {yield.amount} USDC</p>
      <p>
        Next Coupon Payment:{" "}
        {new Date(nextCoupon.paymentDate).toLocaleDateString()}
      </p>
      <p>Next Coupon Amount: {nextCoupon.amount} USDC</p>
      <a href="/observability/yield" target="_blank">
        View Yield Distribution Dashboard
      </a>
    </div>
  );
}

Step 3: claim accumulated yield

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { usePublicClient } from "wagmi";
import { orpc } from "@/orpc/orpc-client";
import { toast } from "sonner";

function ClaimBondYield({
  bondId,
  availableYield,
}: {
  bondId: string;
  availableYield: string;
}) {
  const queryClient = useQueryClient();
  const publicClient = usePublicClient();

  const claimYield = useMutation({
    mutationFn: orpc.yield.claim.mutate,
    onSuccess: async (result) => {
      toast.info("Yield claim submitted, waiting for confirmation...");

      // Wait for blockchain confirmation
      const receipt = await publicClient.waitForTransactionReceipt({
        hash: result.transactionHash as `0x${string}`,
      });

      if (receipt.status === "success") {
        toast.success(
          `Yield claimed: ${availableYield} USDC transferred to your wallet`
        );

        // Invalidate queries
        queryClient.invalidateQueries({
          queryKey: orpc.yield.available.getQueryKey({
            input: { tokenId: bondId },
          }),
        });
        queryClient.invalidateQueries({
          queryKey: orpc.yield.history.getQueryKey({
            input: { tokenId: bondId },
          }),
        });
      } else {
        toast.error("Transaction reverted");
      }
    },
    onError: (error) => {
      toast.error(`Yield claim failed: ${error.message}`);
    },
  });

  return (
    <button
      onClick={() => claimYield.mutate({ tokenId: bondId })}
      disabled={claimYield.isPending}
    >
      {claimYield.isPending
        ? "Claiming Yield..."
        : `Claim ${availableYield} USDC`}
    </button>
  );
}

Step 4: view complete yield history

import { useSuspenseQuery } from "@tanstack/react-query";
import { orpc } from "@/orpc/orpc-client";

function BondYieldHistory({ bondId }: { bondId: string }) {
  const { data: history } = useSuspenseQuery(
    orpc.yield.history.queryOptions({
      input: {
        tokenId: bondId,
        limit: 50,
        orderBy: "claimedAt",
        orderDirection: "desc",
      },
    })
  );

  const totalClaimed = history.claims.reduce(
    (sum, claim) => sum + BigInt(claim.amount),
    0n
  );

  return (
    <div>
      <h3>Yield Claim History</h3>
      <p>Total Claimed: {totalClaimed.toString()} USDC</p>

      <table>
        <thead>
          <tr>
            <th>Date</th>
            <th>Amount</th>
            <th>Transaction</th>
          </tr>
        </thead>
        <tbody>
          {history.claims.map((claim) => (
            <tr key={claim.id}>
              <td>{new Date(claim.claimedAt).toLocaleString()}</td>
              <td>{claim.amount} USDC</td>
              <td>
                <a
                  href={`https://etherscan.io/tx/${claim.transactionHash}`}
                  target="_blank"
                  rel="noopener noreferrer"
                >
                  {claim.transactionHash.slice(0, 10)}...
                </a>
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Query and mutation workflow diagram

Rendering chart...

This diagram shows how queries cache data and refetch in the background, while mutations optimistically update the UI, execute the operation, and invalidate affected queries on success.

Next steps

  • Explore procedure middleware: Understand authentication layers in kit/dapp/src/orpc/procedures/README.md
  • Review route schemas: Check kit/dapp/src/orpc/routes/*/routes/*.schema.ts for input/output types
  • Study handler implementations: Read handlers in kit/dapp/src/orpc/routes/*/routes/*.ts for business logic patterns
  • Reference API endpoints: See all available routes in API Reference
  • Understand data models: Learn about backing schemas in Data Model Reference
  • Monitor API performance: Use Observability & Monitoring to track API latency and success rates
Contract reference
API reference
llms-full.txt

On this page

ORPC architecture for DALP operationsWhy this architectureClient setup and isomorphic executionServer-side execution in loadersClient-side execution in componentsQueries: fetching DALP stateBasic vault query with automatic type inferencePaginated DvP settlement historyConditional queries based on user statePrefetching related data in loadersMutations: DALP lifecycle operationsCreating a stablecoin vault with reserve configurationExecuting DvP settlement with optimistic updatesClaiming yield with transaction lifecycle trackingType safety and validationInput validation prevents invalid requestsOutput types inferred from schemasDiscriminated unions for polymorphic dataError handling strategiesCatching and categorizing mutation errorsHandling Zod validation errors with field detailsGlobal error handling and observabilityAuthentication and authorization contextPublic routes with optional authenticationAuthenticated routes requiring loginOnboarded routes requiring KYC completionObservability and performance monitoringTracking API latency metricsMonitoring mutation success ratesReferencing observability dashboardsCommon integration patternsInfinite scroll for transaction historyDependent queries for complex data relationshipsPolling for real-time yield updatesBatching parallel requests for dashboardsEnd-to-end workflow: bond issuance with yield distributionStep 1: issue bond with coupon scheduleStep 2: monitor yield accumulationStep 3: claim accumulated yieldStep 4: view complete yield historyQuery and mutation workflow diagramNext steps