withPT applies to transaction methods only. It is ignored on message signing.Privileges
Privileges enable seamless, pre-approved transaction flows for dApps.
By requesting a Privilege, a dApp can obtain user approval for a specific scope of transactions (defined by spend limit, allowed addresses, and expiration). Once granted, subsequent valid transactions are signed automatically in the background.
If 2FA is disabled for Privileges, and transaction is high risk, then 2FA will be skipped. Note: this is to skip extra steps for users due to false positive warnings of dApp’s white-listed addresses/contracts.
Overview
- Seamless UX: Eliminate repetitive signing for frequent actions (e.g., gaming moves, social posts).
- Secure Scope: Tokens are bound to your dApp’s origin and strictly limited by constraints you define.
- No Confirmation Prompt: Valid transactions are signed without interrupting the user. Combine with
asyncTxsif you also want broadcast to continue in the background.
How to use Privilege
requestPermissionToken
To start using Privileges, you must first request one from the user. This will prompt a modal asking them to approve the specific limits.
The method takes a config object defining the token’s constraints.
const params = {
allowedAddresses: ["0xd8...", "0x12..."], // Required: One or more addresses
chain: "evm:11155111", // Required: canonical chain identifier
requestedAmountUsd: "100.00", // Optional: cumulative USD spend limit
requestedExpirySeconds: 3600 // Required: duration in seconds (max 7200)
};
try {
const result = await window.waap.requestPermissionToken(params);
if (result.success) {
console.log("Privilege Created!");
}
} catch (error) {
console.error("User rejected Privilege request or error occurred", error);
}- allowedAddresses: Array of addresses this token can send assets to. Pass an empty array
[]to allow any recipient (riskier). - chain: Canonical chain identifier.
evm:<id>,sui:<network>, orsolana:<network>. For exampleevm:11155111,sui:mainnet,solana:mainnet. - requestedAmountUsd: Optional cumulative US Dollar limit across all transactions under this token. Omit it (or pass
null) for an address-only scope with no spend cap. - requestedExpirySeconds: How long the token remains valid. Maximum 7200 (two hours).
There is no walletMode parameter. The facade decides it. initWaaP mints a standard-mode token, initWaaPSquid mints a Squid one. Request the token on the same facade that will send the transaction; a token is not redeemable by the other mode.
getPermissionTokenStatus returns redacted metadata only. Never the signed token, which stays inside the wallet.
Sending Transactions with Privilege
Once a Privilege is created, dApp can use it to send transactions in the background.
To invoke the Privilege flow, set withPT: true on a transaction call. Which methods accept it depends on the chain:
| Chain | Methods that accept withPT |
|---|---|
| EVM | request() for eth_sendTransaction and eth_signTransaction |
| Sui | signTransaction, signAndExecuteTransaction, and the legacy signTransactionBlock / signAndExecuteTransactionBlock pair |
| Solana | signTransaction, signAndSendTransaction |
withPT is ignored on message signing. personal_sign and eth_signTypedData_v4 are permission-token-ineligible wallet-side, whatever the caller passes.
Holding a token changes nothing on its own; you opt into it per call. The wallet resolves only a token matching the requesting origin, chain, wallet mode, and selected signer. Otherwise the transaction follows the ordinary approval and 2FA flow. A Privilege is not a browser-side authorization bypass: the Policy Engine still makes the decision.
The EVM example below shows the flow end to end.
A Privilege removes the confirmation prompt, not the wait. withPT: true
on its own resolves exactly like an ordinary request. With the signature or
transaction hash. And emits no lifecycle events.
To get the background flow, enable async mode as well: asyncTxs: true at
initialization, or async: true on the request. Then the
async contract applies and the events below
fire.
// check if a valid Privilege is available with requestPermissionToken
// const result = await window.waap.requestPermissionToken(params);
// send with a Privilege: no confirmation prompt, resolves with the tx hash
const txHash = await window.waap.request({
method: "eth_sendTransaction",
params: [{
from: userAddress,
to: "0xd8...", // Must match allowedAddresses if set
value: "0x...", // Value in Wei
data: "0x..." // Contract call data
}],
withPT: true
});Features:
- Automatic Match: The wallet automatically finds a valid Privilege for the current origin and chain.
- Constraint Check: If the transaction exceeds the limit, expires, or targets an unauthorized address, the request falls back to the standard UI modal (or fails, depending on configuration).
- Events: Privilege transactions use the same async transaction events as other async flows; listen for progress and errors via
window.waap.on().
Listening to Events
Events fire only when the request is also async. withPT alone does not
produce them. With asyncTxs: true (or async: true per request), a Privilege
transaction uses the same event system as any other
async transaction: track progress via window.waap.on(), or in React with the
useWaapTransaction hook.
With asyncTxs: true, the request resolves with the signed transaction hash, and every event carries that same hash. No correlation id is needed. Only the deprecated asyncSigning wire mode returns { pendingTxId, status: 'pending' }.
Event lifecycle: waap_sign_pending → (optionally waap_2fa_required) → waap_sign_complete or waap_sign_failed → waap_tx_pending → waap_tx_confirmed or waap_tx_failed.
| Event | When it fires | Payload type |
|---|---|---|
waap_sign_pending | Background signing has started | AsyncSignPendingEvent |
waap_2fa_required | High-risk transaction requires 2FA; modal re-opens | Async2faRequiredEvent |
waap_sign_complete | Signing finished (before broadcast) | AsyncSignCompleteEvent |
waap_sign_failed | Error during signing | AsyncSignFailedEvent |
waap_tx_pending | Transaction broadcast; waiting for confirmation | AsyncTxPendingEvent |
waap_tx_confirmed | Transaction confirmed on-chain | AsyncTxConfirmedEvent |
waap_tx_failed | Error during broadcast or confirmation | AsyncTxFailedEvent |
For full payload shapes and types, see the Event System section in the Transactions guide. In React apps you can use the useWaapTransaction hook instead of manual window.waap.on() listeners for a callback-based API.
// Listen for Privilege transaction events (same as async transaction events)
window.waap.on('waap_sign_complete', (event) => {
console.log('Signed', event.signature);
});
window.waap.on('waap_tx_pending', (event) => {
console.log('Transaction sent', event.txHash);
});
window.waap.on('waap_tx_confirmed', (event) => {
console.log('Transaction confirmed', event.txHash, event.receipt);
});
window.waap.on('waap_tx_failed', (event) => {
console.error('Transaction failed', event.error, event.stage);
});Event payloads (summary)
| Event | Payload |
|---|---|
waap_sign_complete | { signature, serializedTx } (serializedTx null for message signing) |
waap_tx_pending | { txHash } |
waap_tx_confirmed | { txHash, receipt: { blockNumber, blockHash, transactionHash, status, gasUsed } } |
waap_tx_failed | { error, stage: 'broadcast' | 'confirmation' } |
waap_sign_pending, waap_2fa_required and waap_sign_failed are not emitted in v2. Signing completes before the promise resolves, so a signing failure rejects it instead.
Complete example for sending a transaction with Privilege
Below is a complete React example using the useWaapTransaction hook for status and callbacks. The hook subscribes to the same async events; we send the transaction with window.waap.request(..., { withPT: true }) because the hook’s sendTransaction does not accept withPT yet.
import { useState } from 'react';
import { useWaapTransaction } from '@human.tech/waap-sdk';
export default function SendWithPrivilegeButton() {
const [status, setStatus] = useState<string>('Idle');
const [txHash, setTxHash] = useState<string | null>(null);
const { isAnyPending } = useWaapTransaction({
onPending: () setStatus('Signing in background...'),
onSigned: () setStatus('Signed! Broadcasting...'),
onTxPending: (event) => {
setStatus('Transaction Sent!');
setTxHash(event.txHash);
},
onConfirmed: () => {
setStatus('Confirmed on-chain ✅');
setTimeout(() => { setStatus('Idle'); setTxHash(null); }, 3000);
},
onSignFailed: (event) => setStatus(`Signing failed: ${event.error}`),
onFailed: (event) => setStatus(`Failed: ${event.error}`),
on2FARequired: () setStatus('2FA required...'),
});
const sendWithPrivilege = async () => {
const params = {
allowedAddresses: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
chain: "evm:11155111",
requestedAmountUsd: "100.00",
requestedExpirySeconds: 3600,
};
const result = await window.waap.requestPermissionToken(params);
if (!result.success) {
alert('Privilege denied or failed');
return;
}
const accounts = await window.waap.request({ method: 'eth_accounts' });
if (!accounts?.length) throw new Error('No account connected');
const fromAddress = accounts[0];
await window.waap.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: "0xaa36a7" }],
});
try {
setStatus('Initiating...');
setTxHash(null);
await window.waap.request({
method: "eth_sendTransaction",
params: [{
from: fromAddress,
to: "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
value: "0x38D7EA4C68000",
}],
withPT: true,
});
} catch (error) {
console.error(error);
setStatus('Error starting transaction');
}
};
const busy = status !== 'Idle' && !status.startsWith('Failed');
return (
<div className="flex flex-col items-center gap-2">
<button
onClick={sendWithPrivilege}
disabled={busy || isAnyPending}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50"
>
{status === 'Idle' && !isAnyPending ? 'Send with Privilege' : status}
</button>
<div className="text-sm font-medium text-gray-700">
Status: <span className="text-blue-600">{status}</span>
</div>
{txHash && (
<div className="text-xs text-gray-500">Hash: {txHash.slice(0, 10)}...</div>
)}
</div>
);
}Related
- Sign Messages: Message and typed data signing guide
- Send Transactions: Transaction sending and sponsored transactions
- Supported Chains: Available chains and adding custom chains
- Squid Mode: Privileges work identically in Squid mode; request them on the
initWaaPSquidfacade - CLI Privileges: minting a Privilege from an agent with
privilege create