Skip to main content

Permission Tokens

Permission Tokens (PTs) enable seamless, pre-approved transaction flows for dApps.

By requesting a PT, 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 PT, 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.
  • Background Signing: Valid transactions are processed silently in the background while the user stays immersed in your app.

How to use Permission Token

requestPermissionToken

To start using Permission Tokens, 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
chainId: 11155111, // Required: The chain ID this token is valid for
requestedAmountUsd: "100.00", // Required: Maximum cumulative spend limit in USD
requestedExpirySeconds: 3600 // Required: Duration in seconds (e.g., 1 hour)
};

try {
const result = await window.waap.requestPermissionToken(params);

if (result.success) {
console.log("PT Created!");
}
} catch (error) {
console.error("User rejected PT request or error occurred", error);
}
  • allowedAddresses: Array of addresses this token can send assets to. Pass an empty array [] to allow any recipient (riskier).
  • chainId: The EVM chain ID (number) where this token applies.
  • requestedAmountUsd: The cumulative US Dollar value limit for all transactions under this token.
  • requestedExpirySeconds: How long the token remains valid.

Sending Transactions with Permission Token

Once a PT is created, dApp can use it to send transactions in the background.

To invoke the Permission Token flow, simply set withPT: true to your standard eth_sendTransaction request.

// check if a valid PT is available with requestPermissionToken
// const result = await window.waap.requestPermissionToken(params);

// send transaction as usual with withPT set to true
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 PT 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: PT transactions use the same async transaction events as other async flows; listen for progress and errors via window.waap.on().

Listening to Events

Transactions sent with withPT: true use the same async transaction event system as async mode in general. The modal does not block; signing and broadcast happen in the background. Track progress via window.waap.on() events or, in React, the useWaapTransaction hook.

When you call request({ method: "eth_sendTransaction", ..., withPT: true }), it returns immediately with { pendingTxId: string, status: 'pending' } (AsyncTxResponse). Use pendingTxId to correlate events for that transaction.

Event lifecycle: waap_sign_pending → (optionally waap_2fa_required) → waap_sign_complete or waap_sign_failedwaap_tx_pendingwaap_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 PT transaction events (same as async transaction events)
window.waap.on('waap_sign_pending', (event) => {
console.log('Signing started', event.pendingTxId);
});

window.waap.on('waap_2fa_required', (event) => {
console.log('2FA required', event.pendingTxId);
});

window.waap.on('waap_sign_complete', (event) => {
console.log('Signed', event.pendingTxId, event.signature);
});

window.waap.on('waap_sign_failed', (event) => {
console.error('Signing failed', event.pendingTxId, event.error);
});

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_pending{ pendingTxId, txRequest: { to?, from?, value?, data?, chainId? } }
waap_2fa_required{ pendingTxId }
waap_sign_complete{ pendingTxId, signature, serializedTx } (serializedTx null for message signing)
waap_sign_failed{ pendingTxId, error }
waap_tx_pending{ pendingTxId, txHash }
waap_tx_confirmed{ pendingTxId, txHash, receipt: { blockNumber, blockHash, transactionHash, status, gasUsed } }
waap_tx_failed{ pendingTxId, error, stage: 'broadcast' | 'confirmation' }

Complete example for sending a transaction with PT

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 SendWithPTButton() {
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 sendWithPT = async () => {
const params = {
allowedAddresses: ["0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"],
chainId: 11155111,
requestedAmountUsd: "100.00",
requestedExpirySeconds: 3600,
};

const result = await window.waap.requestPermissionToken(params);
if (!result.success) {
alert('Permission token 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={sendWithPT}
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 Permission Token' : 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>
);
}