Appearance
Delegate Actions (NEP‑461)
Delegate actions let users sign a DelegateAction off-chain, then have a relayer pay gas and submit the on-chain transaction. This enables meta‑transactions and sponsored actions while keeping the user’s key on their device.
When to Use Delegate Actions
Use delegate actions when:
- You want a relayer account to pay gas on behalf of the user.
- You need to pre‑sign intent (e.g. “call this contract method with these args”) and submit later.
- You want to keep the same Tx Confirmer UX as normal transactions, but change who pays fees.
Under the hood this uses NEP‑461 SignedDelegate objects:
DelegateAction: describes the sender, receiver, actions, nonce, max block height and the user’s public key.SignedDelegate: wraps theDelegateActionplus the user’s signature.
Client Flow: signDelegateAction → relayer
From the TatchiPasskey client you:
- Build a
DelegateActionInputdescribing what you want the user to sign. - Call
signDelegateActionto produce{ hash, signedDelegate }. - POST
{ hash, signedDelegate }to your relayer’s/signed-delegateendpoint.
Example (simplified):
ts
import { TatchiPasskey } from '@tatchi-xyz/sdk';
import { ActionType } from '@tatchi-xyz/sdk/core';
const passkey = new TatchiPasskey(configs);
const { hash, signedDelegate } = await passkey.signDelegateAction({
nearAccountId: 'alice.testnet',
delegate: {
senderId: 'alice.testnet', // user paying in logical terms
receiverId: 'w3a-v1.testnet', // contract to call
actions: [
{
action_type: ActionType.FunctionCall,
method_name: 'do_something',
args: JSON.stringify({ foo: 'bar' }),
gas: '150000000000000',
deposit: '0',
},
],
nonce: 0, // 0 = let SDK fetch latest user nonce
maxBlockHeight: 0, // 0 = let SDK derive safe expiry
publicKey: 'ed25519:...', // user access-key public key
},
options: {
onEvent: console.log, // optional: progress events
},
});
// Send to relayer
const relayerUrl = `${configs.relayer.url}${configs.relayer.delegateActionRoute ?? '/signed-delegate'}`;
const response = await fetch(relayerUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ hash, signedDelegate }),
});The wallet iframe opens a Tx Confirmer modal for the delegate action, just like a normal transaction, and returns the signed payload only after the user approves.
Relayer Flow: /signed-delegate → NEAR Transaction
On the relayer side you expose a small endpoint which forwards the signed delegate to AuthService.executeSignedDelegate.
Node/Express example (see examples/relay-server/src/index.ts):
ts
app.post('/signed-delegate', async (req, res) => {
const { hash, signedDelegate } = req.body || {};
if (typeof hash !== 'string' || !hash || !signedDelegate) {
res.status(400).json({ ok: false, code: 'invalid_body', message: 'Expected { hash, signedDelegate }' });
return;
}
const result = await authService.executeSignedDelegate({ hash, signedDelegate });
if (!result || !result.ok) {
res.status(400).json({
ok: false,
code: result?.code || 'delegate_execution_failed',
message: result?.error || 'Failed to execute delegate action',
});
return;
}
res.status(200).json({
ok: true,
relayerTxHash: result.transactionHash || null,
status: 'submitted',
outcome: result.outcome ?? null,
});
});Cloudflare Workers follow the same pattern (see examples/relay-cloudflare-worker/src/delegate-route.ts), using AuthService from @tatchi-xyz/sdk/server.
Configuration Notes
- Client:
TatchiPasskeyConfigs.relayer.url– base URL of your relay server.TatchiPasskeyConfigs.relayer.delegateActionRoute– path for the delegate endpoint (defaults to/signed-delegate).
- Relayer:
- Uses the same
AuthServiceinstance as account creation and email recovery. - Serializes outgoing transactions via its internal queue to avoid nonce conflicts.
- Uses the same
With this wiring in place you get end‑to‑end NEP‑461 support: the browser signs delegate actions, the relayer pays gas, and the on-chain transaction is indistinguishable from a direct user action.