Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build VoIP calling apps on Android using Telnyx WebRTC SDK. Covers authentication, making/receiving calls, push notifications (FCM), call quality metrics, and AI Agent integration. Use when implementing real-time voice communication on Android.
.claude/skills/team-telnyx-telnyx-webrtc-client-android/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 133% | 0% |
| case-05 | ✗→✓ | ▲ Improved | 139% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 264% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 194% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 155% | 0% |
Build real-time voice communication into Android applications using Telnyx WebRTC.
> Prerequisites: Create WebRTC credentials and generate a login token using the Telnyx server-side SDK. See the telnyx-webrtc-* skill in your server language plugin (e.g., telnyx-python, telnyx-javascript).
Add JitPack repository to your project's build.gradle:
gradleallprojects { repositories { maven { url 'https://jitpack.io' } } }
Add the dependency:
gradledependencies { implementation 'com.github.team-telnyx:telnyx-webrtc-android:latest-version' }
Add to AndroidManifest.xml:
xml<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- For push notifications (Android 14+) --> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_PHONE_CALL"/>
kotlinval telnyxClient = TelnyxClient(context) telnyxClient.connect() val credentialConfig = CredentialConfig( sipUser = "your_sip_username", sipPassword = "your_sip_password", sipCallerIDName = "Display Name", sipCallerIDNumber = "+15551234567", fcmToken = fcmToken, // Optional: for push notifications logLevel = LogLevel.DEBUG, autoReconnect = true ) telnyxClient.credentialLogin(credentialConfig)
kotlinval tokenConfig = TokenConfig( sipToken = "your_jwt_token", sipCallerIDName = "Display Name", sipCallerIDNumber = "+15551234567", fcmToken = fcmToken, logLevel = LogLevel.DEBUG, autoReconnect = true ) telnyxClient.tokenLogin(tokenConfig)
| Parameter | Type | Description | |-----------|------|-------------| | sipUser / sipToken | String | Credentials from Telnyx Portal | | sipCallerIDName | String? | Caller ID name displayed to recipients | | sipCallerIDNumber | String? | Caller ID number | | fcmToken | String? | Firebase Cloud Messaging token for push | | ringtone | Any? | Raw resource ID or URI for ringtone | | ringBackTone | Int? | Raw resource ID for ringback tone | | logLevel | LogLevel | NONE, ERROR, WARNING, DEBUG, INFO, ALL | | autoReconnect | Boolean | Auto-retry login on failure (3 attempts) | | region | Region | AUTO, US_EAST, US_WEST, EU_WEST |
kotlin// Create a new outbound call telnyxClient.call.newInvite( callerName = "John Doe", callerNumber = "+15551234567", destinationNumber = "+15559876543", clientState = "my-custom-state" )
Listen for socket events using SharedFlow (recommended):
kotlinlifecycleScope.launch { telnyxClient.socketResponseFlow.collect { response -> when (response.status) { SocketStatus.ESTABLISHED -> { // Socket connected } SocketStatus.MESSAGERECEIVED -> { response.data?.let { data -> when (data.method) { SocketMethod.CLIENT_READY.methodName -> { // Ready to make/receive calls } SocketMethod.LOGIN.methodName -> { // Successfully logged in } SocketMethod.INVITE.methodName -> { // Incoming call! val invite = data.result as InviteResponse // Show incoming call UI, then accept: telnyxClient.acceptCall( invite.callId, invite.callerIdNumber ) } SocketMethod.ANSWER.methodName -> { // Call was answered } SocketMethod.BYE.methodName -> { // Call ended } SocketMethod.RINGING.methodName -> { // Remote party is ringing } } } } SocketStatus.ERROR -> { // Handle error: response.errorCode } SocketStatus.DISCONNECT -> { // Socket disconnected } } } }
kotlin// Get current call val currentCall: Call? = telnyxClient.calls[callId] // End call currentCall?.endCall(callId) // Mute/Unmute currentCall?.onMuteUnmutePressed() // Hold/Unhold currentCall?.onHoldUnholdPressed(callId) // Send DTMF tone currentCall?.dtmf(callId, "1")
kotlin// Get all active calls val calls: Map<UUID, Call> = telnyxClient.calls // Iterate through calls calls.forEach { (callId, call) -> // Handle each call }
Add Firebase to your project and get an FCM token:
kotlinFirebaseMessaging.getInstance().token.addOnCompleteListener { task -> if (task.isSuccessful) { val fcmToken = task.result // Use this token in your login config } }
In your FirebaseMessagingService:
kotlinclass MyFirebaseService : FirebaseMessagingService() { override fun onMessageReceived(remoteMessage: RemoteMessage) { val params = remoteMessage.data val metadata = JSONObject(params as Map<*, *>).getString("metadata") // Check for missed call if (params["message"] == "Missed call!") { // Show missed call notification return } // Show incoming call notification (use Foreground Service) showIncomingCallNotification(metadata) } }
kotlin// The SDK now handles decline automatically telnyxClient.connectWithDeclinePush( txPushMetaData = pushMetaData, credentialConfig = credentialConfig ) // SDK connects, sends decline, and disconnects automatically
xml<service android:name=".YourForegroundService" android:foregroundServiceType="phoneCall" android:exported="true" />
Enable metrics to monitor call quality in real-time:
kotlinval credentialConfig = CredentialConfig( // ... other config debug = true // Enables call quality metrics ) // Listen for quality updates lifecycleScope.launch { currentCall?.callQualityFlow?.collect { metrics -> println("MOS: ${metrics.mos}") println("Jitter: ${metrics.jitter * 1000} ms") println("RTT: ${metrics.rtt * 1000} ms") println("Quality: ${metrics.quality}") // EXCELLENT, GOOD, FAIR, POOR, BAD } }
| Quality Level | MOS Range | |---------------|-----------| | EXCELLENT | > 4.2 | | GOOD | 4.1 - 4.2 | | FAIR | 3.7 - 4.0 | | POOR | 3.1 - 3.6 | | BAD | ≤ 3.0 |
Connect to a Telnyx Voice AI Agent without traditional SIP credentials:
kotlintelnyxClient.connectAnonymously( targetId = "your_ai_assistant_id", targetType = "ai_assistant", // Default targetVersionId = "optional_version_id", userVariables = mapOf("user_id" to "12345") )
kotlin// After anonymous login, call the AI Agent telnyxClient.newInvite( callerName = "User Name", callerNumber = "+15551234567", destinationNumber = "", // Ignored for AI Agent clientState = "state", customHeaders = mapOf( "X-Account-Number" to "123", // Maps to {{account_number}} "X-User-Tier" to "premium" // Maps to {{user_tier}} ) )
kotlinlifecycleScope.launch { telnyxClient.transcriptUpdateFlow.collect { transcript -> transcript.forEach { item -> println("${item.role}: ${item.content}") // role: "user" or "assistant" } } }
kotlin// Send text message during active call telnyxClient.sendAIAssistantMessage("Hello, I need help with my account")
Implement your own logger:
kotlinclass MyLogger : TxLogger { override fun log(level: LogLevel, tag: String?, message: String, throwable: Throwable?) { // Send to your logging service MyAnalytics.log(level.name, tag ?: "Telnyx", message) } } val config = CredentialConfig( // ... other config logLevel = LogLevel.ALL, customLogger = MyLogger() )
If using code obfuscation, add to proguard-rules.pro:
proguard-keep class com.telnyx.webrtc.** { *; } -dontwarn kotlin.Experimental$Level -dontwarn kotlin.Experimental -dontwarn kotlinx.coroutines.scheduling.ExperimentalCoroutineDispatcher
| Issue | Solution | |-------|----------| | No audio | Check RECORD_AUDIO permission is granted | | Push not received | Verify FCM token is passed in config | | Login fails | Verify SIP credentials in Telnyx Portal | | Call drops | Check network stability, enable autoReconnect | | sender_id_mismatch (push) | FCM project mismatch - ensure app's google-services.json matches server credentials |
<!-- BEGIN AUTO-GENERATED API REFERENCE -- do not edit below this line -->
references/webrtc-server-api.md has the server-side WebRTC API — credential creation, token generation, and push notification setup. You MUST read it when setting up authentication or push notifications.
TelnyxClient is the main entry point for interacting with the Telnyx WebRTC SDK. It handles connection management, call creation, and responses from the Telnyx platform.
newInvite), accept (acceptCall), and end (endCall) calls.TxSocketListener to process events from the socket, such as incoming calls (onOfferReceived), call answers (onAnswerReceived), call termination (onByeReceived), and errors (onErrorReceived).SharedFlow (recommended: socketResponseFlow) and deprecated LiveData (e.g., socketResponseLiveData) for UI consumption.TxSocket: Manages the underlying WebSocket communication.TxSocketListener: An interface implemented by TelnyxClient to receive and process socket events. Notably:onOfferReceived(jsonObject: JsonObject): Handles incoming call invitations.onAnswerReceived(jsonObject: JsonObject): Processes answers to outgoing calls.onByeReceived(jsonObject: JsonObject): Handles call termination notifications. The jsonObject now contains richer details including cause, causeCode, sipCode, and sipReason, allowing the client to populate CallState.DONE with a detailed CallTerminationReason.onErrorReceived(jsonObject: JsonObject): Manages errors reported by the socket or platform.onClientReady(jsonObject: JsonObject): Indicates the client is ready for operations after connection and initial setup.onGatewayStateReceived(gatewayState: String, receivedSessionId: String?): Provides updates on the registration status with the Telnyx gateway.Call Class: Represents individual call sessions. TelnyxClient creates and manages instances of Call.CallState: The client updates the CallState of individual Call objects based on socket events and network conditions. This includes states like DROPPED(reason: CallNetworkChangeReason), RECONNECTING(reason: CallNetworkChangeReason), and DONE(reason: CallTerminationReason?) which now provide more context.socketResponseFlow: SharedFlow<SocketResponse<ReceivedMessageBody>>: This SharedFlow stream is the recommended approach for applications. It emits SocketResponse objects that wrap messages received from the Telnyx platform. For BYE messages, the ReceivedMessageBody will contain a com.telnyx.webrtc.sdk.verto.receive.ByeResponse which is now enriched with termination cause details.socketResponseLiveData: LiveData<SocketResponse<ReceivedMessageBody>>: DEPRECATED] This LiveData stream is deprecated in favor of socketResponseFlow. It's maintained for backward compatibility but new implementations should use SharedFlow.Recommended approach using SharedFlow:
kotlin// Initializing the client val telnyxClient = TelnyxClient(context) // Observing responses using SharedFlow (Recommended) lifecycleScope.launch { telnyxClient.socketResponseFlow.collect { response -> when (response.status) { SocketStatus.MESSAGERECEIVED -> { response.data?.let { when (it.method) { SocketMethod.INVITE.methodName -> { val invite = it.result as InviteResponse // Handle incoming call invitation } SocketMethod.BYE.methodName -> { val bye = it.result as com.telnyx.webrtc.sdk.verto.receive.ByeResponse // Call ended by remote party, bye.cause, bye.sipCode etc. are available Log.d("TelnyxClient", "Call ended: ${bye.callId}, Reason: ${bye.cause}") } // Handle other methods like ANSWER, RINGING, etc. } } } SocketStatus.ERROR -> { // Handle errors Log.e("TelnyxClient", "Error: ${response.errorMessage}") } // Handle other statuses: ESTABLISHED, LOADING, DISCONNECT } } }
Deprecated approach using LiveData:
kotlin@Deprecated("Use socketResponseFlow instead. LiveData is deprecated in favor of Kotlin Flows.") // Observing responses (including errors and BYE messages) telnyxClient.socketResponseLiveData.observe(lifecycleOwner, Observer { response -> when (response.status) { SocketStatus.MESSAGERECEIVED -> { response.data?.let { when (it.method) { SocketMethod.INVITE.methodName -> { val invite = it.result as InviteResponse // Handle incoming call invitation } SocketMethod.BYE.methodName -> { val bye = it.result as com.telnyx.webrtc.sdk.verto.receive.ByeResponse // Call ended by remote party, bye.cause, bye.sipCode etc. are available Log.d("TelnyxClient", "Call ended: ${bye.callId}, Reason: ${bye.cause}") } // Handle other methods like ANSWER, RINGING, etc. } } } SocketStatus.ERROR -> { // Handle errors Log.e("TelnyxClient", "Error: ${response.errorMessage}") } // Handle other statuses: ESTABLISHED, LOADING, DISCONNECT } }) // Connecting and Logging In (example with credentials) telnyxClient.connect( credentialConfig = CredentialConfig( sipUser = "your_sip_username", sipPassword = "your_sip_password", // ... other config ... ) ) // Making a call val outgoingCall = telnyxClient.newInvite( callerName = "My App", callerNumber = "+11234567890", destinationNumber = "+10987654321", clientState = "some_state" ) // Observing the specific call's state outgoingCall.callStateFlow.collect { state -> if (state is CallState.DONE) { Log.d("TelnyxClient", "Outgoing call ended. Reason: ${state.reason?.cause}") } // Handle other states }
Refer to the SDK's implementation and specific method documentation for detailed usage patterns and configuration options.
NOTE: Remember to add and handle INTERNET, RECORD_AUDIO and ACCESS_NETWORK_STATE permissions
<p align="center"> <img align="center" src="https://user-images.githubusercontent.com/9112652/117322479-f4731c00-ae85-11eb-9259-6333fc20b629.png" /> </p>
To initialize the TelnyxClient you will have to provide the application context.
kotlintelnyxClient = TelnyxClient(context)
Once an instance is created, you can call the one of two available .connect(....) method to connect to the socket.
kotlinfun connect( providedServerConfig: TxServerConfiguration = TxServerConfiguration(), credentialConfig: CredentialConfig, txPushMetaData: String? = null, autoLogin: Boolean = true, )
We need to react for a socket connection state or incoming calls. We do this by getting the Telnyx Socket Response callbacks from our TelnyxClient.
kotlinval socketResponseFlow: SharedFlow<SocketResponse<ReceivedMessageBody>>
Class that represents a Call and handles all call related actions, including answering and ending a call.
In order to make a call invitation, you need to provide your callerName, callerNumber, the destinationNumber (or SIP credential), and your clientState (any String value).
kotlintelnyxClient.call.newInvite(callerName, callerNumber, destinationNumber, clientState)
In order to be able to accept a call, we first need to listen for invitations. We do this by getting the Telnyx Socket Response as LiveData:
kotlinfun getSocketResponse(): LiveData<SocketResponse<ReceivedMessageBody>>? = telnyxClient.getSocketResponse()
The Telnyx WebRTC SDK allows for multiple calls to be handled at once. You can use the callId to differentiate the calls..
callId: UUID: A unique identifier for the call.sessionId: String: The session ID associated with the Telnyx connection.callStateFlow: StateFlow<CallState>: A Kotlin Flow that emits updates to the call's current state. This is the primary way to observe real-time changes to the call. States include:CallState.NEW: The call has been locally initiated but not yet sent.CallState.CONNECTING: The call is in the process of connecting.CallState.RINGING: The call invitation has been sent, and the remote party is being alerted.CallState.ACTIVE: The call is established and active.CallState.HELD: The call is on hold.CallState.DONE(reason: CallTerminationReason?): The call has ended. The optional reason parameter provides details about why the call terminated (e.g., normal hangup, call rejected, busy, SIP error). CallTerminationReason contains cause, causeCode, sipCode, and sipReason.CallState.ERROR: An error occurred related to this call.CallState.DROPPED(reason: CallNetworkChangeReason): The call was dropped, typically due to network issues. The reason (CallNetworkChangeReason.NETWORK_LOST or CallNetworkChangeReason.NETWORK_SWITCH) provides context.CallState.RECONNECTING(reason: CallNetworkChangeReason): The SDK is attempting to reconnect the call after a network disruption. The reason provides context.onCallQualityChange: ((CallQualityMetrics) -> Unit)?: A callback for real-time call quality metrics.audioManager: AudioManager: Reference to the Android AudioManager for controlling audio settings.peerConnection: Peer?: Represents the underlying WebRTC peer connection.newInvite(...): (Typically initiated via TelnyxClient) Initiates a new outgoing call.acceptCall(...): (Typically initiated via TelnyxClient) Accepts an incoming call.endCall(callId: UUID): Terminates the call. This is usually called on the TelnyxClient which then manages the specific Call object.onMuteUnmutePressed(): Toggles the microphone mute state.onLoudSpeakerPressed(): Toggles the loudspeaker state.onHoldUnholdPressed(callId: UUID): Toggles the hold state for the call.dtmf(callId: UUID, tone: String): Sends DTMF tones.Applications should observe the callStateFlow to react to changes in the call's status and update the UI accordingly. For example, displaying call duration when ACTIVE, showing a "reconnecting" indicator when RECONNECTING, or presenting termination reasons when DONE.
kotlin// Example: Observing call state in a ViewModel or Composable viewModelScope.launch { myCall.callStateFlow.collect { state -> when (state) { is CallState.ACTIVE -> { // Update UI to show active call controls } is CallState.DONE -> { // Call has ended, update UI // Access state.reason for termination details val reasonDetails = state.reason?.let { "Cause: ${it.cause}, SIP Code: ${it.sipCode}" } ?: "No specific reason provided." Log.d("Call Ended", "Reason: $reasonDetails") } is CallState.DROPPED -> { // Call dropped, possibly show a message with state.reason.description Log.d("Call Dropped", "Reason: ${state.callNetworkChangeReason.description}") } is CallState.RECONNECTING -> { // Call is reconnecting, update UI Log.d("Call Reconnecting", "Reason: ${state.callNetworkChangeReason.description}") } // Handle other states like NEW, CONNECTING, RINGING, HELD, ERROR else -> { /* ... */ } } } }
For more details on specific parameters and advanced usage, refer to the SDK's source code and the main TelnyxClient documentation.
A data class the represents the structure of every message received via the socket connection
kotlindata class ReceivedMessageBody(val method: String, val result: ReceivedResult?)
Where the params are:
ReceivedResultEnum class to detail the Method property of the response from the Telnyx WEBRTC client with the given methodName]
kotlindata class ReceivedMessageBody( val method: String, // The Telnyx Message Method (e.g., "telnyx_rtc.invite", "telnyx_rtc.bye") val result: ReceivedResult? // The content of the actual message )
method: String: This field indicates the type of message received. It corresponds to one of the SocketMethod enums (e.g., SocketMethod.INVITE, SocketMethod.ANSWER, SocketMethod.BYE). Your application will typically use this field in a when statement to determine how to process the result.result: ReceivedResult?: This field holds the actual payload of the message. ReceivedResult is a sealed class, and the concrete type of result will depend on the method. For example:method is SocketMethod.LOGIN.methodName, result will be a LoginResponse.method is SocketMethod.INVITE.methodName, result will be an InviteResponse.method is SocketMethod.ANSWER.methodName, result will be an AnswerResponse.method is SocketMethod.BYE.methodName, result will be a com.telnyx.webrtc.sdk.verto.receive.ByeResponse. Importantly, this ByeResponse now includes detailed termination information such as cause, causeCode, sipCode, and sipReason, in addition to the callId.ReceivedResult subtypes include RingingResponse, MediaResponse, and DisablePushResponse.When you observe TelnyxClient.socketResponseLiveData, you receive a SocketResponse<ReceivedMessageBody>. If the status is SocketStatus.MESSAGERECEIVED, the data field of SocketResponse will contain the ReceivedMessageBody.
kotlintelnyxClient.socketResponseLiveData.observe(this, Observer { response -> if (response.status == SocketStatus.MESSAGERECEIVED) { response.data?.let { receivedMessageBody -> Log.d("SDK_APP", "Method: ${receivedMessageBody.method}") when (receivedMessageBody.method) { SocketMethod.LOGIN.methodName -> { val loginResponse = receivedMessageBody.result as? LoginResponse // Process login response } SocketMethod.INVITE.methodName -> { val inviteResponse = receivedMessageBody.result as? InviteResponse // Process incoming call invitation } SocketMethod.BYE.methodName -> { val byeResponse = receivedMessageBody.result as? com.telnyx.webrtc.sdk.verto.receive.ByeResponse byeResponse?.let { // Process call termination, access it.cause, it.sipCode, etc. Log.i("SDK_APP", "Call ${it.callId} ended. Reason: ${it.cause}, SIP Code: ${it.sipCode}") } } // Handle other methods... } } } })
By checking the method and casting the result to its expected type, your application can effectively handle the diverse messages sent by the Telnyx platform.
<!-- END AUTO-GENERATED API REFERENCE -->
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-04 | fail→pass | 18,928 | 10,312 | -46% | 1 | 1 | 0% | 3,586 | 8,347 | +133% | 0 | 0 | — |
case-05 | fail→pass | 15,620 | 4,489 | -71% | 1 | 1 | 0% | 3,100 | 7,421 | +139% | 0 | 0 | — |
case-06 | fail→pass | 13,811 | 8,088 | -41% | 1 | 1 | 0% | 2,280 | 8,308 | +264% | 0 | 0 | — |
case-07 | fail→pass | 14,618 | 5,122 | -65% | 1 | 1 | 0% | 2,588 | 7,614 | +194% | 0 | 0 | — |
case-01 | fail→fail | 16,387 | 8,847 | -46% | 1 | 1 | 0% | 3,248 | 8,519 | +162% | 0 | 0 | — |
case-02 | fail→pass | 19,115 | 9,894 | -48% | 1 | 1 | 0% | 3,408 | 8,676 | +155% | 0 | 0 | — |
case-03 | fail→pass | 22,246 | 10,299 | -54% | 1 | 1 | 0% | 4,262 | 8,744 | +105% | 0 | 0 | — |
case-08 | fail→pass | 10,514 | 6,273 | -40% | 1 | 1 | 0% | 1,974 | 7,935 | +302% | 0 | 0 | — |
case-09 | fail→pass | 12,758 | 4,691 | -63% | 1 | 1 | 0% | 2,308 | 7,552 | +227% | 0 | 0 | — |
case-10 | fail→pass | 18,590 | 5,400 | -71% | 1 | 1 | 0% | 3,126 | 7,641 | +144% | 0 | 0 | — |
case-11 | pass→pass | 5,162 | 2,305 | -55% | 1 | 1 | 0% | 881 | 6,990 | +693% | 0 | 0 | — |
case-12 | fail→pass | 16,066 | 5,132 | -68% | 1 | 1 | 0% | 2,699 | 7,645 | +183% | 0 | 0 | — |
case-13 | fail→pass | 13,752 | 5,777 | -58% | 1 | 1 | 0% | 2,475 | 7,749 | +213% | 0 | 0 | — |
case-14 | fail→pass | 11,099 | 4,639 | -58% | 1 | 1 | 0% | 1,936 | 7,409 | +283% | 0 | 0 | — |
case-15 | pass→pass | 9,874 | 3,959 | -60% | 1 | 1 | 0% | 1,816 | 7,379 | +306% | 0 | 0 | — |
case-16 | fail→pass | 17,738 | 6,458 | -64% | 1 | 1 | 0% | 3,343 | 7,830 | +134% | 0 | 0 | — |
case-17 | fail→pass | 16,568 | 5,716 | -65% | 1 | 1 | 0% | 2,942 | 7,770 | +164% | 0 | 0 | — |
case-18 | fail→pass | 15,079 | 5,509 | -63% | 1 | 1 | 0% | 2,740 | 7,723 | +182% | 0 | 0 | — |
case-19 | fail→pass | 14,889 | 4,183 | -72% | 1 | 1 | 0% | 2,630 | 7,375 | +180% | 0 | 0 | — |
case-20 | pass→fail | 13,745 | 8,582 | -38% | 1 | 1 | 0% | 2,850 | 8,380 | +194% | 0 | 0 | — |
case-21 | pass→pass | 8,076 | 7,260 | -10% | 1 | 1 | 0% | 1,675 | 8,179 | +388% | 0 | 0 | — |
case-22 | pass→pass | 10,882 | 8,120 | -25% | 1 | 1 | 0% | 2,195 | 8,307 | +278% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +68 percentage points is the difference between those two pass rates over the 22 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.