User Auth
Authenticate User with Signed Data
Exchange a signed message for a User-Session access token
POST
/
v0
/
auth
/
authenticate
Authenticate user with signed data
curl --request POST \
--url https://1click.chaindefuser.com/v0/auth/authenticate \
--header 'Content-Type: application/json' \
--data '
{
"signedData": {
"payload": {
"message": "<string>",
"nonce": "<string>",
"recipient": "<string>",
"callbackUrl": "<string>"
},
"public_key": "<string>",
"signature": "<string>",
"standard": "nep413"
}
}
'import requests
url = "https://1click.chaindefuser.com/v0/auth/authenticate"
payload = { "signedData": {
"payload": {
"message": "<string>",
"nonce": "<string>",
"recipient": "<string>",
"callbackUrl": "<string>"
},
"public_key": "<string>",
"signature": "<string>",
"standard": "nep413"
} }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
signedData: {
payload: {
message: '<string>',
nonce: '<string>',
recipient: '<string>',
callbackUrl: '<string>'
},
public_key: '<string>',
signature: '<string>',
standard: 'nep413'
}
})
};
fetch('https://1click.chaindefuser.com/v0/auth/authenticate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://1click.chaindefuser.com/v0/auth/authenticate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'signedData' => [
'payload' => [
'message' => '<string>',
'nonce' => '<string>',
'recipient' => '<string>',
'callbackUrl' => '<string>'
],
'public_key' => '<string>',
'signature' => '<string>',
'standard' => 'nep413'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://1click.chaindefuser.com/v0/auth/authenticate"
payload := strings.NewReader("{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://1click.chaindefuser.com/v0/auth/authenticate")
.header("Content-Type", "application/json")
.body("{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://1click.chaindefuser.com/v0/auth/authenticate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}"
response = http.request(request)
puts response.read_body{
"accessToken": "<string>",
"refreshToken": "<string>",
"expiresIn": 123,
"refreshExpiresIn": 123
}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 (
The nonce has to be a versioned nonce.
Some TRON wallets reject a short message. The string near.com signs adds
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.
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 assignedData. "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. |
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.
signature verification failed is the response when intents.near rejects the payload in simulate_intents. A signer_id that does not match the signing key, a recovery byte left at 27 or 28, or any difference between the signed string and the string you submit all return that same error.- NEAR
- EVM
- Solana
- TRON
- TON
- Stellar
- Passkey
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 });
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 });
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"
}
}'
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));
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"
}
}'
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"
}
}'
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"
}
}'
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
}
signedData is the same MultiPayload signed-message format used for Signing Intents, signed here with an empty intents array. It’s used purely as proof of account ownership, not a real swap.Store
refreshToken securely. Anyone holding it can mint new accessTokens for this account until it expires.Body
application/json
Assuming wallets want to interact with Intents protocol, besides preparing the data in a certain form, they have to have the capability to sign raw messages (off-chain signatures) using an algorithm we understand. This enum solves that problem.
For example, because we support ERC-191 and know how to verify messages with that standard, we can allow wallets, like Metamask, sign messages to perform intents without having to support new cryptographic primitives and signing standards.
- Option 1
- Option 2
- Option 3
- Option 4
- Option 5
- Option 6
- Option 7
- Option 8
Show child attributes
Show child attributes
Was this page helpful?
⌘I
Authenticate user with signed data
curl --request POST \
--url https://1click.chaindefuser.com/v0/auth/authenticate \
--header 'Content-Type: application/json' \
--data '
{
"signedData": {
"payload": {
"message": "<string>",
"nonce": "<string>",
"recipient": "<string>",
"callbackUrl": "<string>"
},
"public_key": "<string>",
"signature": "<string>",
"standard": "nep413"
}
}
'import requests
url = "https://1click.chaindefuser.com/v0/auth/authenticate"
payload = { "signedData": {
"payload": {
"message": "<string>",
"nonce": "<string>",
"recipient": "<string>",
"callbackUrl": "<string>"
},
"public_key": "<string>",
"signature": "<string>",
"standard": "nep413"
} }
headers = {"Content-Type": "application/json"}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
signedData: {
payload: {
message: '<string>',
nonce: '<string>',
recipient: '<string>',
callbackUrl: '<string>'
},
public_key: '<string>',
signature: '<string>',
standard: 'nep413'
}
})
};
fetch('https://1click.chaindefuser.com/v0/auth/authenticate', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://1click.chaindefuser.com/v0/auth/authenticate",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'signedData' => [
'payload' => [
'message' => '<string>',
'nonce' => '<string>',
'recipient' => '<string>',
'callbackUrl' => '<string>'
],
'public_key' => '<string>',
'signature' => '<string>',
'standard' => 'nep413'
]
]),
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://1click.chaindefuser.com/v0/auth/authenticate"
payload := strings.NewReader("{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://1click.chaindefuser.com/v0/auth/authenticate")
.header("Content-Type", "application/json")
.body("{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://1click.chaindefuser.com/v0/auth/authenticate")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Content-Type"] = 'application/json'
request.body = "{\n \"signedData\": {\n \"payload\": {\n \"message\": \"<string>\",\n \"nonce\": \"<string>\",\n \"recipient\": \"<string>\",\n \"callbackUrl\": \"<string>\"\n },\n \"public_key\": \"<string>\",\n \"signature\": \"<string>\",\n \"standard\": \"nep413\"\n }\n}"
response = http.request(request)
puts response.read_body{
"accessToken": "<string>",
"refreshToken": "<string>",
"expiresIn": 123,
"refreshExpiresIn": 123
}