Signing and executing intents
Build, sign and submit NEAR Intents: the signed payload, NEP-413 and ERC-191 envelopes, versioned nonces and deadlines, simulate_intents, then execute_intents.
Advanced10 min read3-question check
Every action in the Verifier is a signed payload: who signs, which contract, until when, a single-use nonce, and a list of intents. The signature is produced off-chain by whatever wallet the user has. Anyone can then submit it on-chain. Docs: Intent types, Signing intents, Simulating.
Anatomy of a signed intent#
{
"signer_id": "alice.near",
"verifying_contract": "intents.near",
"deadline": "2026-10-04T12:05:00.000Z",
"nonce": "Vij2xgAlKBKz…(32 bytes, base64)",
"intents": [
{
"intent": "transfer",
"receiver_id": "bob.near",
"tokens": { "nep141:wrap.near": "1000000000000000000000000" },
"memo": "invoice-17"
}
]
}How it is wrapped depends on the wallet’s signing standard. The standard field tells the Verifier how to hash and verify it:
standard | Wallets | Envelope |
|---|---|---|
nep413 | NEAR wallets (NEP-413 signMessage) | payload: { message, nonce, recipient: "intents.near" }, where message is the JSON string of { signer_id, deadline, intents }; plus public_key, signature (ed25519:<base58>) |
erc191 | MetaMask and EVM wallets (personal_sign) | payload = the whole payload JSON as a string; signature = secp256k1:<base58> of r‖s‖v with v normalized to 0/1; no public_key (recovered) |
raw_ed25519 | Phantom and other Solana wallets | Payload string, public_key, signature |
webauthn | Passkeys (P-256 or ed25519) | Payload string, key, signature, client_data_json, authenticator_data |
tip191, ton_connect, sep53 | TRON, TON, Stellar | See the signing docs |
Nonces and deadlines#
- Deadline: after it, the intent is rejected (
DeadlineExpired). Keep it short: minutes, not days. A signed intent is a bearer instrument until then. - Nonce: 32 bytes, base64, single use per account. Reuse fails with
NonceUsed. - Versioned nonces (what the SDK builds): magic prefix
5628f6c6, a version byte, the contract’s current 4-byte salt (current_saltview, e.g.252812b3), the nonce’s own expiry, then random bytes. The Verifier rejects them if the salt was rotated out or the nonce expired, and can garbage-collect them. That is why real nonces start withVij2xg…, the base64 of the prefix. - Plain random 32-byte nonces are still accepted by the contract today, but they can never be cleaned up; prefer versioned ones.
Sign with an EVM wallet, simulate, submit#
The Intents SDK (`@defuse-protocol/intents-sdk`, 0.88 in October 2026) builds payloads, fetches the salt for the nonce, and signs with NEP-413 or viem (ERC-191). Here an Ethereum key moves USDC it holds inside intents.near to a game contract, with no NEAR wallet involved.
// npm install @defuse-protocol/intents-sdk viem
import { IntentsSDK, createIntentSignerViem } from '@defuse-protocol/intents-sdk'
import { privateKeyToAccount } from 'viem/accounts'
const USDC = 'nep141:17208628f84f5d6ad33f0da3bbbeb27ffcb398eac501a31bd6ad2011e36133a1'
// signer_id defaults to the implicit account of this key: its lowercased 0x address
const evmAccount = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`)
const signer = createIntentSignerViem({ signer: evmAccount })
const sdk = new IntentsSDK({
referral: 'my-app',
intentSigner: signer,
solverRelayApiKey: process.env.INTENTS_API_KEY, // partner key for the relay
})
// 1. Build + sign. The builder fetches the salt and creates a versioned nonce.
const { signed } = await sdk
.intentBuilder()
.setDeadline(new Date(Date.now() + 2 * 60_000))
.addIntent({
intent: 'transfer',
receiver_id: 'game.near',
tokens: { [USDC]: '5000000' }, // 5 USDC
memo: 'player:42',
})
.buildAndSign(signer)
// 2. Simulate: a view call, nothing changes on-chain
const sim = await view('intents.near', 'simulate_intents', { signed: [signed] })
if (sim.invariant_violated) throw new Error('unbalanced: ' + JSON.stringify(sim.invariant_violated))
console.log(sim.intents_executed, sim.logs)
// 3. Submit through the relay and wait for settlement
const { tickets } = await sdk.sendSignedIntents({ multiPayloads: [signed] })
const settled = await sdk.waitForIntentSettlement({ intentHash: tickets[0] })
console.log('settled in NEAR tx', settled)
// Minimal NEAR RPC view helper (any RPC provider works)
async function view(contractId: string, method: string, args: object) {
const res = await fetch('https://rpc.mainnet.near.org', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 'learn',
method: 'query',
params: {
request_type: 'call_function',
finality: 'final',
account_id: contractId,
method_name: method,
args_base64: Buffer.from(JSON.stringify(args)).toString('base64'),
},
}),
})
const { result, error } = await res.json()
if (error || result?.error) throw new Error(JSON.stringify(error ?? result.error)) // e.g. bad signature, nonce used
return JSON.parse(Buffer.from(result.result).toString())
}Submitting without the relay#
execute_intents is a normal change method that anyone may call; the signatures inside carry the authority. A backend, a relayer, or the user can submit:
near contract call-function as-transaction intents.near execute_intents \
file-args signed.json \
prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' \
sign-as relayer.near network-config mainnet sign-with-keychain send
# signed.json: { "signed": [ { "standard": "erc191", "payload": "…", "signature": "secp256k1:…" } ] }simulate_intents field | Use it for |
|---|---|
intents_executed[] | Intent hash, account and nonce of each payload that would run |
logs | The NEP-297 events (dip4 standard) execution would emit: transfer, token_diff, intents_executed… |
invariant_violated | Present when token_diffs do not balance: the missing deltas a counterparty must provide |
state.fee, state.current_salt | Current fee in pips and the salt for building nonces |
| (RPC error) | Invalid signature, unknown key, expired deadline, used nonce, insufficient balance: the call panics with the reason |
Check yourself
3 questions · progress saved in this browser