Early Withdrawal
Users can redeem an AI trading order before the investment cycle ends. When they do, an early withdrawal penalty is applied based on how much of the cycle has elapsed. This page explains the penalty formula, the redemption API, and the result structure.
How Penalties Work
The penalty is designed to discourage premature withdrawals while still allowing users to exit if needed. The earlier you withdraw, the higher the penalty.
Penalty decreases as cycle progresses:
100% ┐
│ ████
75% │ ████████
│ ████████████
50% │ ████████████████
│ ████████████████████
25% │ ████████████████████████
│ ████████████████████████████
0% └────────────────────────────────
0% 25% 50% 75% 100%
Cycle Completion
Penalty Formula
The penalty is calculated as a percentage of the gross profit (not the total capital). If the order is at a loss, no penalty is applied to the loss portion.
completionRate = daysElapsed / cycleDays
penaltyPercent = maxPenaltyPercent * (1 - completionRate)
penaltyAmount = max(0, grossProfit) * penaltyPercent
netPayout = finalNav - penaltyAmount
Where:
| Variable | Description |
|---|---|
completionRate | Fraction of the cycle that has elapsed (0 to 1) |
daysElapsed | Number of days since the order was activated |
cycleDays | Total lock period in days |
maxPenaltyPercent | Maximum penalty percentage, typically 30% (0.30) |
grossProfit | finalNav - investedAmount (can be negative) |
penaltyAmount | Absolute dollar amount deducted as penalty |
netPayout | Amount returned to the user |
If the order is at a loss (grossProfit <= 0), no penalty is applied. The user receives the full finalNav regardless of cycle completion. The penalty only applies to the profit portion.
Penalty Examples
The table below shows the penalty for a $1,000 investment with a $200 gross profit ($1,200 final NAV) across different completion rates, using a 30% max penalty.
| Completion Rate | Days Elapsed (30-day cycle) | Penalty % | Penalty Amount | Net Payout |
|---|---|---|---|---|
| 0% (day 0) | 0 | 30% | $60.00 | $1,140.00 |
| 25% (day 7.5) | 7.5 | 22.5% | $45.00 | $1,155.00 |
| 50% (day 15) | 15 | 15% | $30.00 | $1,170.00 |
| 75% (day 22.5) | 22.5 | 7.5% | $15.00 | $1,185.00 |
| 90% (day 27) | 27 | 3% | $6.00 | $1,194.00 |
| 100% (day 30) | 30 | 0% | $0.00 | $1,200.00 |
If the same order were at a loss (say $950 final NAV, -$50 gross profit):
| Completion Rate | Penalty % | Penalty Amount | Net Payout |
|---|---|---|---|
| Any | 0% | $0.00 | $950.00 |
Redeeming an Order
Use the redeemAIOrder method to perform an early withdrawal. This method works on orders with active or paused status.
import { OneEngineClient, setAITradingAccessToken } from '@one_deploy/sdk';
setAITradingAccessToken(userToken);
const engine = new OneEngineClient({
apiKey: process.env.ONE_API_KEY!,
projectId: process.env.ONE_PROJECT_ID!,
});
const result = await engine.redeemAIOrder('order_xyz789');
console.log('Final NAV:', result.finalNav);
console.log('Gross profit:', result.grossProfit);
console.log('Completion rate:', (result.completionRate * 100).toFixed(1) + '%');
console.log('Penalty:', result.penaltyPercent + '%', '=', '$' + result.penaltyAmount.toFixed(2));
console.log('Net payout:', '$' + result.netPayout.toFixed(2));
console.log('Payout tx:', result.transactionHash);
Method Signature
redeemAIOrder(orderId: string): Promise<AIRedemptionResult>
AIRedemptionResult Type
interface AIRedemptionResult {
/** The updated order object with status 'redeemed'. */
order: AIOrder;
/** NAV at the time of redemption. */
finalNav: number;
/** Gross profit before penalty (finalNav - invested amount). */
grossProfit: number;
/** Penalty amount deducted for early withdrawal. 0 if the cycle completed or at a loss. */
penaltyAmount: number;
/** Penalty percentage applied (0-30). */
penaltyPercent: number;
/** Net amount paid out to the user (finalNav - penaltyAmount). */
netPayout: number;
/** Wallet address the payout was sent to. */
payoutAddress: string;
/** On-chain transaction hash for the payout. */
transactionHash: string;
/** Completion rate at the time of redemption (0 to 1). */
completionRate: number;
/** ISO 8601 timestamp of the redemption. */
redeemedAt: string;
}
Early Withdrawal Confirmation UI
Before redeeming, you should show users a preview of the penalty they will incur. Here is an example confirmation screen.
import { useState, useEffect } from 'react';
import { OneEngineClient, DEFAULT_SHARE_RATES } from '@one_deploy/sdk';
import type { AIOrder, AIRedemptionResult } from '@one_deploy/sdk';
const MAX_PENALTY_PERCENT = 0.30;
interface WithdrawalConfirmProps {
engine: OneEngineClient;
order: AIOrder;
onComplete: (result: AIRedemptionResult) => void;
}
function WithdrawalConfirm({ engine, order, onComplete }: WithdrawalConfirmProps) {
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Calculate estimated penalty
const now = new Date();
const created = new Date(order.createdAt);
const expires = new Date(order.expiresAt);
const totalDays = (expires.getTime() - created.getTime()) / (1000 * 60 * 60 * 24);
const elapsedDays = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24);
const completionRate = Math.min(elapsedDays / totalDays, 1);
const grossProfit = order.currentNav - order.amount;
const penaltyPercent = grossProfit > 0
? MAX_PENALTY_PERCENT * (1 - completionRate)
: 0;
const penaltyAmount = Math.max(0, grossProfit) * penaltyPercent;
const estimatedPayout = order.currentNav - penaltyAmount;
const handleRedeem = async () => {
setSubmitting(true);
setError(null);
try {
const result = await engine.redeemAIOrder(order.id);
onComplete(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'Redemption failed');
} finally {
setSubmitting(false);
}
};
return (
<View style={styles.container}>
<Text style={styles.heading}>Early Withdrawal</Text>
<Text style={styles.warning}>
You are redeeming before the cycle ends. A penalty may apply.
</Text>
<View style={styles.details}>
<DetailRow label="Invested" value={`$${order.amount.toFixed(2)}`} />
<DetailRow label="Current NAV" value={`$${order.currentNav.toFixed(2)}`} />
<DetailRow label="Gross Profit" value={`$${grossProfit.toFixed(2)}`} />
<DetailRow
label="Cycle Progress"
value={`${(completionRate * 100).toFixed(1)}% (${elapsedDays.toFixed(0)} / ${totalDays.toFixed(0)} days)`}
/>
<DetailRow
label="Penalty"
value={`${(penaltyPercent * 100).toFixed(1)}% = $${penaltyAmount.toFixed(2)}`}
/>
<View style={styles.divider} />
<DetailRow
label="Estimated Payout"
value={`$${estimatedPayout.toFixed(2)}`}
bold
/>
</View>
{error && <Text style={styles.error}>{error}</Text>}
<Pressable
onPress={handleRedeem}
disabled={submitting}
style={styles.redeemButton}
>
<Text style={styles.redeemButtonText}>
{submitting ? 'Processing...' : 'Confirm Withdrawal'}
</Text>
</Pressable>
<Text style={styles.disclaimer}>
Final amounts are calculated at the time of execution and may differ
slightly from the estimates shown above due to NAV changes.
</Text>
</View>
);
}
function DetailRow({
label,
value,
bold,
}: {
label: string;
value: string;
bold?: boolean;
}) {
return (
<View style={styles.detailRow}>
<Text style={bold ? styles.boldLabel : styles.label}>{label}</Text>
<Text style={bold ? styles.boldValue : styles.value}>{value}</Text>
</View>
);
}
Key Points
- Penalties only apply to profits, never to the original capital.
- If an order is at a loss, the full current NAV is returned with no penalty.
- The maximum penalty is 30% of gross profit, applied when the completion rate is 0%.
- The penalty decreases linearly as the cycle progresses toward completion.
- At 100% completion (cycle end), the penalty is 0% -- the order completes normally.
- Penalties are calculated at the moment of redemption -- the values shown in a preview are estimates.
Next Steps
- Strategy Management -- pause or resume orders instead of redeeming.
- Portfolio Dashboard -- view redeemed orders alongside active ones.
- Investment Cycles -- review lock period and share rate details.