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.
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}` | nullDiscriminated 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
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.tsfor input/output types - Study handler implementations: Read handlers in
kit/dapp/src/orpc/routes/*/routes/*.tsfor 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