Skip to main content
Allowance voucher (aka Session Key) allows owner of a ciphertext to delegate viewing/compute to someone else for a period of time completely off-chain.
Vouchers are like sessions and are revokable.

Security model

A session voucher grants the holder access to all of the sharer’s encrypted handles — it is not scoped to a specific dApp or contract. canUseSession checks only that the session has not expired and that the caller matches the authorized decrypter; it does not restrict access by which application the encrypted value came from. This means a single voucher signed for one application also covers handles created by any other Inco-enabled dApp the user has interacted with (auction bids, game states, private messages, etc). To limit exposure:
  • Use the shortest expiry that makes sense for your use case (minutes rather than hours or days).
  • Call updateActiveVouchersSessionNonce() as soon as the session is no longer needed to invalidate all outstanding vouchers immediately, regardless of their expiry.
  • Never log, persist, or transmit the voucher outside of the narrowest scope that requires it.

Wallet signing prompt

When a user signs a session voucher, wallets render all EIP-712 struct fields. The AllowanceVoucher includes a warning field that is part of the signed payload:
Inco Warning: signing this message may leak your private data, including from unrelated apps. Sign only if you fully trust this app.
The warning field is placed first in the EIP-712 struct so wallets show it before the opaque byte fields, ensuring users see it prominently. This message is included automatically when using grantSessionKeyAllowanceVoucher. Do not suppress or alter it — the on-chain verifier rejects any AllowanceProof whose voucher does not contain this exact text (reverts with InvalidVoucherWarning), and altering it also produces a different EIP-712 digest that will fail signature verification.

Getting Started

To create a voucher and grant Bob permission to decrypt on Alice’s behalf:
import { createWalletClient, custom } from 'viem'
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { getViemChain, supportedChains, type SupportedChainId } from '@inco/lightning-js';
import { Lightning } from '@inco/lightning-js/lite';

// Alice connects her wallet
const walletClient = createWalletClient({
  chain: getViemChain(supportedChains.baseSepolia),
  transport: custom(window.ethereum!)
})

// Alice creates an ephemeral Ethereum account (the session key)
const ephemeralAccount = privateKeyToAccount(generatePrivateKey());

// for Base mainnet: Lightning.baseMainnet()
// recommended: pass your own RPC URLs (otherwise viem's public RPC is used)
const zap = await Lightning.baseSepoliaTestnet({
  hostChainRpcUrls: ['https://your-rpc-url'],
});
// Default session verifier address
const sessionVerfier = "0xc34569efc25901bdd6b652164a2c8a7228b23005";

// Alice grants the ephemeral account permission to decrypt on her behalf
const voucherWithSig = await zap.grantSessionKeyAllowanceVoucher(
    walletClient,
    ephemeralAccount.address,
    new Date(Date.now() + 1000 * 60 * 60 * 24), // 1 day
    sessionVerfier,
);
Some specific use cases may require a custom session verifier logic, to for example check whether a payment has been made in order to access secrets. grantSessionKeyAllowanceVoucher() allows the owner to specify a custom session verifier contract address with a custom canUseSession() function that can either allow or deny access based on certain on-chain conditions.
Using the default session verifier will give the session key unverified access to all user handles for the specified time period.DEFAULT_SESSION_VERIFIER (Base Sepolia): 0xc34569efc25901bdd6b652164a2c8a7228b23005DEFAULT_SESSION_VERIFIER (Base mainnet): 0x68a5b59b4caf23416885859c1662746619a471f3
import {ALLOWANCE_GRANTED_MAGIC_VALUE} from "@inco/lightning/src/Types.sol";

struct Session {
    address decrypter;
    uint256 expiresAt;
}

contract MyCustomSessionVerifier {
    function canUseSession(
        bytes32, /* handle */
        address account, /* user trying to use the session */
        bytes memory sharerArgData,
        bytes memory /* requesterArgData */
    ) external view returns (bytes32) {
        Session memory session = abi.decode(sharerArgData, (Session));
        if (session.expiresAt >= block.timestamp && session.decrypter == account) {
            return ALLOWANCE_GRANTED_MAGIC_VALUE;
        }
        return bytes32(0);
    }
}

Attested Decrypt with Voucher/Session Key

The voucher and signature now allow Bob to decrypt any handle that Alice owns using the attestedDecryptWithVoucher() function:
import type { HexString } from '@inco/lightning-js';

// Bob calls the attestedDecryptWithVoucher function with voucher
const decrypted = await zap.attestedDecryptWithVoucher(
    ephemeralAccount,
    voucherWithSig,
    ['0x...' as HexString]
);
const plaintext = decrypted[0].plaintext.value;

AttestedCompute with Voucher/Session Key

Bob can also perform attested compute on behalf of Alice using attestedComputeWithVoucher() to check if the value is equal to an expected value:
import type { AttestedComputeSupportedOps } from '@inco/lightning-js/lite';
import type { HexString } from '@inco/lightning-js';

const computed = await zap.attestedComputeWithVoucher(
    ephemeralAccount,
    voucherWithSig,
    '0x...' as HexString,
    AttestedComputeSupportedOps.Eq,
    100n
);

const plaintext = computed.plaintext.value;

SessionKey Reencrypt

Similarly, Bob can also perform reencryption on behalf of Alice using attestedDecryptWithVoucher() with a reencrypt public key. This is similar to standard attested decrypt reencryption.
import { generateXwingKeypair } from '@inco/lightning-js/lite';
import type { HexString } from '@inco/lightning-js';

// Generate an X-Wing keypair for reencryption
const reencryptKeypair = await generateXwingKeypair();
const reencryptPubKey = reencryptKeypair.encodePublicKey();

// Bob performs reencryption on behalf of Alice
const reencrypted = await zap.attestedDecryptWithVoucher(
    ephemeralAccount,
    voucherWithSig,
    ['0x...' as HexString],
    { reencryptPubKey, reencryptKeypair }
);

// Bob can decrypt the reencrypted data
const decrypted = await decrypt(
    reencryptKeypair,
    hexToBytes(reencrypted[0].encryptedPlaintext.ciphertext.value)
);
const plaintext = BigInt('0x' + Buffer.from(decrypted).toString('hex'));

Revoking a Voucher

In order to invalidate existing vouchers, you can call this function:
const txHash = await zap.updateActiveVouchersSessionNonce(walletClient);
Which will invalidate all existing vouchers, despite their expiration time.
If you want to selectively revoke access to specific users, your dApp would need to handle the logic of reissuing new vouchers to users who still require access.