Skip to content

Add with-deposit-address example - #1632

Open
cylon56 wants to merge 27 commits into
mainfrom
example/with-deposit-address
Open

cylon56 wants to merge 27 commits into
mainfrom
example/with-deposit-address

Conversation

@cylon56

@cylon56 cylon56 commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Adds examples/transaction-management/with-deposit-address, a Next.js example of the universal deposit address flow on Base Sepolia, for ETH and USDC:

  • Enable: the user enters a destination. From the user's session, the example uploads a USDC transfer ABI to the sub-org, fetchOrCreateP256ApiKeyUser adds a non-root delegated user, and fetchOrCreatePolicies adds two policies limiting that user to the destination.
  • Sweep: a parent-org BALANCE_CONFIRMED_UPDATES webhook hits /api/webhooks/balances. The route verifies the signature, de-duplicates by event id and maps the address to its sub-org. The delegated key then sends the full balance with sponsor: true. A denial is logged and never retried.
  • Withdraw: signed with the user's own session.
  • pnpm validate: send to destination allowed; send elsewhere, raw payload and USDC transfer to the wrong recipient denied. A control confirms USDC to the right recipient is allowed.

The policies:

eth.tx.to == '<destination, lowercase>'

eth.tx.to == '<USDC, lowercase>' && eth.tx.function_name == 'transfer' &&
eth.tx.contract_call_args['recipient'] == '<destination, checksummed>'

These are two policies because the engine does not short-circuit a condition. A single condition joined with || errors on eth.tx.function_name for a plain ETH send, and the send is denied.

The README covers setup, who controls what, adding destinations later, what the policy can and can't see, login options for a platform with no login of its own, and common failures. STATUS.md has the full results.

No new packages. The lockfile change is limited to this example's entries.

Checked live on Base Sepolia

  • Fresh user: Enable, then a 1 USDC deposit and a 0.0001 ETH deposit each swept with no manual step and sponsored gas. The dashboard shows the hashes.
  • Withdraw: 1 USDC sent through the panel, signed by the user's own session.
  • Sub-org, read through the API: the user as sole root, one non-root delegated user, two policies.
  • pnpm validate returns the expected result on all five checks.
  • pnpm typecheck, prettier and next build are clean.

Known issues

  • The SDK's session refresh sometimes logs Failed to refresh session, and the user is sent back to the login page. A passkey re-login also failed once. Neither cause is known yet.
  • A live duplicate webhook delivery was never seen, so de-duplication has only been checked offline.

🤖 Generated with Claude Code

Starting point for the deposit address example. Same code as
examples/access-control/with-delegated/client-side, renamed, with the
tsconfig path adjusted for the new depth. CHANGELOG.md and the unused
Next.js boilerplate SVGs are left out.
Replace the delegated-access playground with one button. From the
user's session it creates the non-root delegated user and one policy that
lets it send ETH, or USDC via transfer(), only to the treasury. The
address is shown as text and a QR code, and the sub-org panel checks that
the user is the sole root and that there is exactly one policy.
register-webhook subscribes the parent org's BALANCE_CONFIRMED_UPDATES
to /api/webhooks/balances, reusing an existing subscription if present.

The route checks the Ed25519 signature against Turnkey's JWKS, drops
repeated event ids, and maps the address to its sub-org through a JSON
registry. The dashboard registers the address once the sub-org shows it
enabled; the server checks with the delegated key that the key's user is
in that sub-org, is not root, and that the address belongs to it.
On a confirmed ETH or USDC deposit the webhook queues a sweep. The
server reads the balance and sends all of it to the treasury from the
deposit address with sponsor: true, signed by the delegated key. Sweeps
for one address run in order. Any failure, including a policy denial, is
logged and recorded; nothing is retried and no other address is tried.

The dashboard polls /api/sweeps and links each sweep hash. Setting
SWEEP_ENABLED=false leaves deposits in place.
The withdraw panel sends ETH or USDC from the deposit address with the
user's own session, sponsored, so it works without the platform key.

pnpm validate signs, without broadcasting, as the delegated key: a send
to the treasury (allowed), a send elsewhere (denied) and a raw payload
(denied), plus the USDC pair. It first checks the key authenticates and
is not root, so a bad key cannot pass as a denial.
Setup, env vars, the custody model, how a platform adds destinations
later, a common failures table, and the example's limits.
Records what was checked offline and what still needs a live run.
@cylon56
cylon56 marked this pull request as ready for review November 4, 2026 15:13

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant