NEAR Intents: Authenticate User with Signed Data
post /v0/auth/authenticate Exchange a signed message for a User-Session access token
Send this endpoint a signed message from your user, and you'll get back a User-Session token. That token is what lets the user's confidential balances and transaction history (GET /v0/account/balances, GET /v0/account/history) be revealed. It's separate from your Partner JWT, which authenticates your integration, not an individual user.
[!NOTE]
This is part of Confidential Intents. How to send the token on later calls is in Authenticating end users.
Request
Have the user's wallet sign an empty intent, then send it as signedData. "intents": [] is the ownership proof. It does not move tokens. Field layout for each wallet is in Signing Intents.
signer_id is the Intents account id for that key:
| Wallet | standard |
signer_id |
|---|---|---|
| NEAR | nep413 |
NEAR account id. Register the public key on intents.near with add_public_key first. See Account Abstraction. |
| Ethereum | erc191 |
The address, lowercase. |
| Solana | raw_ed25519 |
Hex of the address bytes (64 hex characters). public_key is ed25519: plus the base58 address. |
| TRON | tip191 |
0x plus the 20-byte address. Drop the leading 41 byte from the TRON address. |
| TON | ton_connect |
Ed25519 public key as 64 lowercase hex characters. |
| Stellar | sep53 |
Hex of the raw 32-byte public key, without the version byte or checksum. |
| Passkey | webauthn |
P-256: 0x plus the last 20 bytes of keccak256 over the bytes p256 concatenated with the public key. Ed25519: hex of the public key. |
The nonce has to be a versioned nonce. intentBuilder().setNonceRandomBytes(VersionedNonceBuilder.createTimestampedNonceBytes(new Date())) writes the current time into the first 8 bytes and loads the salt from intents.near (production salt e110f317). The server rejects a start time more than 5 minutes off with timestamp validation failed. A random 32-byte nonce comes back as invalid nonce.
Send signatures as ed25519:<base58> or secp256k1:<base58>. For erc191 and tip191, the recovery byte is 0 or 1. Map wallet values 27 and 28 to 0 and 1 before you encode.
[!WARNING]
signature verification failedis the response whenintents.nearrejects the payload insimulate_intents. Asigner_idthat does not match the signing key, a recovery byte left at27or28, or any difference between the signed string and the string you submit all return that same error.
NEAR
standard is nep413. The NEP-413 message is the string below. recipient is intents.near.
The signMessage callback returns a base64 signature. createIntentSignerNEP413 encodes it as ed25519: plus base58 before signed is sent.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "nep413",
"payload": {
"recipient": "intents.near",
"nonce": "YOUR_VERSIONED_NONCE",
"message": "{\"deadline\":\"2026-09-30T09:00:00.000Z\",\"intents\":[],\"signer_id\":\"your-account.near\"}"
},
"public_key": "ed25519:YOUR_PUBLIC_KEY",
"signature": "ed25519:YOUR_SIGNATURE"
}
}'
import { createIntentSignerNEP413, IntentsSDK, VersionedNonceBuilder } from '@defuse-protocol/intents-sdk';
import { UserAuthService } from '@defuse-protocol/one-click-sdk-typescript';
const signer = createIntentSignerNEP413({
accountId: userAccountId,
signMessage: async (_payload, hash) => {
const { publicKey, signature } = await userWallet.signMessage(hash);
return { publicKey: publicKey.toString(), signature: Buffer.from(signature).toString('base64') };
},
});
const sdk = new IntentsSDK({ referral: 'your-integration', env: 'production' });
const { signed } = await sdk
.intentBuilder()
.setDeadline(new Date(Date.now() + 5 * 60_000))
.setNonceRandomBytes(VersionedNonceBuilder.createTimestampedNonceBytes(new Date()))
.buildAndSign(signer);
const auth = await UserAuthService.authenticate({ signedData: signed });
EVM
standard is erc191. There is no public_key. The wallet signs payload with personal_sign, and you submit that same string.
createIntentSignerViem sets signer_id from the account address and encodes the signature as secp256k1: plus base58 with the recovery byte already normalized.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "erc191",
"payload": "{\"signer_id\":\"0xca67c1bb3fd69857e5edaf6aa1c65371bf46a464\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}",
"signature": "secp256k1:YOUR_SIGNATURE"
}
}'
import { createIntentSignerViem, IntentsSDK, VersionedNonceBuilder } from '@defuse-protocol/intents-sdk';
import { UserAuthService } from '@defuse-protocol/one-click-sdk-typescript';
import { privateKeyToAccount } from 'viem/accounts';
const signer = createIntentSignerViem({
signer: privateKeyToAccount(privateKey),
});
const sdk = new IntentsSDK({ referral: 'your-integration', env: 'production' });
const { signed } = await sdk
.intentBuilder()
.setDeadline(new Date(Date.now() + 5 * 60_000))
.setNonceRandomBytes(VersionedNonceBuilder.createTimestampedNonceBytes(new Date()))
.buildAndSign(signer);
const auth = await UserAuthService.authenticate({ signedData: signed });
Solana
standard is raw_ed25519. Sign the UTF-8 bytes of payload. deadline is an ISO 8601 string.
import { base58, hex } from '@scure/base';
const signerId = hex.encode(base58.decode(solanaAddress));
// public_key: `ed25519:${solanaAddress}`
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "raw_ed25519",
"payload": "{\"signer_id\":\"YOUR_64_CHAR_HEX_ADDRESS\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}",
"public_key": "ed25519:YOUR_BASE58_ADDRESS",
"signature": "ed25519:YOUR_SIGNATURE"
}
}'
TRON
standard is tip191. Same signature rules as ERC-191, including the recovery byte. signer_id is the 20-byte address with a 0x prefix, not the base58 T... address.
import { base58, hex } from '@scure/base';
const decoded = base58.decode(tronAddress);
const signerId = '0x' + hex.encode(decoded.slice(1, 21));
Some TRON wallets reject a short message. The string near.com signs adds message_size_validation and is pretty-printed with a two-space indent. Submit the string the wallet signed.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "tip191",
"payload": "{\"signer_id\":\"0xYOUR_20_BYTE_ADDRESS\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}",
"signature": "secp256k1:YOUR_SIGNATURE"
}
}'
TON
standard is ton_connect. address, domain, timestamp, and payload come back from the TonConnect sign-data request. payload.text is the intent JSON the wallet signed. signer_id inside that JSON is the 64-character hex public key. public_key is ed25519: plus that same key in base58.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "ton_connect",
"address": "YOUR_TON_ADDRESS",
"domain": "your-app.com",
"timestamp": "2026-09-30T09:00:00.000Z",
"payload": {
"type": "text",
"text": "{\"signer_id\":\"YOUR_64_CHAR_HEX_PUBLIC_KEY\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}"
},
"public_key": "ed25519:YOUR_PUBLIC_KEY",
"signature": "ed25519:YOUR_SIGNATURE"
}
}'
Stellar
standard is sep53. Sign payload with the Stellar key (SEP-53). signer_id is the hex encoding of the 32-byte public key. public_key is ed25519: plus the base58 encoding of those bytes. The G... address is not signer_id.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "sep53",
"payload": "{\"signer_id\":\"YOUR_64_CHAR_HEX_PUBLIC_KEY\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}",
"public_key": "ed25519:YOUR_PUBLIC_KEY",
"signature": "ed25519:YOUR_SIGNATURE"
}
}'
Passkey
standard is webauthn. client_data_json and authenticator_data come from the WebAuthn assertion. See Signing Intents for those two fields. payload is still an empty intent, and nonce is still a versioned nonce.
curl -X POST https://1click.chaindefuser.com/v0/auth/authenticate \
-H "Content-Type: application/json" \
-d '{
"signedData": {
"standard": "webauthn",
"payload": "{\"signer_id\":\"0xYOUR_INTENTS_ACCOUNT\",\"verifying_contract\":\"intents.near\",\"deadline\":\"2026-09-30T09:00:00.000Z\",\"nonce\":\"YOUR_VERSIONED_NONCE\",\"intents\":[]}",
"public_key": "p256:YOUR_PUBLIC_KEY",
"signature": "p256:YOUR_SIGNATURE",
"client_data_json": "{\"type\":\"webauthn.get\",\"challenge\":\"YOUR_CHALLENGE\",\"origin\":\"https://your-app.com\"}",
"authenticator_data": "YOUR_AUTHENTICATOR_DATA"
}
}'
Example response
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresIn": 3600,
"refreshExpiresIn": 2592000
}
[!TIP]
signedDatais the sameMultiPayloadsigned-message format used for Signing Intents, signed here with an emptyintentsarray. It's used purely as proof of account ownership, not a real swap.
[!WARNING]
Store
refreshTokensecurely. Anyone holding it can mint newaccessTokens for this account until it expires.