Fee Sharing
Earn a share of every deposit you route into Upshift vaults, using origin codes on the Universal Adapter.
If you route user deposits into Upshift vaults from your own app, you can take a fee on top of those deposits. The Universal Adapter has this built in: Upshift registers an origin code for you, you pass that code on every deposit call, and the adapter sends your fee straight to a wallet you control in the same transaction.
There is nothing to deploy, no wrapper contract to write, and no invoicing. Your fee settles on-chain at deposit time.
How it works
An origin code is a 32-byte identifier. On-chain it maps to two values: a fee in basis points, and the address that collects that fee.
mapping(bytes32 => OriginEntry) public origins;
struct OriginEntry {
uint256 originFee; // basis points
address originFeeCollector; // your wallet
}When a deposit arrives carrying your code, the adapter:
- Takes
originFeebps off the incoming deposit amount. - Transfers that amount to your
originFeeCollector. - Deposits the remainder into the vault and mints shares to the end user.
The fee comes out of the deposit, so the user receives shares for the net amount. Price this into whatever you show them before they sign.
Passing the all-zero code (bytes32(0)) means no origin fee — that is the
default for anyone who has not registered.
Fee limits
| Value | On-chain source | Current |
|---|---|---|
| Basis-point denominator | BPS_DENOMINATOR() | 10000 |
| Maximum origin fee | MAX_ORIGIN_FEE_BPS() | 7000 |
7000 bps is the contract's hard ceiling, not a recommendation. Agree a realistic
number with us — it is visible on-chain and it reduces the user's deposit.
Two fee layers
Each enabled vault also carries an Upshift swap fee (vaultInfo(vault).swapFee,
capped by MAX_SWAP_FEE_BPS()), applied on the swap paths. Your origin fee is
separate and additive. Read both before you quote a net deposit amount to a user.
Getting a code
addOrigin is owner-only, so codes are registered by Upshift — you cannot
self-register. Send us three things:
| What | Notes |
|---|---|
| A short label | e.g. acme-app. Becomes your bytes32 code. |
| Your fee, in bps | 100 = 1%. Must be ≤ MAX_ORIGIN_FEE_BPS(). |
| Your collector address | Receives the fee. Must be able to hold arbitrary ERC-20s. |
Reach us at support@upshift.finance. Registration is a single transaction on our side.
Once registered we can change your fee or collector (updateOrigin) or retire the
code (revokeOrigin) without you shipping anything — your integration keeps
passing the same code.
Deriving your code
By convention a code is its label as UTF-8 bytes, right-padded with zeros. Derive it yourself rather than hard-coding a hex blob:
import { stringToHex } from 'viem';
const ORIGIN_CODE = stringToHex('acme-app', { size: 32 });
// 0x61636d652d617070000000000000000000000000000000000000000000000000With ethers v6:
import { encodeBytes32String } from 'ethers';
const ORIGIN_CODE = encodeBytes32String('acme-app');Labels are capped at 31 bytes with encodeBytes32String (it reserves a
terminator) and 32 bytes with stringToHex. Confirm the exact code with us after
registration — the contract stores raw bytes and treats an unregistered code as
invalid.
Passing the code
Every deposit entry point on the adapter takes originCode as its first argument:
| Function | Use for |
|---|---|
deposit(bytes32 originCode, uint256 depositAmount, address vaultAddr, address assetAddr, address receiverAddr) | Depositing the vault's reference asset directly. |
depositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr) | Native ETH into a wrapped-native vault. payable. |
swapAndDeposit(bytes32 originCode, address vaultAddr, address receiverAddr, SwapParams[] swapParams) | Any whitelisted ERC-20, swapped to the reference asset en route. |
swapAndDepositNativeToken(bytes32 originCode, address vaultAddr, address receiverAddr) | Native ETH into a non-wrapped-native vault. payable. |
Each returns the shares minted to receiverAddr.
Via the SDK
The SDK picks the right entry point from the asset you pass, so you call one method for all four paths:
import AugustSDK from '@augustdigital/sdk';
import { stringToHex } from 'viem';
const sdk = new AugustSDK({ appName: '<APP_NAME>' });
sdk.evm.setSigner(signer);
const txHash = await sdk.evm.swapRouterDeposit({
chainId: 1,
vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
depositAsset: '0xdAC17F958D2ee523a2206206994597C13D831ec7', // USDT
amount: '1000',
originCode: stringToHex('acme-app', { size: 32 }),
slippageBps: 50,
});swapRouterDeposit reads the vault's reference asset on-chain, chooses the
direct / swap / native path, fetches and pins the swap quote, handles the ERC-20
approval, and forwards your originCode on whichever path it takes. Omit
originCode and it falls back to the code set on the SDK instance (see
below), then to the zero sentinel.
Requires an SDK release newer than 8.24.0. Up to and including 8.24.0,
swapRouterDepositacceptsoriginCodebut drops it before dispatch, so deposits settle on the zero code and no fee is collected. On an older version, callswapAndDeposit,depositViaSwapRouter, ordepositNativeViaSwapRouterdirectly instead — those have always forwarded it.
Reading a code's settings
const [originFee, originFeeCollector] = await adapter.origins(ORIGIN_CODE);An unregistered code returns a zero fee and the zero address.
Setting the code on the SDK instance
Instead of passing originCode on every call, set it once when you construct
the SDK:
const sdk = new AugustSDK({
appName: '<APP_NAME>',
attribution: {
originCode: stringToHex('acme-app', { size: 32 }),
strictOrigin: true,
},
});originCode must be the full 32-byte code; a malformed value throws from the
constructor. strictOrigin is optional and defaults to false. The config is
process-global: the last AugustSDK constructed in a process wins.
A deposit carries, in order of precedence:
- The
originCodepassed on the call (the router methods take one;vaultDepositdoes not). - The instance's
attribution.originCode. - The zero sentinel — no origin fee.
When vaultDeposit uses the router
With a code configured, vaultDeposit deposits through SwapRouter.deposit
carrying the code — a direct deposit, never a swap — only when all of these hold:
- The vault is on Ethereum mainnet and registered on the router.
- The vault accepts the deposit asset directly, and the router whitelists it.
- It is not a permit, native-token, adapter, OVault, Solana, Stellar, or Sui deposit.
Any other deposit takes the vault's native deposit path and the SDK logs a
vaultDeposit:attribution-skipped warning with the reason. With
strictOrigin: true, a code that is not registered on the router throws
AugustValidationError before any transaction instead. With no code
configured, nothing changes.
swapRouterDeposit, depositViaSwapRouter, depositNativeViaSwapRouter, and
swapAndDeposit also use the instance code on mainnet, after checking that it
is registered. An unregistered code passed explicitly on one of these calls
throws before any approval.
Approvals
On the router route the spender is the SwapRouter, not the vault. vaultDeposit
approves it for you; if you manage approvals yourself, approve the spender
that getDepositRoute returns:
const { via, spender } = await sdk.evm.getDepositRoute({ target: vault, depositAsset });via is 'router' or 'native'; spender is null for a native-token deposit.
Gas
Measured on a mainnet fork, 1,000 USDC into the Core USDC vault. Each token also needs a one-time approval of the router as a new spender.
| Route | Gas | vs direct |
|---|---|---|
| Direct vault deposit | ~102k | — |
| Router, zero-fee code | ~162k | +~60k |
| Router, 1% code | ~173k | +~71k |
Zero-fee codes and operators
A zero-fee code is valid, but the router emits OriginFeeApplied only when it
takes a fee. Attribution-only partners on a zero-fee code cannot be reconciled
from that event.
The router owner registers codes with addOrigin, in Crest at
/swap-router-config. The origin fee is taken from the deposit amount, so a
dust deposit below the fee floor reverts with OriginFeeTooLow.
Prerequisites
Fee sharing rides on the adapter, so the adapter's constraints are yours too.
- Ethereum and HyperEVM only. The Universal Adapter is deployed at
0xAC771209FF2b71EECfF6E85a9AD01db8Ff2618B0on Ethereum and0xF47ce4B8df30bc3B3b6cc21Fe1552A2198A4A4F7on HyperEVM. HyperEVM has no swap route yet, so codes there apply todepositanddepositNativeTokenonly. - The vault must be enabled on the adapter. Check with
vaultInfo(vault)— areferenceAssetof the zero address means it is not registered, and deposits revert withInvalidVault. Enabling a vault is a config call on our side; ask us which vaults you need. - Deposit tokens must be whitelisted. Both the token in and the token out of
a swap leg. Non-whitelisted tokens revert with
InputTokenNotWhitelisted/OutputTokenNotWhitelisted. - Swap legs are capped at
MAX_SWAPS()(currently9) per call. - Deposits only. The deployed adapter handles deposits and swap-then-deposit. Redemptions go through the vault directly — see the vault interface.
Reporting on your fees
Every applied fee emits an event, indexed by origin code, so you can reconcile independently of anything Upshift reports:
event OriginFeeApplied(
address indexed vaultAddr,
address indexed assetAddr,
uint256 consumableAmount, // deposit amount the fee was taken from
uint256 feeAmount, // what your collector received
bytes32 indexed originCode
);Filter on originCode for your whole history. OriginAdded, OriginUpdated, and
OriginRevoked give you the audit trail of your code's configuration.
The adapter itself emits no deposit event — the vault does. To resolve what a given deposit actually produced (the real post-swap amount, which differs from the pre-trade quote), use the SDK:
const result = await sdk.evm.getSwapRouterDepositResult({
txHash,
vault: '0x74ad2f789ed583dbd141bbdafc673fe1f033718b',
});
// result?.amountOut — reference asset that actually reached the vault
// result?.shares — shares minted to the receiverErrors
| Error | Cause |
|---|---|
InvalidOrigin | The code is not registered, or was revoked. |
OriginAlreadyExists | addOrigin on a code that already exists — use updateOrigin. |
OriginFeeTooHigh | Requested fee exceeds MAX_ORIGIN_FEE_BPS(). |
OriginFeeTooLow | The deposit is small enough that the origin fee would round to zero. |
InvalidFeeCollector | The collector address is not usable. |
InvalidVault | The vault is not enabled on the adapter. |
InputTokenNotWhitelisted | The deposit token is not whitelisted. |
OutputTokenNotWhitelisted | A swap leg's output token is not whitelisted. |
TooManySwaps | More than MAX_SWAPS() legs in one call. |
SlippageError | A swap leg produced less than its minAmountOut. |
ContractIsPaused | The adapter is paused. |
The Universal Adapter reference covers the wider function, event, and error surface — note the accuracy caveat at the top of that page, and treat the signatures here as the verified ones for deposits.
Universal Adapter
Full reference for the Universal Adapter (SwapRouter) — the single entry point for atomically swapping any whitelisted token and depositing the proceeds into an Upshift vault.
Deployed Contracts
Registry of Upshift protocol contract addresses and live vaults across every supported chain, consolidated from the protocol docs and the live vault API.