Skip to Content
WaaP for AppsPrivileges
Available on EVM, Sui, and Solana. 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 asyncTxs if 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>, or solana:<network>. For example evm: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:

ChainMethods that accept withPT
EVMrequest() for eth_sendTransaction and eth_signTransaction
SuisignTransaction, signAndExecuteTransaction, and the legacy signTransactionBlock / signAndExecuteTransactionBlock pair
SolanasignTransaction, 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.

EventWhen it firesPayload type
waap_sign_pendingBackground signing has startedAsyncSignPendingEvent
waap_2fa_requiredHigh-risk transaction requires 2FA; modal re-opensAsync2faRequiredEvent
waap_sign_completeSigning finished (before broadcast)AsyncSignCompleteEvent
waap_sign_failedError during signingAsyncSignFailedEvent
waap_tx_pendingTransaction broadcast; waiting for confirmationAsyncTxPendingEvent
waap_tx_confirmedTransaction confirmed on-chainAsyncTxConfirmedEvent
waap_tx_failedError during broadcast or confirmationAsyncTxFailedEvent

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)

EventPayload
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> ); }
Last updated on