Headless Mode
Headless mode submits a 1auth intent with signatures produced outside the passkey dialog. Use it when the ERC-7579 account has another installed validator and your app can produce its expected signatures.
1auth prepares the intent and forwards opaque signature bytes; the installed on-chain validator decides whether they are valid.
For example, the non-passkey signer might be:
- an app-managed ECDSA signer,
- a backend or AWS KMS signing service,
- another ERC-7579 validator your app has installed out of band.
Use OneAuthClient.sendIntent when every intent should show a passkey approval dialog.
What 1auth does
Headless mode still provides:
/api/intent/prepare?signingMode=headlessquotes the intent, performs balance checks, and returns the message hashes your validator signer must sign.- Sponsorship helpers mint access and extension JWTs for apps that sponsor intents.
/api/intent/headless-executeforwards your pre-encoded validator signatures to the orchestrator.- Status and wait endpoints work the same way as the passkey flow.
1auth treats the submitted signatures as opaque bytes. It does not open clear signing UI and does not verify the validator signature locally.
Requirements
Before using headless mode:
- the account must already have a validator installed that can authorize the intent,
- your app must know how to sign the prepared hashes for that validator,
- your app must encode the signatures in the validator-prefixed format expected by the orchestrator,
- submitted calls must stay inside whatever policy or authorization the installed validator enforces.
This guide covers only the prepare-sign-submit path. For SmartSession validators, use the Permissions flow for setup, policy review, and session-key handling.
Prepare
import { OneAuthHeadlessClient } from "@rhinestone/1auth/headless";
const headless = new OneAuthHeadlessClient({
providerUrl: "https://passkey.1auth.app",
sponsorship: {
accessTokenUrl: "/api/sponsorship/access-token",
extensionTokenUrl: "/api/sponsorship/extension-token",
},
});
const prepared = await headless.prepareIntent({
accountAddress,
targetChain: 84532,
calls: [
{
to: vault,
data: depositCalldata,
value: 0n,
},
],
});prepareIntent() returns intentOp and digestResult for unchanged submission, per-origin hashes, destination and target-execution hashes when required, and the echoed account, calls, target chain, and expiry.
Sign
Sign the prepared hashes with the non-passkey validator your app controls. 1auth does not prescribe the signer or encoding here; use the encoder for your validator.
const {
originSignatures,
destinationSignature,
targetExecutionSignature,
} = await signWithInstalledValidator({
prepared,
validatorContext,
});The contents of validatorContext depend on the installed validator.
Submit
const result = await headless.submitIntent({
intentOp: prepared.intentOp,
digestResult: prepared.digestResult,
binding: prepared.binding,
accountAddress: prepared.accountAddress,
targetChain: prepared.targetChain,
calls: prepared.calls,
expiresAt: prepared.expiresAt,
originSignatures,
destinationSignature,
targetExecutionSignature,
});prepareIntent() fixes the funding mode before signing and returns the quote-time sponsorshipToken for sponsored submission. Use sponsorshipMode: "preferred" to allow a self-funded re-quote, or "disabled" to request self-funding.
Wait
const final = await headless.waitForIntent(
result.intentId,
result.transactionResult,
);waitForIntent() returns the latest orchestrator status and any available transaction hash.
Trust Model
Headless mode removes the per-intent passkey dialog, so your app decides what to sign. 1auth binds a sponsored intent's extension token to the prepared intentOp, but a malicious or buggy app-side signer can still sign the wrong thing.
The safety boundary is the installed validator:
- if the validator approves the submitted signature, the intent can execute,
- if the calls drift outside that validator's policy, the validator should reject on-chain.