Portal’s Web SDK provides token delegation capabilities through the portal.delegations API. This enables approving token spending, revoking approvals, checking delegation status, and transferring tokens as a delegate on both EVM and Solana chains.
Overview
The delegations functionality allows you to:
- Approve other addresses to spend tokens on behalf of your wallet
- Revoke existing delegations to remove spending permissions
- Check status of active delegations and balances
- Transfer tokens as a delegate from another address
- Approve, revoke, and transfer in one step using the high-level submit helpers (
approveAndSubmit, revokeAndSubmit, transferAndSubmit)
Prerequisites
Before using delegation operations, ensure you have:
Delegations apply to ERC-20 tokens (EVM) and SPL Tokens (Solana) only. Native assets like ETH, MON, and SOL cannot be delegated — they have no on-chain approve / transferFrom (or SPL delegate) semantics. Calls using a native asset identifier will be rejected. See Delegations for the protocol-level reason and workarounds.
High-Level Methods
These methods provide auto-submit behavior. For most use cases, these helpers are the recommended starting point. They call the matching delegation API method, then sign and broadcast each returned transaction in order — all in a single call.
The low-level methods (approve, revoke, transferFrom) only build delegation transactions via the API. You receive EVM transaction objects or Solana-encoded payloads and must call portal.request with the correct method yourself (eth_sendTransaction or sol_signAndSendTransaction). The high-level methods handle that signing and submission step for you, resolving to transaction hashes only — they do not wait for on-chain confirmation.
When you use the standard Portal client, portal.delegations is automatically wired with a default signAndSendTransaction implementation that picks eth_sendTransaction vs sol_signAndSendTransaction based on whether the chain id starts with solana.
The signer is resolved in priority order:
- Per-call override —
options.signAndSendTransaction when provided
- Instance-level default — configured on
Portal (automatic when you construct Portal)
- Error — thrown if neither is available:
[Delegations] No signer configured. Call setSignAndSendTransaction() on the instance or pass signAndSendTransaction in options.
approveAndSubmit
Overview
Calls approve, then signs and sends every transaction in the response. Use it when you want a single step from approval intent to submitted transactions instead of manually calling portal.request after approve.
Example
// Using Portal — no signAndSendTransaction needed (auto-configured)
const { hashes } = await portal.delegations.approveAndSubmit(
{
chain: 'eip155:11155111',
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
amount: '10',
},
{
onProgress: (event) => console.log(event),
},
)
console.log('Submitted:', hashes)
Parameters
| Name | Type | Required | Description |
|---|
params | object | Yes | Same shape as approve. |
params.chain | string | Yes | Chain CAIP ID (EVM eip155:… or Solana solana:…). |
params.token | string | Yes | Token symbol for the delegations API. |
params.delegateAddress | string | Yes | Address allowed to spend on your behalf. |
params.amount | string | Yes | Approval amount as a positive decimal string. |
options | object | No | Optional callbacks and configuration. |
options.signAndSendTransaction | function | No | Per-call signer override. Auto-configured by Portal when using the default client. |
options.onProgress | (event) => void | No | See Progress events below. |
Progress events (when onProgress is set): the callback is invoked twice per transaction — once with step: 'signing' (fields: index, total) before signing begins, and once with step: 'submitted' (fields: index, total, hash) after signAndSendTransaction resolves. For a batch of n transactions, onProgress is called 2n times in total.
Returns
{ hashes: string[] } — One entry per submitted transaction, in the same order as transactions (EVM) or encodedTransactions (Solana) from the API. Each value is the hash returned by signAndSendTransaction for that item.
Notes / Gotchas
- Transactions are submitted sequentially (not batched in one RPC call). If the API returns multiple payloads, the second is only sent after the first send resolves.
- The method returns as soon as transactions are submitted; waiting for confirmations is your app’s responsibility.
- Each step triggers a wallet / signing flow through the Portal provider.
- For Solana responses, encoded payloads are used when
transactions is missing or empty (same selection rules as the implementation).
- If the API response includes no
transactions and no encodedTransactions, the method throws: No transactions in delegation response.
revokeAndSubmit
Overview
Calls revoke, then signs and sends each returned transaction. Use it to remove spending permission and broadcast the revocation in one flow.
Example
const { hashes } = await portal.delegations.revokeAndSubmit(
{
chain: 'eip155:11155111',
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
},
{
onProgress: (event) => console.log(event),
},
)
Parameters
| Name | Type | Required | Description |
|---|
params | object | Yes | Same shape as revoke. |
params.chain | string | Yes | Chain CAIP ID. |
params.token | string | Yes | Token symbol. |
params.delegateAddress | string | Yes | Delegate address whose approval you revoke. |
options | object | No | Same as approveAndSubmit (signAndSendTransaction, onProgress). |
Returns
{ hashes: string[] } — Same semantics as approveAndSubmit: ordered hashes from each signAndSendTransaction call.
Notes / Gotchas
- Same execution, signing, confirmation, chain-routing, and empty-response behavior as
approveAndSubmit.
transferAndSubmit
Overview
Calls transferFrom, then signs and sends each returned transaction. Use it when your wallet is the delegate moving funds from an owner you were approved for.
Example
const { hashes } = await portal.delegations.transferAndSubmit(
{
chain: 'eip155:11155111',
token: 'USDC',
fromAddress: '0x03c66353df426e18e6e7866fa9e2e73ef6833500',
toAddress: '0xdFd8302f44727A6348F702fF7B594f127dE3A902',
amount: '0.0001',
},
{
onProgress: (event) => console.log(event),
},
)
Parameters
| Name | Type | Required | Description |
|---|
params | object | Yes | Same shape as transferFrom. |
params.chain | string | Yes | Chain CAIP ID. |
params.token | string | Yes | Token symbol. |
params.fromAddress | string | Yes | Owner who delegated to your wallet. |
params.toAddress | string | Yes | Recipient of the transfer. |
params.amount | string | Yes | Amount to move. |
options | object | No | Same as approveAndSubmit (signAndSendTransaction, onProgress). |
Returns
{ hashes: string[] } — Same ordering rules as the other submit helpers.
Notes / Gotchas
- Same sequential submit, no confirmation wait, signing, chain routing, and empty-response error as above.
- Unlike
transferFrom, transferAndSubmit does not return API metadata (for example TransferAsDelegateMetadata); only { hashes } is exposed after broadcast.
Overriding the transaction sender
When using Portal, the default signAndSendTransaction is automatically configured, so you don’t need to provide it. For custom signing logic (for example a different submission path), pass signAndSendTransaction in the options:
await portal.delegations.approveAndSubmit(
{
chain: 'eip155:11155111',
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
amount: '10',
},
{
signAndSendTransaction: async (tx, chainId) => {
const method = chainId.startsWith('solana')
? 'sol_signAndSendTransaction'
: 'eth_sendTransaction'
const hash = await portal.request({
chainId,
method,
params: [tx],
})
if (typeof hash !== 'string' || hash.trim().length === 0) {
throw new Error('No transaction hash returned from provider')
}
return hash
},
onProgress: (event) => console.log(event),
},
)
The payload tx is the same shape the default signer passes through: an EVM transaction object from transactions[], or a Solana payload from encodedTransactions[].
Low-level methods
The following methods return raw API responses — transaction objects for EVM or base64-encoded payloads for Solana — which you sign and broadcast yourself via portal.request. Use these when you need full control over the signing or submission step.
Approving Delegations
Use approve to grant another address permission to spend tokens on your behalf. This method works for both EVM and Solana chains.
EVM Approval
async function approveEVMDelegation(portal: Portal) {
const tx = await portal.delegations.approve({
chain: 'eip155:11155111', // Sepolia testnet
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
amount: '10',
})
console.log('Approve response:', tx)
// Sign and send the transaction
const hash = await portal.request({
chainId: 'eip155:11155111',
method: 'eth_sendTransaction',
params: [tx.transactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Solana Approval
async function approveSolanaDelegation(portal: Portal) {
const tx = await portal.delegations.approve({
chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', // Solana Devnet
token: 'USDC',
delegateAddress: '7EYg9HUZBoCeCfWbcj3EFNX5Ecgjn9FTY2UhnRny5NYv',
amount: '10',
})
console.log('Approve response:', tx)
// Sign and send the transaction
const hash = await portal.request({
chainId: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
method: 'sol_signAndSendTransaction',
params: [tx.encodedTransactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Checking Delegation Status
Use getStatus to check current delegations and token balances for a specific delegate address.
EVM Status Check
async function getEVMDelegationStatus(portal: Portal) {
const status = await portal.delegations.getStatus({
chain: 'eip155:11155111',
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
})
console.log('Delegation status:', status)
console.log('Balance:', status.balance)
console.log('Delegations:', status.delegations)
}
Solana Status Check
async function getSolanaDelegationStatus(portal: Portal) {
const status = await portal.delegations.getStatus({
chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
token: 'USDC',
delegateAddress: '7EYg9HUZBoCeCfWbcj3EFNX5Ecgjn9FTY2UhnRny5NYv',
})
console.log('Delegation status:', status)
}
Revoking Delegations
Use revoke to remove spending permissions from a delegate address.
EVM Revoke
async function revokeEVMDelegation(portal: Portal) {
const tx = await portal.delegations.revoke({
chain: 'eip155:11155111',
token: 'USDC',
delegateAddress: '0xb52a818536341003c9d923103abd3659c27e5a2b',
})
console.log('Revoke response:', tx)
const hash = await portal.request({
chainId: 'eip155:11155111',
method: 'eth_sendTransaction',
params: [tx.transactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Solana Revoke
async function revokeSolanaDelegation(portal: Portal) {
const tx = await portal.delegations.revoke({
chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
token: 'USDC',
delegateAddress: '7EYg9HUZBoCeCfWbcj3EFNX5Ecgjn9FTY2UhnRny5NYv',
})
console.log('Revoke response:', tx)
const hash = await portal.request({
chainId: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
method: 'sol_signAndSendTransaction',
params: [tx.encodedTransactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Always revoke unused delegations after completing operations to minimize security risks.
Transferring as a Delegate
Use transferFrom to transfer tokens from another address that has delegated spending permission to you.
EVM Transfer From
async function transferFromEVM(portal: Portal) {
const tx = await portal.delegations.transferFrom({
chain: 'eip155:11155111',
token: 'USDC',
fromAddress: '0x03c66353df426e18e6e7866fa9e2e73ef6833500', // Token owner
toAddress: '0xdFd8302f44727A6348F702fF7B594f127dE3A902', // Recipient
amount: '0.0001',
})
console.log('TransferFrom response:', tx)
const hash = await portal.request({
chainId: 'eip155:11155111',
method: 'eth_sendTransaction',
params: [tx.transactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Solana Transfer From
async function transferFromSolana(portal: Portal) {
const tx = await portal.delegations.transferFrom({
chain: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
token: 'USDC',
fromAddress: 'GoFBWzCVxSEGYxgSAmdm2itS3EUbYmLpgzEcQ4J3WnsN', // Token owner
toAddress: 'GPsPXxoQA51aTJJkNHtFDFYui5hN5UxcFPnheJEHa5Du', // Recipient
amount: '0.0001',
})
console.log('TransferFrom response:', tx)
const hash = await portal.request({
chainId: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1',
method: 'sol_signAndSendTransaction',
params: [tx.encodedTransactions[0]]
})
console.log('Transaction hash:', hash)
return hash
}
Delegation Roles: fromAddress is the token owner who approved the delegation. Your wallet (the delegate) signs the transaction to transfer tokens from the owner to the toAddress recipient.
Supported Networks
Delegations work on all Portal-supported EVM and Solana chains:
- EVM: Ethereum, Polygon, Base, Arbitrum, Optimism, Monad, and all other EVM-compatible chains
- Solana: Solana Mainnet and Devnet
For a complete list, see Blockchain Support.
Next Steps