Skip to content

feat(sdp-api): forward user_id to Kora on sign calls (PRO-1348)#548

Open
niks3089 wants to merge 1 commit into
mainfrom
feat/kora-user-id-sponsorship
Open

feat(sdp-api): forward user_id to Kora on sign calls (PRO-1348)#548
niks3089 wants to merge 1 commit into
mainfrom
feat/kora-user-id-sponsorship

Conversation

@niks3089

@niks3089 niks3089 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Closes the Kora user_id requirement (PRO-1348): Kora rejects signTransaction/signAndSendTransaction with "user_id is required when usage tracking is enabled and pricing is free" — its mainnet config. Forwards a stable per-end-user user_id.

Approach: stay on @solana/kora 0.2.1 (no SDK bump)

0.2.1's client forwards every request field to the JSON-RPC params (signTransaction(request) → rpcRequest('signTransaction', request)), so user_id reaches the server via a small typed cast (0.2.1's request type doesn't declare it yet).

I first bumped to 0.3.0-beta.0 (which types user_id) but it broke CI: that beta ships an empty exports map + peer kit-plugins (@solana/kit-plugin-payer) that esbuild can't resolve in the Docker bundle, and the bump destabilized @solana/pay's resolution of the same plugin. Reverted — net dependency change is now zero (package.json + lockfile == main). Swap to the typed user_id when @solana/kora 0.3.0 stable ships.

Changes (6 files, source-only)

  • KoraAdapter: buildSignRequest() attaches user_id; forwarded on both sign calls.
  • resolveKoraUserId(c): SDP session/clerk user id, else API key id.
  • Threaded through the per-request adapter construction (getFeePayment(c), custody signer-check) — no per-call-site changes.
  • Adapter test: forwarded + omitted, for both sign methods.

Devnet impact: none

kora-devnet has usage tracking off, so user_id is ignored there. Mainnet-only effect.

Follow-up (not in this PR)

Background/service paths (mosaic, solana/factory, recurring-payments) don't thread userId yet — fine for devnet, needed before they hit mainnet Kora.

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
sdp-docs Ready Ready Preview, Comment Jun 30, 2026 8:55am
sdp-web Ready Ready Preview, Comment Jun 30, 2026 8:55am

Request Review

@linear

linear Bot commented Jun 30, 2026

Copy link
Copy Markdown

PRO-1348

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the mainnet Kora rejection ("user_id is required when usage tracking is enabled and pricing is free") by threading a stable per-user identifier through the per-request adapter construction path. A new resolveKoraUserId helper sources the id from the SDP session, Clerk, or API key, and a new buildSignRequest private method on KoraAdapter attaches it as user_id on every signTransaction/signAndSendTransaction call.

  • KoraAdapter gains an optional userId field forwarded via buildSignRequest; the type cast is needed because @solana/kora@0.2.1 (the installed version, confirmed by lockfile) has no user_id in its TypeScript types — the adapter relies on the SDK passing all params through to the JSON-RPC payload.
  • getFeePayment in payments/context.ts and the signer-check handler are updated to inject resolveKoraUserId(c) at adapter construction; three background callers (mosaic, solana/factory, recurring-payments) are explicitly deferred as follow-up work.
  • Four new unit tests verify user_id is forwarded when set and omitted when absent, covering both signAndSend and signAsFeePayer.

Confidence Score: 5/5

Safe to merge for the per-request sign paths; the three deferred background callers are a known gap acknowledged in the PR description and won't break devnet.

The logic is clean and isolated: userId is optional everywhere so the change is backward-compatible, existing callers compile without modification, and the new unit tests cover all four sign × presence combinations. The one noteworthy item is that the PR description's claim of a package bump to 0.3.0-beta.0 is contradicted by both the lockfile (still 0.2.1) and the code comment itself — the actual approach relies on 0.2.1 forwarding extra params at runtime rather than on typed SDK support. This is documented in the code comment and is consistent with how JSON-RPC clients work, but it does mean mainnet is the first real end-to-end test of the user_id delivery.

apps/sdp-api/src/services/adapters/fee-payment/kora/kora.adapter.ts — the buildSignRequest cast and its relationship to the installed SDK version.

Important Files Changed

Filename Overview
apps/sdp-api/src/lib/kora-user.ts New helper that resolves a stable per-user identifier from session, Clerk, or API key — clean, well-documented, no issues.
apps/sdp-api/src/services/adapters/fee-payment/kora/kora.adapter.ts Adds userId field and buildSignRequest helper; user_id forwarding uses a type cast because the installed @solana/kora is still 0.2.1 (contradicting the PR description's claim of a 0.3.0-beta.0 bump), relying on the SDK transparently forwarding all params.
apps/sdp-api/src/services/adapters/fee-payment/index.ts Threads optional userId through createFeePaymentAdapter and createKoraAdapter factory functions; native adapter path correctly ignores the field.
apps/sdp-api/src/routes/payments/context.ts getFeePayment now passes resolveKoraUserId(c) to the adapter — straightforward and correct.
apps/sdp-api/src/routes/custody/handlers/signer-check.ts Threads userId into the per-request adapter construction; only API key callers reach this handler so apiKey.id will always resolve.
apps/sdp-api/src/services/adapters/fee-payment/kora/kora.adapter.test.ts Covers all four combinations (user_id set/absent × signAndSend/signAsFeePayer), addressing both forwarding and omission semantics.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Req as HTTP Request
    participant RKU as resolveKoraUserId(c)
    participant CTX as payments/context.ts or signer-check.ts
    participant FAC as createFeePaymentAdapter(env, userId)
    participant KA as KoraAdapter
    participant BSR as buildSignRequest(base64Tx)
    participant SDK as KoraClient (@solana/kora 0.2.1)
    participant KORA as Kora RPC (mainnet)

    Req->>CTX: inbound sign request
    CTX->>RKU: resolve userId
    RKU-->>CTX: session.userId ?? clerk.userId ?? apiKey.id
    CTX->>FAC: createFeePaymentAdapter(env, userId)
    FAC->>KA: "new KoraAdapter({ ..., userId })"
    KA-->>FAC: adapter instance
    FAC-->>CTX: FeePaymentPort

    CTX->>KA: signAndSend(txBytes) or signAsFeePayer(txBytes)
    KA->>BSR: buildSignRequest(base64Tx)
    BSR-->>KA: "{ transaction, user_id? } cast to SDK param type"
    KA->>SDK: signAndSendTransaction(params) or signTransaction(params)
    SDK->>KORA: "JSON-RPC { method, params: { transaction, user_id } }"
    KORA-->>SDK: "{ signature, signed_transaction }"
    SDK-->>KA: response
    KA-->>CTX: Signature or signed bytes
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Req as HTTP Request
    participant RKU as resolveKoraUserId(c)
    participant CTX as payments/context.ts or signer-check.ts
    participant FAC as createFeePaymentAdapter(env, userId)
    participant KA as KoraAdapter
    participant BSR as buildSignRequest(base64Tx)
    participant SDK as KoraClient (@solana/kora 0.2.1)
    participant KORA as Kora RPC (mainnet)

    Req->>CTX: inbound sign request
    CTX->>RKU: resolve userId
    RKU-->>CTX: session.userId ?? clerk.userId ?? apiKey.id
    CTX->>FAC: createFeePaymentAdapter(env, userId)
    FAC->>KA: "new KoraAdapter({ ..., userId })"
    KA-->>FAC: adapter instance
    FAC-->>CTX: FeePaymentPort

    CTX->>KA: signAndSend(txBytes) or signAsFeePayer(txBytes)
    KA->>BSR: buildSignRequest(base64Tx)
    BSR-->>KA: "{ transaction, user_id? } cast to SDK param type"
    KA->>SDK: signAndSendTransaction(params) or signTransaction(params)
    SDK->>KORA: "JSON-RPC { method, params: { transaction, user_id } }"
    KORA-->>SDK: "{ signature, signed_transaction }"
    SDK-->>KA: response
    KA-->>CTX: Signature or signed bytes
Loading

Reviews (2): Last reviewed commit: "feat(sdp-api): forward user_id to Kora o..." | Re-trigger Greptile

Comment thread apps/sdp-api/src/lib/kora-user.ts Outdated
* Returns undefined when unauthenticated; the adapter then omits `user_id`.
*/
export function resolveKoraUserId(c: AppContext): string | undefined {
return c.get("session")?.userId ?? c.get("clerk")?.userId ?? c.get("apiKey")?.id ?? undefined;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant trailing ?? undefined

All three nullable-access expressions (session?.userId, clerk?.userId, apiKey?.id) produce string | undefined — none of the underlying properties are typed as string | null. The final ?? undefined therefore never fires and just adds noise to the chain. Removing it keeps the intent clear and the return type still satisfies string | undefined.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +48 to +55
it("omits user_id when not configured (e.g. unauthenticated / devnet)", async () => {
const adapter = new KoraAdapter({ rpcUrl: "http://kora" });
await adapter.signAndSend(TX);
const arg = mocks.signAndSendTransaction.mock.calls[0]?.[0] ?? {};
expect(arg).not.toHaveProperty("user_id");
expect(arg).toHaveProperty("transaction");
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing omission test for signAsFeePayer

The "omits user_id when not configured" case only exercises signAndSendsignAndSendTransaction. The symmetrical signAsFeePayersignTransaction path uses identical spread logic but isn't covered. If the spread were accidentally dropped from one of the two methods, the existing tests wouldn't catch it.

Kora rejects signTransaction/signAndSendTransaction with "user_id is required"
under its mainnet config (free pricing + usage tracking). Forward a stable
per-end-user user_id on both sign calls.

Stays on @solana/kora 0.2.1: its client forwards every request field to the
JSON-RPC params, so user_id reaches the server via a typed cast (0.2.1's request
type doesn't declare it). Deliberately did NOT bump to 0.3.0-beta.0 — that beta
ships an empty `exports` map + unbundleable peer kit-plugins that break the
esbuild Docker bundle (and destabilized @solana/pay's resolution). Swap to the
typed user_id when @solana/kora 0.3.0 stable ships.

resolveKoraUserId(c) sources the id (SDP session/clerk user id, else API key id),
threaded through the per-request adapter (payments getFeePayment, custody
signer-check) so every signAndSend carries it. Devnet has usage tracking off, so
this is a no-op there. Adapter test covers forwarded + omitted for both methods.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@niks3089 niks3089 force-pushed the feat/kora-user-id-sponsorship branch from 9310286 to 54c9cd8 Compare June 30, 2026 08:54
@niks3089

Copy link
Copy Markdown
Contributor Author

Addressed both nits — and a bigger CI fix:

  • Redundant ?? undefined removed from resolveKoraUserId.
  • signAsFeePayer omission test added — now covers forwarded + omitted for both sign methods (4 cases, green).
  • ⚠️ Reverted the 0.3.0-beta.0 bump (it caused the Docker Build / Smoke / integration failures). That beta has an empty exports map + peer kit-plugins (@solana/kit-plugin-payer) esbuild can't resolve in the bundle — it also destabilized @solana/pay's resolution. Now staying on 0.2.1 and sending user_id via its request passthrough (the client forwards all request fields to JSON-RPC params) with a typed cast. Net dep change is zero (package.json + lockfile == main), so the build is unblocked. We swap to the typed field at 0.3.0 stable.

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