DOCS / POLICY DEMO

See program approval rules in action.

See how a Solana program turns application rules into PairKey signing authority. The same program can answer a read-only pairkey_approve request or commit a state-changing pairkey_approve_commit decision.

These are instructions in your application program, not two separate policies or programs. The example keeps its rules in a program-owned policy account. The excerpts below show the decision points; use the complete source for account validation, serialization and errors.

One policy, two approval paths

InstructionDecisionSolana effect per signing request
pairkey_approve/v1Return Output { input_digest, allowed } for the exact input.Each signer simulates deployed, immutable program code. No approval transaction or state write.
pairkey_approve_commit/v1Check the same rules, consume an allowance and invoke the PairKey gateway.One transaction changes application state and records the exact approval atomically.

Both paths still require an authenticated caller and an exact request. Both signers independently check current authority before signing. Simulation can read current state, but cannot reserve an allowance or consume a nonce.

Create the application program

  1. Download the hosted controller template. It contains the typed gateway ABI and both approval instructions.
  2. Edit vendor/programs/example-controller/src/approval.rs. Put your application conditions in AppPolicy::allows; keep the caller authentication, exact-input binding, PDA checks and gateway CPI.
  3. Pin the hosted gateway ID from /api/v2/config, build the Solana program, and deploy it. PairKey hosts the gateway and signers; you deploy only your application logic.
  4. Create the owner’s policy account with a signed pairkey_policy/v1 instruction. The template and hosted SDK do this through setProgramPolicy.
# In the downloaded hosted-controller directory:
PAIRKEY_REGISTRY_ID=YOUR_HOSTED_GATEWAY cargo build-sbf -- --locked
solana program deploy --url devnet --keypair YOUR_DEVNET_WALLET.json \
  target/deploy/pairkey_hosted_controller_example.so

The example authority PDA uses ["controller", owner]; its rules PDA uses ["pairkey-policy", owner]. The policy stores the owner, enabled flag, destination, path prefix, payload limit, approval mode, remaining allowance and a revision. Your owner wallet authorizes creation and each edit. Creating the PDA requires rent; later same-size edits reuse it.

Create and edit the rules

After connecting a wallet with the program quickstart, use the hosted SDK to create revision 1 or increment the current revision. This example selects the hosted program and its on-chain approval mode; replace its ID with your deployed program when ready.

const program = (await pairkey.config()).example_program;
const mode = 'Onchain'; // 'Offchain' only for immutable, read-only code
const current = await pairkey.programPolicy(program, wallet.publicKey);

await pairkey.setProgramPolicy(wallet, {
  program,
  owner: wallet.publicKey.toBase58(),
  revision: (current?.revision ?? 0) + 1,
  enabled: true,
  chain: 'solana',
  path_prefix: [0],
  max_payload_bytes: 1232,
  require_commit: mode === 'Onchain',
  remaining_approvals: 5
});

setProgramPolicy builds and checks the policy instruction, then asks the owner wallet to sign a Solana transaction. It is a setup or edit transaction, not a registration transaction and not something to repeat before every signature. The current revision must increase by exactly one. require_commit selects which approval instruction may grant permission; changing the mode does not change the program authority or its derived keys.

Sample: pairkey_approve

Use this path for read-only conditions such as an enabled flag, allowed destination, path prefix, membership or current application state. The program authenticates the signer-attested caller and returns a decision bound to the exact input. Each signer independently runs and checks the simulation.

// Excerpt from the example's pairkey_approve/v1 branch.
let input = Input::try_from_slice(bytes).map_err(|_| bad())?;
let p = policy(program, &accounts[2])?;
pair_registry::program_approval::authenticate_input(
    accounts,
    &input,
    &pair_registry::set_address(&pair_registry::id(), &p.enclave_set).0,
)?;
let allowed = input.authority
    == authority(program, &Pubkey::new_from_array(p.owner)).0.to_bytes()
    && !p.require_commit
    && p.allows(input.caller, &input.account, &input.payload);
set_return_data(&borsh::to_vec(&Output {
    input_digest: input.digest(),
    allowed,
}).map_err(|_| bad())?);

The example’s AppPolicy::allows checks the designated caller, enabled flag, destination, path prefix and canonical payload length. The full handler also validates account count and the policy PDA. Off-chain evaluation requires immutable deployed program code; an upgradeable hosted example uses the commit path while you develop.

Sample: pairkey_approve_commit

Use this path when approval itself must change state: consume a one-time allowance, mark a proposal used or reserve a budget. The example consumes one approval count. It does not parse destination transactions or implement a financial spending limit.

// Excerpt from the example's pairkey_approve_commit/v1 branch.
let call = CommitCall::try_from_slice(bytes).map_err(|_| bad())?;
let source = call.validate(Clock::get()?.unix_timestamp).map_err(|_| bad())?;
// Validate the owner's transaction signature, source, authority PDA,
// gateway and ordered application accounts before changing state.
let mut p = policy(program, policy_info)?;
if !p.allows(owner.key.to_bytes(), &call.request.commit.account, &call.payload)
    || p.remaining_approvals == 0
{
    return Err(bad());
}
p.remaining_approvals -= 1;
policy_info.try_borrow_mut_data()?.copy_from_slice(
    &borsh::to_vec(&p).map_err(|_| bad())?
);
// Invoke RegistryInstruction::ApproveProgram { approval: call.approval() }
// through invoke_signed with the program authority PDA seeds.

The program changes its policy account and records approval through gateway CPI in the same transaction. A failed CPI rolls back the state change. After finality, both signers require the matching approval record. If signing later fails, the committed allowance is not automatically refunded; recover the original job before making another request.

Try the hosted example

  1. Open the live message-signing demo and choose Solana program.
  2. Select Use hosted example, connect a devnet wallet, and keep On-chain · consume an allowance.
  3. Open Program policy setup, set an allowance, and sign the policy transaction once. Then load a derived key.
  4. Write a message and approve the exact signing request. The program consumes one allowance and commits the approval before the signers produce the signature.

The on-chain example needs devnet SOL for the policy edit and each commit. The read-only example is represented by the code above; do not remove a program’s upgrade authority merely to try a demo. For your own immutable deployment, set require_commit: false, select Offchain, and keep the same caller and request checks. Read Solana program integration for the ABI and trust model, or Policy & rules for the broader account, passkey and OIDC policy model.