Backend proxy
This Next.js route validates the fields your UI uses and forwards them to AKKA. Add your own session, origin, and rate-limit checks before production.app/api/akka/quote/route.ts
import { NextRequest, NextResponse } from 'next/server';
const AKKA_API = 'https://api.akka.finance';
const ALLOWED = ['src', 'dst', 'amount', 'payer', 'recipient', 'slippage'];
export async function GET(request: NextRequest) {
const chainId = request.nextUrl.searchParams.get('chainId');
if (!chainId || !/^\d+$/.test(chainId)) {
return NextResponse.json({ message: 'Invalid chainId' }, { status: 400 });
}
const params = new URLSearchParams();
for (const key of ALLOWED) {
const value = request.nextUrl.searchParams.get(key);
if (value) params.set(key, value);
}
const response = await fetch(
`${AKKA_API}/partner/v1/${chainId}/quote?${params}`,
{
headers: { apikey: process.env.AKKA_API_KEY! },
cache: 'no-store',
signal: AbortSignal.timeout(10_000),
},
);
return new NextResponse(await response.text(), {
status: response.status,
headers: { 'content-type': 'application/json' },
});
}
Never use
NEXT_PUBLIC_AKKA_API_KEY. Executable quotes can contain signed RFQ
calldata; do not cache responses in your proxy, service worker, CDN, or
application state beyond their immediate UI lifecycle.Shared response type
lib/akka.ts
export type PartnerQuote = {
quoteId: string;
buyAmount: string;
minAmountOut: string;
sellToken: `0x${string}`;
sellAmount: string;
allowanceTarget: `0x${string}` | null;
expiresAt: string;
issues: Array<{ code: string; message: string }>;
validation: Record<'balance' | 'allowance' | 'gas', { status: string }>;
simulation: { status: string; gas: string };
tx: {
to: `0x${string}`;
data: `0x${string}`;
value: string;
gas: string;
gasPrice: string;
type?: 'legacy' | 'eip1559';
maxFeePerGas?: string;
maxPriorityFeePerGas?: string;
};
};
export async function fetchPartnerQuote(input: {
chainId: number;
src: string;
dst: string;
amount: string;
payer: string;
recipient?: string;
slippage: number;
signal?: AbortSignal;
}): Promise<PartnerQuote> {
const { chainId, signal, ...query } = input;
const response = await fetch(
`/api/akka/quote?${new URLSearchParams({
...query,
recipient: query.recipient ?? query.payer,
slippage: String(query.slippage),
})}`,
{ cache: 'no-store', signal },
);
const body = await response.json();
if (!response.ok) throw new Error(body.message ?? `AKKA ${response.status}`);
return body;
}
useAkkaQuote
This hook debounces display quotes. Always request a fresh quote again when the user confirms execution.
hooks/useAkkaQuote.ts
import { useEffect, useState } from 'react';
import { fetchPartnerQuote, type PartnerQuote } from '../lib/akka';
export function useAkkaQuote(input: {
chainId: number;
src: string;
dst: string;
amount: string;
payer?: string;
slippage?: number;
}) {
const [quote, setQuote] = useState<PartnerQuote | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!input.payer || !input.amount || input.amount === '0') {
setQuote(null);
return;
}
const controller = new AbortController();
const timer = setTimeout(async () => {
setLoading(true);
try {
setQuote(
await fetchPartnerQuote({
...input,
payer: input.payer!,
slippage: input.slippage ?? 1,
signal: controller.signal,
}),
);
setError(null);
} catch (error) {
if (!controller.signal.aborted) {
setQuote(null);
setError(error instanceof Error ? error.message : 'Quote failed');
}
} finally {
if (!controller.signal.aborted) setLoading(false);
}
}, 400);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [
input.chainId,
input.src,
input.dst,
input.amount,
input.payer,
input.slippage,
]);
return { quote, loading, error };
}
useAkkaSwap
The execution hook requests a fresh quote on click, approves the exact AKKA allowanceTarget if required, then refreshes before broadcasting.
hooks/useAkkaSwap.ts
import { useCallback, useState } from 'react';
import { erc20Abi } from 'viem';
import {
useAccount,
usePublicClient,
useSendTransaction,
useWriteContract,
} from 'wagmi';
import { fetchPartnerQuote } from '../lib/akka';
type Status =
| 'idle'
| 'quoting'
| 'approving'
| 'swapping'
| 'confirming'
| 'done'
| 'error';
export function useAkkaSwap(input: {
chainId: number;
src: `0x${string}`;
dst: `0x${string}`;
amount: string;
recipient?: `0x${string}`;
slippage?: number;
}) {
const { address } = useAccount();
const publicClient = usePublicClient();
const { sendTransactionAsync } = useSendTransaction();
const { writeContractAsync } = useWriteContract();
const [status, setStatus] = useState<Status>('idle');
const [txHash, setTxHash] = useState<`0x${string}`>();
const [error, setError] = useState<string>();
const execute = useCallback(async () => {
if (!address || !publicClient || !input.amount || input.amount === '0')
return;
try {
setError(undefined);
setStatus('quoting');
const request = () =>
fetchPartnerQuote({
...input,
payer: address,
recipient: input.recipient ?? address,
slippage: input.slippage ?? 1,
});
let quote = await request();
if (
quote.issues.some((issue) => issue.code === 'INSUFFICIENT_ALLOWANCE')
) {
if (!quote.allowanceTarget) throw new Error('Missing allowance target');
setStatus('approving');
const approvalHash = await writeContractAsync({
address: quote.sellToken,
abi: erc20Abi,
functionName: 'approve',
args: [quote.allowanceTarget, BigInt(quote.sellAmount)],
});
await publicClient.waitForTransactionReceipt({ hash: approvalHash });
quote = await request();
}
if (quote.issues.length) throw new Error(quote.issues[0].message);
if (
Object.values(quote.validation).some(
(check) => check.status === 'UNKNOWN',
)
) {
throw new Error('AKKA could not complete payer validation');
}
setStatus('swapping');
const feeFields =
quote.tx.type === 'eip1559'
? {
maxFeePerGas: BigInt(quote.tx.maxFeePerGas!),
maxPriorityFeePerGas: BigInt(quote.tx.maxPriorityFeePerGas!),
}
: { gasPrice: BigInt(quote.tx.gasPrice) };
const hash = await sendTransactionAsync({
to: quote.tx.to,
data: quote.tx.data,
value: BigInt(quote.tx.value),
gas: BigInt(quote.tx.gas),
...feeFields,
});
setTxHash(hash);
setStatus('confirming');
await publicClient.waitForTransactionReceipt({ hash });
setStatus('done');
} catch (error) {
setError(error instanceof Error ? error.message : 'Swap failed');
setStatus('error');
}
}, [address, publicClient, sendTransactionAsync, writeContractAsync, input]);
return { execute, status, txHash, error };
}
