Ghost credentials allow you to create identity records and assign points to users before they’ve connected to your realm. This is powerful for pre-seeding rewards, running off-platform campaigns, or migrating users from external systems.
Overview
A ghost credential is a non-verified identity record (Twitter ID, wallet address, email, etc.) that can accumulate point balances independently. When the user eventually connects that identity to your realm, the ghost credential is linked to their account and all accumulated balances are transferred.
Ghost credentials are realm-specific. The same Twitter ID can exist as separate ghost credentials in different realms, each with their own point balances.
Use Cases
Pre-Credit Campaigns Award points to Twitter followers or wallet holders before they join your community
Off-Platform Activity Track and reward activity that happens outside your realm (external events, partnerships)
User Migration Import users from external systems with their pre-existing point balances
Email Onboarding Assign welcome bonuses to email addresses before users complete signup
Credential Types
Ghost credentials support multiple identity formats:
Type Description Example Value twitter-idTwitter/X user ID 1234567890discord-idDiscord user ID 123456789012345678walletBlockchain wallet address 0x742d35Cc6634C0532925a3b844Bc454e4438f44eemailEmail address [email protected] customCustom identifier (requires source) customer_12345
Creating Credentials
Social Credentials
Create credentials for social platform identities like Twitter or Discord:
async function createTwitterCredential ( realmId , twitterId , username ) {
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/social` ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
provider: 'twitter' ,
providerId: twitterId ,
username: username
})
}
);
return response . json ();
}
// Create a ghost credential for a Twitter user
const credential = await createTwitterCredential (
'YOUR_REALM_ID' ,
'1234567890' ,
'twitter_handle'
);
Wallet Credentials
Create credentials for blockchain wallet addresses:
async function createWalletCredential ( realmId , address , chain ) {
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/wallet` ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
address: address ,
chain: chain
})
}
);
return response . json ();
}
// Create a ghost credential for an Ethereum wallet
const credential = await createWalletCredential (
'YOUR_REALM_ID' ,
'0x742d35Cc6634C0532925a3b844Bc454e4438f44e' ,
'ethereum'
);
Finding Credentials
Look up existing ghost credentials by their identifier:
async function findCredential ( realmId , type , value ) {
const params = new URLSearchParams ({ type , value });
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/find? ${ params } ` ,
{
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } `
}
}
);
if ( response . status === 404 ) {
return null ; // Credential doesn't exist
}
return response . json ();
}
// Find by Twitter ID
const twitterCred = await findCredential ( 'YOUR_REALM_ID' , 'twitter-id' , '1234567890' );
// Find by wallet address
const walletCred = await findCredential ( 'YOUR_REALM_ID' , 'wallet' , '0x742d35Cc...' );
// Find by email
const emailCred = await findCredential ( 'YOUR_REALM_ID' , 'email' , '[email protected] ' );
Managing Balances
Update Balance
Award or deduct points from a ghost credential:
async function updateCredentialBalance ( realmId , type , value , amount , realmPointId = null ) {
const params = new URLSearchParams ({ type , value });
const body = { amount };
if ( realmPointId ) {
body . realmPointId = realmPointId ;
}
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/balance? ${ params } ` ,
{
method: 'PATCH' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( body )
}
);
return response . json ();
}
// Award 100 points to a Twitter user (uses default currency)
await updateCredentialBalance ( 'YOUR_REALM_ID' , 'twitter-id' , '1234567890' , 100 );
// Deduct 50 points from a wallet
await updateCredentialBalance ( 'YOUR_REALM_ID' , 'wallet' , '0x742d35Cc...' , - 50 );
// Award 200 of a specific currency
await updateCredentialBalance ( 'YOUR_REALM_ID' , 'email' , '[email protected] ' , 200 , 'CURRENCY_ID' );
If realmPointId is not specified, the realm’s default currency is used.
Batch Updates
Update multiple credentials in a single request:
async function batchUpdateBalances ( realmId , updates ) {
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/transaction` ,
{
method: 'PATCH' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({ updates })
}
);
return response . json ();
}
// Batch award points to multiple credentials
const result = await batchUpdateBalances ( 'YOUR_REALM_ID' , [
{ type: 'twitter-id' , value: '1234567890' , amount: 100 },
{ type: 'twitter-id' , value: '9876543210' , amount: 150 },
{ type: 'wallet' , value: '0x742d35Cc...' , amount: 200 },
{ type: 'email' , value: '[email protected] ' , amount: 75 }
]);
console . log ( `Success: ${ result . results . length } , Errors: ${ result . errors . length } ` );
Transfer Points
Transfer points between two credentials:
async function transferPoints ( realmId , from , to , amount , realmPointId = null ) {
const params = new URLSearchParams ({
fromType: from . type ,
fromValue: from . value ,
toType: to . type ,
toValue: to . value
});
const body = { amount };
if ( realmPointId ) {
body . realmPointId = realmPointId ;
}
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/transfer? ${ params } ` ,
{
method: 'PATCH' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ( body )
}
);
return response . json ();
}
// Transfer 50 points from one Twitter user to another
await transferPoints (
'YOUR_REALM_ID' ,
{ type: 'twitter-id' , value: '1234567890' },
{ type: 'twitter-id' , value: '9876543210' },
50
);
// Transfer points from a wallet to an email
await transferPoints (
'YOUR_REALM_ID' ,
{ type: 'wallet' , value: '0x742d35Cc...' },
{ type: 'email' , value: '[email protected] ' },
100
);
Linking to Accounts
When a user connects their identity to your realm, you can link the ghost credential to their account. This transfers all accumulated balances to their account.
async function linkCredentialToAccount ( realmId , type , value , accountId ) {
const params = new URLSearchParams ({ type , value , accountId });
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/link? ${ params } ` ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } `
}
}
);
if ( response . status === 204 ) {
return { success: true };
}
return response . json ();
}
// Link a Twitter credential to a user's account
await linkCredentialToAccount (
'YOUR_REALM_ID' ,
'twitter-id' ,
'1234567890' ,
'ACCOUNT_ID'
);
Linking is a one-way operation. Once linked, ghost balances are transferred to the account and the credential cannot be unlinked. The credential will still exist but future balance updates will go directly to the account.
What Happens When Linking
Ghost credential is found by identifier
All GhostCredentialPointBalance records are transferred to AccountPointBalance
Ghost balance records are deleted
Credential gets accountId set
If the account has a verified credential with the same identity, the ghost credential can auto-upgrade to verified
Store custom data on credentials for your application’s needs:
async function updateCredentialMetadata ( realmId , type , value , metadata ) {
const params = new URLSearchParams ({ type , value });
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/metadata? ${ params } ` ,
{
method: 'PATCH' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({ metadata })
}
);
return response . status === 204 ;
}
// Store custom metadata
await updateCredentialMetadata (
'YOUR_REALM_ID' ,
'twitter-id' ,
'1234567890' ,
{
source: 'airdrop_campaign_2024' ,
tier: 'gold' ,
referredBy: 'partner_xyz'
}
);
Metadata is returned when finding credentials and can be used to track campaign attribution, user segments, or any custom data your application needs.
Deleting Credentials
Remove ghost credentials that are no longer needed:
async function deleteCredential ( realmId , type , value ) {
const params = new URLSearchParams ({ type , value });
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials? ${ params } ` ,
{
method: 'DELETE' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } `
}
}
);
return response . status === 204 ;
}
// Delete a ghost credential
await deleteCredential ( 'YOUR_REALM_ID' , 'twitter-id' , '1234567890' );
You cannot delete credentials that have non-zero point balances. Zero out the balance first or link the credential to transfer the balance.
Custom Credential Types
For identifiers that don’t fit the standard types, use the custom type with a source parameter:
// Create a custom credential (e.g., Shopify customer ID)
const response = await fetch (
`https://api.drip.re/api/v1/realms/ ${ realmId } /credentials/social` ,
{
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ process . env . DRIP_API_KEY } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
provider: 'shopify' , // Your custom source
providerId: 'customer_12345' ,
username: 'Customer Name'
})
}
);
// Find by custom type
const credential = await findCredential ( 'YOUR_REALM_ID' , 'custom' , 'customer_12345' );
// Note: For custom types, include source parameter:
// ?type=custom&value=customer_12345&source=shopify
Best Practices
Batch Operations Use batch endpoints when updating multiple credentials to reduce API calls and improve performance
Idempotent Creates Creating a credential that already exists returns a 409 Conflict - check first or handle the error
Track Attribution Use metadata to track where credentials came from (campaigns, partners, events)
Clean Up Periodically review and delete unused credentials with zero balances
Error Handling
Common errors and how to handle them:
Error Cause Solution 409 ConflictCredential already exists Find existing credential instead of creating 404 Not FoundCredential doesn’t exist Create the credential first 400 INSUFFICIENT_BALANCETrying to deduct more than available Check balance before deducting 400 CANNOT_DELETE_WITH_BALANCESCredential has non-zero balance Zero out balance or link first
Next Steps