text
Send signup email → Verify email → Complete registration →
Generate API keys → Store securely in 1Password → Wire .env
Do this in a single unbroken browser session. Never close the browser between steps.
user+alias@domain.com as the same useruser@domain.com, the alias will route to that existing accountPlaywright's WebAuthn.addVirtualAuthenticator creates an in-memory credential store
The passkey it registers is only valid for that browser process
If you close the browser and reopen it, the credential is gone forever
The only way to reuse it is to export the credential before closing, then re-import on next run
Export immediately after registration: js
const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
Re-import on next session: js
const saved = JSON.parse(fs.readFileSync('/tmp/webauthn-creds.json'));
for (const cred of saved.credentials) {
await cdp.send('WebAuthn.addCredential', { authenticatorId, credential: cred });
}
=\n) in QP-encoded emailsapp.service.com/internal/api/* endpoints require session cookiesapi.service.com/public/v1/* endpoints require API key stampingjs
const ctxClean = await browser.newContext({ storageState: undefined });
const page = await ctxClean.newPage();
await page.goto('https://service.com/signup');
// fill email, click continue
// close ctxClean immediately after submitting
await ctxClean.close();
Why separate? Prevents existing session cookies from hijacking the signup flow.
js
const ctx = await browser.newContext({ storageState: undefined });
const page = await ctx.newPage();
const cdp = await ctx.newCDPSession(page);
// Set up virtual authenticator BEFORE navigating
await cdp.send('WebAuthn.enable', { enableUI: false });
const { authenticatorId } = await cdp.send('WebAuthn.addVirtualAuthenticator', {
options: {
protocol: 'ctap2', transport: 'internal',
hasResidentKey: true, hasUserVerification: true,
isUserVerified: true, automaticPresenceSimulation: true,
}
});
// Navigate to verify link
await page.goto(verifyUrl);
// Complete signup steps...
// IMMEDIATELY export credential after passkey registration
const creds = await cdp.send('WebAuthn.getCredentials', { authenticatorId });
fs.writeFileSync('/tmp/webauthn-creds.json', JSON.stringify(creds));
console.log('Credentials backed up:', creds.credentials?.length);
// Continue to API key + wallet creation IN SAME SESSION
// ...save API keys...
// Close browser ONLY after saving everything to 1Password
js
function fetchLatestTurnkeyLink(host='127.0.0.1', port=1143, user, pass) {
return new Promise((resolve) => {
const socket = net.connect(port, host);
let buf='', tls2=null, step=0, body=[], inBody=false;
const t = setTimeout(() => { try{(tls2||socket).destroy()}catch(e){}; resolve(null); }, 22000);
function send(cmd) { (tls2||socket).write(cmd+'\r\n'); }
function onData(data) {
buf += data.toString();
const lines = buf.split('\r\n'); buf = lines.pop();
for (const l of lines) {
if (inBody) body.push(l);
if (step===0 && l.includes('OK')) { step=1; send('a1 STARTTLS'); }
else if (step===1 && l.includes('a1 OK')) { tls2=tls.connect({socket,rejectUnauthorized:false}); tls2.on('data',onData); step=2; send(`a2 LOGIN "${user}" "${pass}"`); }
else if (step===2 && l.includes('a2 OK')) { step=3; send('a3 SELECT INBOX'); }
else if (step===3 && l.includes('a3 OK')) { step=4; send('a4 SEARCH ALL'); }
else if (step===4 && l.startsWith('* SEARCH')) {
const nums = l.replace('* SEARCH','').trim().split(' ').filter(Boolean);
step=5; inBody=true; send(`a5 FETCH ${nums[nums.length-1]} (BODY[TEXT])`);
}
else if (step===5 && l.includes('a5 OK')) {
clearTimeout(t); (tls2||socket).end();
// Decode quoted-printable
const decoded = body.join('\n')
.replace(/=\r?\n/g, '')
.replace(/=([0-9A-Fa-f]{2})/g, (_, h) => String.fromCharCode(parseInt(h, 16)));
// Extract redirect URLs
const urls = [...decoded.matchAll(/https:\/\/service\.com\/redirect\?token=[^\s"<>)]+/g)].map(m=>m[0]);
resolve(urls[0] || null);
}
}
}
socket.on('data', onData);
socket.on('error', () => { clearTimeout(t); resolve(null); });
});
}
Key: Pattern-match the redirect URL to the service's domain, not a generic URL.
127.0.0.1, port: 1143, STARTTLSop://OpenClaw/Proton Bridge - Monk Fenix/...ps aux | grep -i bridgeStandard element.fill() sometimes fails on React inputs. Use this:
js
await page.evaluate((value) => {
const input = document.querySelector('input');
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')
.set.call(input, value);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}, value);
Buttons outside viewport fail with element is outside of the viewport. Always scroll:
js
await page.evaluate((text) => {
const btn = [...document.querySelectorAll('button')]
.find(b => b.textContent?.toLowerCase().includes(text) && !b.disabled);
if (btn) { btn.scrollIntoView(); btn.click(); }
}, buttonText);
For services with internal browser APIs:
js
// Call authenticated internal API from page context
const data = await page.evaluate(async () => {
const r = await fetch('/internal/api/v1/whoami');
return r.json();
});
// data.organizationId, data.userId, etc.
Once authenticated (cookie present), call internal endpoints from the page context:
js
const result = await page.evaluate(async ({ orgId, publicKey }) => {
const r = await fetch('/tkhq/api/v1/activities', { // adjust per service
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'ACTIVITY_TYPE_CREATE_API_KEYS',
organizationId: orgId,
parameters: { apiKeys: [{ apiKeyName: 'my-key', publicKey, curveType: 'API_KEY_CURVE_P256' }] }
})
});
return { status: r.status, body: await r.text() };
}, { orgId, publicKey });
Important: Internal endpoints vary per service. Before creating resources, capture network traffic to learn the real endpoint:
js
page.on('request', req => {
if (req.method() !== 'GET' && req.url().includes(serviceDomain))
console.log(req.method(), req.url());
});
bash
op item create \
--vault OpenClaw \
--title "Turnkey API Credentials — Reddi Agent Protocol" \
--category "API Credential" \
"org_id[text]=$ORG_ID" \
"user_id[text]=$USER_ID" \
"api_public_key[text]=$API_PUB" \
"api_private_key[password]=$API_PRIV" \
"wallet_id[text]=$WALLET_ID" \
"wallet_address[text]=$WALLET_ADDR"
Before starting any signup, add an entry to the Notion signup registry:
322eb552-581a-81dc-adbc-fabb7af1d311app.turnkey.com)#/tkhq/api/v1/activities (internal, cookie-auth)api.turnkey.com/public/v1/ requires X-Stamp (signed request)+tag) map to the same Turnkey account — use a different provider for separate orgsb7378687-cf82-45ab-a46c-7dda9239001d (Reddi Agent Protocol)Before starting any signup: