Prerequisites. You already have a wallet, a signer group, and at least one ES256 private key. If not, start with Create a Wallet (Non-Custodial).
The Three Parts of a Signed Transaction
Dakota wallets use an intent-based model: every transaction begins with a canonical JSON description of what the wallet should do, signed with the private key of at least one signer in the wallet’s signer group. Platform forwards the signed intent to the policy engine, which verifies each signature against the stored public keys and — if the approval threshold is satisfied — executes the transaction on-chain. A signed wallet transaction has three parts:- The intent — a JSON object describing the operation. Fields use
snake_case, amounts are strings. - The canonical digest — the intent canonicalized per RFC 8785 JCS, then hashed with SHA-256.
- The signature(s) — ECDSA P-256 signatures of that digest in ASN.1 DER encoding, each base64-encoded.
The Intent
Field names are
snake_case (wallet_id, asset_id, idempotency_key). Amounts are strings ("10.5", not 10.5). The operation.kind field determines which sub-fields are required: transfer uses from / to / amount / asset_id; evm_contract_call additionally accepts method / args / data. Omit any field that doesn’t apply — canonicalization drops unset fields and sorts keys alphabetically before hashing.Canonicalize, Hash, and Sign
Canonicalization is what ensures that a client and the policy engine hash the same bytes regardless of JSON key ordering or whitespace. All three reference libraries below implement RFC 8785 JCS, so their output is byte-identical for the same input.Submit the Signed Transaction
signatures array holds as many entries as the signer group’s approval threshold requires; each entry is a DER-then-base64 ECDSA signature from a distinct signer in the group. The intent object must be byte-equivalent to what was canonicalized and signed — platform re-canonicalizes the intent on the server side before signature verification, so ordering and whitespace in the wire JSON do not matter, but field values must match exactly.
Modifying Policies, Wallets, and Signer Groups
Adding rules to a policy, attaching a signer group to a wallet, and any other mutation of a wallet’s authorization graph after creation go through the same endorsed-request pattern:- Build an intent JSON object with a
typediscriminator field - Canonicalize it per RFC 8785 JCS and sign with ECDSA P-256 (use the same
signIntentfunction above — it works for every intent type) - POST to the mutation endpoint with
{ "signatures": [...], "intent": {...} }
type field that tells the server which schema to expect.
| Operation | Endpoint | type value |
|---|---|---|
| Add a rule to a policy | POST /policies/{policy_id}/rules | add_policy_rule |
| Update a rule’s definition | PATCH /policies/{policy_id}/rules/{rule_id} | update_policy_rule |
| Remove a rule from a policy | DELETE /policies/{policy_id}/rules/{rule_id} | remove_policy_rule |
| Delete a policy | DELETE /policies/{policy_id} | delete_policy |
| Attach a policy to a wallet | PUT /policies/{policy_id}/wallets/{wallet_id} | attach_policy_to_wallet |
| Detach a policy from a wallet | DELETE /policies/{policy_id}/wallets/{wallet_id} | detach_policy_from_wallet |
| Attach a signer group to a wallet | PUT /wallets/{wallet_id}/signer-groups/{signer_group_id} | attach_group_to_wallet |
| Detach a signer group from a wallet | DELETE /wallets/{wallet_id}/signer-groups/{signer_group_id} | detach_group_from_wallet |
Adding or removing signers from a signer group (
POST /signer-groups/{signer_group_id}/signers and DELETE /signer-groups/{signer_group_id}/signers/{signer_id}) is a plain API call authenticated with your API key — it does not require an endorsed intent. See Signing & Endorsed Requests for the full list of which mutations require signatures.The signers that must endorse each intent are determined by the resource being mutated:
- Policy mutations (
add_policy_rule,remove_policy_rule,update_policy_rule,delete_policy) must be signed by members of the policy’s own signer group. - Wallet mutations (
attach_policy_to_wallet,detach_policy_from_wallet,attach_group_to_wallet,detach_group_from_wallet) must be signed by members of a signer group already attached to the wallet.
Example: Add a Rule to an Existing Policy
TheAddPolicyRuleIntent shape:
Field shape reminders:
typeis required on every intent and must match the operation (e.g."add_policy_rule") — it’s part of what you signactionis"allow"or"deny"rule_typeis"approval_threshold","amount_threshold", or"address_list"definitionis a flat object for the chosenrule_type— e.g.{"threshold": 2}forapproval_threshold,{"addresses": [...]}foraddress_list
signIntent function from above, then submit:
type discriminator matching the operation, and string enum values.
