Add a Signing Key to an Existing Wallet ("Cold Boot")
This flow adds an initial BENJI intent-signing key to an existing BENJI wallet.
Use this flow when you see an INTENT_SIGNING_FAILURE, which refers to a successfully created walletId and Ed25519 public/private signing-key pair, but the wallet does not already have a signing key.
The flow has three main actions:
- Create the signing key with
walletSigningKeyCreate - Sign the returned intent payload locally with the Ed25519 private key
- Submit the signature with
walletSigningKeySubmit
1. Understand the keys involved
There are three separate key concepts in this process:
- BENJI authentication key — P-256 / ES256
Used to authenticate the API client when requesting access to BENJI. - BENJI intent-signing key — Ed25519
Used to sign BENJI intent payloads for wallet operations. - Blockchain wallet key — Network-specific
Controls the underlying blockchain wallet address and is separate from BENJI authentication and intent signing.
For this flow, signingPublicKey and signingPrivateKey must be the matching Ed25519 key pair used for BENJI intent signing.
2. Required values
Before running the signing-key flow, configure the BENJI authentication, environment, wallet, and signing-key values used by the script.
Some values are provided by Franklin Templeton, some are generated during setup, and others are selected by the user.
| Environment variable | Source | Purpose | Example / expected format |
|---|---|---|---|
BENJI_CLIENT_ID | Provided by Franklin Templeton | BENJI OAuth client ID assigned during onboarding | "0oa..." |
BENJI_PRI_KEY_JSON | Generated during authentication setup | Onboarded P-256 private JWK used to authenticate the API client | { "kty": "EC", "crv": "P-256", "x": "...", "y": "...", "d": "...", "kid": "..." } |
BENJI_OKTA_URL | Standard BENJI environment configuration | BENJI OAuth token endpoint | "https://login-preview.digitalassets.franklintempleton.com/oauth2/.../v1/token" |
BENJI_SCOPE | See Okta Scopes | OAuth scope requested for the BENJI access token | Each individual Okta scope, separated by a space. Example for read-only access: wallets.readonly bank_transactions.readonly transfers.readonly institutions.readonly |
BENJI_ENDPOINT | Standard BENJI environment configuration | BENJI GraphQL API endpoint | "https://api-uat.frk.com/tf/platform/bve/graphql" |
WALLET_ID | Provided by Franklin Templeton / BENJI | BENJI-generated ID of the existing wallet | BENJI wallet ID, not the blockchain 0x... address |
SIGNING_PUBLIC_KEY | Generated as part of the Ed25519 signing-key pair | Ed25519 public key registered with the wallet | Base64-encoded public key |
SIGNING_PRIVATE_KEY | Generated as part of the Ed25519 signing-key pair | Matching Ed25519 private key used to sign the returned intent payload | Base64-encoded private key |
SIGNING_KEY_NAME | Selected by the user | Name assigned to the new signing key | "InitialKey" |
Example local configuration
A configured PowerShell session should therefore resemble:
# Provided by Franklin Templeton
$env:BENJI_CLIENT_ID = "your-client-id"
$env:WALLET_ID = "your-benji-wallet-id"
# Generated during authentication setup
$env:BENJI_PRI_KEY_JSON = Get-Content -Raw "C:\path\to\benji-private-jwk.json"
# BENJI UAT environment
$env:BENJI_OKTA_URL = "https://login-preview.digitalassets.franklintempleton.com/oauth2/aus3nefxjv7YUkjW31d7/v1/token"
$env:BENJI_ENDPOINT = "https://api-uat.frk.com/tf/platform/bve/graphql"
# Generated Ed25519 signing-key pair
$env:SIGNING_PUBLIC_KEY = "your-ed25519-public-key"
$env:SIGNING_PRIVATE_KEY = "your-ed25519-private-key"
# Selected by the user
$env:SIGNING_KEY_NAME = "InitialKey"
3. Create the signing key
The first mutation registers the Ed25519 public key with the existing wallet.
/**
* Create a signing key on an existing wallet.
*
* The wallet must not already have a signing key. The mutation returns the
* signingKeyId and the payload that must be signed before the key is submitted.
*/
export async function walletSigningKeyCreate(
walletId,
signingKeyName,
signingKey
) {
const QUERY = gql`
mutation walletSigningKeyCreate(
$walletId: ID!
$signingKeyName: String!
$signingKey: String!
) {
walletSigningKeyCreate(
input: {
walletId: $walletId
signingKeyName: $signingKeyName
signingKey: $signingKey
}
) {
signingKeyId
intentSigningPackage {
payloadType
payload
}
}
}
`;
const variables = {
walletId,
signingKeyName,
signingKey
};
try {
return await client.request(QUERY, variables);
} catch (err) {
console.error('GraphQL error:', err);
throw err;
}
}
The important values returned are:
signingKeyId
intentSigningPackage.payload
The payload must be signed before the signing key can be submitted.
4. Sign the returned intent payload
Call walletSigningKeyCreate using the wallet ID, key name, and Ed25519 public key:
const signingKeyCreateResult = await walletSigningKeyCreate(
walletId,
signingKeyName,
signingPublicKey
);
Then retrieve the payload returned by BENJI and sign it with the matching Ed25519 private key:
const intentSignature = intentSignPayload(
signingPrivateKey,
signingKeyCreateResult.walletSigningKeyCreate
.intentSigningPackage.payload
);
The exact returned payload is what must be signed. Do not substitute or reconstruct the payload.
5. Submit the signed intent
The second mutation takes:
- the
signingKeyIdreturned bywalletSigningKeyCreate - the generated
intentSignature
/**
* Submit the signed intent returned by walletSigningKeyCreate.
*/
export async function walletSigningKeySubmit(
signingKeyId,
intentSignature
) {
const QUERY = gql`
mutation walletSigningKeySubmit(
$signingKeyId: ID!
$intentSignature: String!
) {
walletSigningKeySubmit(
institutionSubmit: {
signingKeyId: $signingKeyId
intentSignature: $intentSignature
}
) {
signingKeyId
blockchainStatus
}
}
`;
const variables = {
signingKeyId,
intentSignature
};
try {
return await client.request(QUERY, variables);
} catch (err) {
console.error('GraphQL error:', err);
throw err;
}
}
Submit the result:
const signingKeySubmitResult = await walletSigningKeySubmit(
signingKeyCreateResult.walletSigningKeyCreate.signingKeyId,
intentSignature
);
console.log(
'GraphQL response:\n',
JSON.stringify(signingKeySubmitResult, null, 2)
);
The response returns:
signingKeyId
blockchainStatus
6. Complete script flow
The core execution flow is therefore:
const signingPublicKey = process.env.SIGNING_PUBLIC_KEY;
const signingPrivateKey = process.env.SIGNING_PRIVATE_KEY;
const walletId = process.env.WALLET_ID;
const signingKeyName =
process.env.SIGNING_KEY_NAME || 'InitialKey';
if (!walletId || !signingPublicKey || !signingPrivateKey) {
throw new Error(
'Set WALLET_ID, SIGNING_PUBLIC_KEY, and SIGNING_PRIVATE_KEY.'
);
}
// Step 1: Create the signing key
const signingKeyCreateResult = await walletSigningKeyCreate(
walletId,
signingKeyName,
signingPublicKey
);
console.log(
'GraphQL response:\n',
JSON.stringify(signingKeyCreateResult, null, 2)
);
// Step 2: Sign the returned BENJI intent payload
const intentSignature = intentSignPayload(
signingPrivateKey,
signingKeyCreateResult.walletSigningKeyCreate
.intentSigningPackage.payload
);
// Step 3: Submit the signing key and signature
const signingKeySubmitResult = await walletSigningKeySubmit(
signingKeyCreateResult.walletSigningKeyCreate.signingKeyId,
intentSignature
);
console.log(
'GraphQL response:\n',
JSON.stringify(signingKeySubmitResult, null, 2)
);