How to sign intents, build endorsed requests, and manage signer group and policy lifecycles
Every action that changes wallet state in Dakota — sending funds, attaching a signer group, modifying a policy — requires an endorsed request: a cryptographically signed declaration of intent. This guide explains the signing model end-to-end, documents every intent type, and walks through the full signer group and policy lifecycles.
If you haven’t set up a wallet yet, start with Non-Custodial Wallets first, then return here for the complete signing reference.
Dakota’s policy engine re-canonicalizes the intent server-side, verifies each signature against the stored public keys, checks that the signer group’s approval threshold is met, and only then executes the action. This means:
You never send private keys — only signatures
The server cannot fabricate intents — every action has cryptographic proof of authorization
Multiple signers can co-approve — the signatures array accepts as many entries as your threshold requires
Create the intent object with the exact fields required for the operation. Field names are snake_case. Amounts are strings (e.g., "10.5", not 10.5). Omit any optional fields that don’t apply.
RFC 8785 JSON Canonicalization Scheme produces a deterministic byte sequence from any JSON object by sorting keys alphabetically and removing insignificant whitespace. This ensures that your client and Dakota’s policy engine hash identical bytes, regardless of how your language orders JSON keys.
Hash the canonical bytes with SHA-256, then sign the hash using your ECDSA P-256 private key. The signature must be in ASN.1 DER encoding (not raw r || s), then base64-encoded.
Browser only:crypto.subtle.sign with ECDSA returns an IEEE P1363 raw r || s encoding, not ASN.1 DER. You must convert it before submitting — the rawEcdsaSignatureToDer helper above does this. Submitting the raw form will fail signature verification.
Every endorsed request carries one of nine intent types. The table below lists all of them with their discriminator value, endpoint, and required fields.
The operation.kind field determines which sub-fields are required:
Kind
Required Fields
Optional Fields
transfer
from, to, amount, asset_id
—
contract_call
from, to, asset_id
method, args, data
Amounts are strings ("10.5", not 10.5). The caip2 field uses CAIP-2 chain identifiers (e.g., eip155:1 for Ethereum mainnet, eip155:11155111 for Sepolia).
Deleting a policy is irreversible. Detach the policy from all wallets first — attempting to delete a policy that is still attached to a wallet will fail.
Before detaching a signer group, ensure the wallet has at least one other signer group attached, or the wallet will become unusable — no one will be able to sign transactions for it.
Adding and removing signers (steps 3 and 6) do not require endorsed requests — they are standard API calls authenticated with your API key. Only wallet-level operations (attach, detach, transact) require cryptographic signatures.
Policies define automated governance rules for wallets — approval thresholds, amount limits, address allowlists, and more. Here is the complete lifecycle.
Deletion order matters. You must detach a policy from all wallets before you can delete it. Attempting to delete an attached policy will return an error.
Signature is in IEEE P1363 format (r || s) instead of ASN.1 DER
Use dsaEncoding: 'der' in Node.js, or convert with rawEcdsaSignatureToDer in browsers. See code examples.
invalid_signature
Canonicalization mismatch — client and server produced different bytes
Ensure you are using an RFC 8785 JCS library (canonicalize for JS, jcs for Python, json-canonicalization for Go). Standard JSON.stringify is not deterministic.
invalid_signature
Amount passed as number instead of string
Use "10.5" not 10.5. Numbers and strings canonicalize differently.
invalid_signature
Extra or missing fields in the intent
The intent you sign must have exactly the fields the server expects. Omit optional fields you are not using — do not set them to null.