Near Learn

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#

The payload (before signing)
JSON
{
  "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:

standardWalletsEnvelope
nep413NEAR 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>)
erc191MetaMask 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_ed25519Phantom and other Solana walletsPayload string, public_key, signature
webauthnPasskeys (P-256 or ed25519)Payload string, key, signature, client_data_json, authenticator_data
tip191, ton_connect, sep53TRON, TON, StellarSee 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_salt view, 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 with Vij2xg…, 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.

transfer-intent.ts
TypeScript
// 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:

Shell
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 fieldUse it for
intents_executed[]Intent hash, account and nonce of each payload that would run
logsThe NEP-297 events (dip4 standard) execution would emit: transfer, token_diff, intents_executed…
invariant_violatedPresent when token_diffs do not balance: the missing deltas a counterparty must provide
state.fee, state.current_saltCurrent 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

  1. 1.Who is allowed to call execute_intents with Alice’s signed payload?
  2. 2.Your simulation returns invariant_violated. What does it mean?
  3. 3.Why do real Verifier nonces start with Vij2xg in base64?